diff --git a/.dev_scripts/diff_images.py b/.dev_scripts/diff_images.py
index e21cae214e9..5208ed41ecb 100644
--- a/.dev_scripts/diff_images.py
+++ b/.dev_scripts/diff_images.py
@@ -20,13 +20,13 @@ def calc_images_mean_L1(image1_path, image2_path):
def parse_args():
parser = argparse.ArgumentParser()
- parser.add_argument('image1_path')
- parser.add_argument('image2_path')
+ parser.add_argument("image1_path")
+ parser.add_argument("image2_path")
args = parser.parse_args()
return args
-if __name__ == '__main__':
+if __name__ == "__main__":
args = parse_args()
mean_L1 = calc_images_mean_L1(args.image1_path, args.image2_path)
print(mean_L1)
diff --git a/.dev_scripts/test_regression_txt2img_dream_v1_4.sh b/.dev_scripts/test_regression_txt2img_dream_v1_4.sh
index 11cbf8f14b8..9326d3c311c 100644
--- a/.dev_scripts/test_regression_txt2img_dream_v1_4.sh
+++ b/.dev_scripts/test_regression_txt2img_dream_v1_4.sh
@@ -5,8 +5,7 @@ SAMPLES_DIR=${OUT_DIR}
python scripts/dream.py \
--from_file ${PROMPT_FILE} \
--outdir ${OUT_DIR} \
- --sampler plms \
- --full_precision
+ --sampler plms
# original output by CompVis/stable-diffusion
IMAGE1=".dev_scripts/images/v1_4_astronaut_rides_horse_plms_step50_seed42.png"
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000000..602ffade5d3
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,11 @@
+*
+!invokeai
+!pyproject.toml
+!uv.lock
+!docker/docker-entrypoint.sh
+!LICENSE
+
+**/dist
+**/node_modules
+**/__pycache__
+**/*.egg-info
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000000..d4b0972edab
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# All files
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Python
+[*.py]
+indent_size = 4
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 00000000000..5c04dc964ef
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,5 @@
+b3dccfaeb636599c02effc377cdd8a87d658256c
+218b6d0546b990fc449c876fb99f44b50c4daa35
+182580ff6970caed400be178c5b888514b75d7f2
+8e9d5c1187b0d36da80571ce4c8ba9b3a37b6c46
+99aac5870e1092b182e6c5f21abcaab6936a4ad1
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index 632f491b7c7..6cf175e7c5a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,7 @@
# Auto normalizes line endings on commit so devs don't need to change local settings.
-# 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/
* text=auto
+docker/** text eol=lf
+tests/test_model_probe/stripped_models/** filter=lfs diff=lfs merge=lfs -text
+tests/model_identification/stripped_models/** filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/AGENTS.md b/.github/AGENTS.md
new file mode 100644
index 00000000000..9f701a7e33d
--- /dev/null
+++ b/.github/AGENTS.md
@@ -0,0 +1,24 @@
+# Agent Instructions
+
+## Package Management
+
+This project uses **pnpm** exclusively for package management in the frontend (`invokeai/frontend/web/`).
+
+- ✅ Use `pnpm` commands (e.g., `pnpm install`, `pnpm run`)
+- ❌ Never use `npm` or `yarn` commands
+- ❌ Never suggest creating or using `package-lock.json` or `yarn.lock`
+- ✅ The lock file is `pnpm-lock.yaml`
+
+Use the following pnpm commands for typical operations:
+
+- pnpm -C invokeai/frontend/web install
+- pnpm -C invokeai/frontend/web build
+- pnpm -C invokeai/frontend/web lint:tsc
+- pnpm -C invokeai/frontend/web lint:dpdm
+- pnpm -C invokeai/frontend/web lint:eslint
+- pnpm -C invokeai/frontend/web lint:prettier
+
+## Project Structure
+
+- Backend: Python in `invokeai/`
+- Frontend: TypeScript/React in `invokeai/frontend/web/` (uses pnpm)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000000..f62b8c90f11
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,30 @@
+# continuous integration
+/.github/workflows/ @lstein @blessedcoolant
+
+# documentation - anyone with write privileges can review
+/docs/
+
+# nodes
+/invokeai/app/ @blessedcoolant @lstein @dunkeroni @JPPhoto
+
+# installation and configuration
+/pyproject.toml @lstein @blessedcoolant
+/docker/ @lstein @blessedcoolant
+/scripts/ @lstein @blessedcoolant
+/installer/ @lstein @blessedcoolant
+/invokeai/assets @lstein @blessedcoolant
+/invokeai/configs @lstein @blessedcoolant
+/invokeai/version @lstein @blessedcoolant
+
+# web ui
+/invokeai/frontend @blessedcoolant @lstein @dunkeroni
+
+# generation, model management, postprocessing
+/invokeai/backend @lstein @blessedcoolant @dunkeroni @JPPhoto @Pfannkuchensack
+
+# front ends
+/invokeai/frontend/CLI @lstein
+/invokeai/frontend/install @lstein
+/invokeai/frontend/merge @lstein @blessedcoolant
+/invokeai/frontend/training @lstein @blessedcoolant
+/invokeai/frontend/web @blessedcoolant @lstein @dunkeroni @Pfannkuchensack
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
new file mode 100644
index 00000000000..d49271b7d45
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml
@@ -0,0 +1,160 @@
+name: 🐞 Bug Report
+
+description: File a bug report
+
+title: '[bug]: '
+
+labels: ['bug']
+
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this Bug Report!
+
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this problem?
+ description: |
+ Please [search](https://github.com/invoke-ai/InvokeAI/issues) first to see if an issue already exists for the problem.
+ options:
+ - label: I have searched the existing issues
+ required: true
+
+ - type: dropdown
+ id: install_method
+ attributes:
+ label: Install method
+ description: How did you install Invoke?
+ multiple: false
+ options:
+ - "Invoke's Launcher"
+ - 'Stability Matrix'
+ - 'Pinokio'
+ - 'Manual'
+ validations:
+ required: true
+
+ - type: markdown
+ attributes:
+ value: __Describe your environment__
+
+ - type: dropdown
+ id: os_dropdown
+ attributes:
+ label: Operating system
+ description: Your computer's operating system.
+ multiple: false
+ options:
+ - 'Linux'
+ - 'Windows'
+ - 'macOS'
+ - 'other'
+ validations:
+ required: true
+
+ - type: dropdown
+ id: gpu_dropdown
+ attributes:
+ label: GPU vendor
+ description: Your GPU's vendor.
+ multiple: false
+ options:
+ - 'Nvidia (CUDA)'
+ - 'AMD (ROCm)'
+ - 'Apple Silicon (MPS)'
+ - 'None (CPU)'
+ validations:
+ required: true
+
+ - type: input
+ id: gpu_model
+ attributes:
+ label: GPU model
+ description: Your GPU's model. If on Apple Silicon, this is your Mac's chip. Leave blank if on CPU.
+ placeholder: ex. RTX 2080 Ti, Mac M1 Pro
+ validations:
+ required: false
+
+ - type: input
+ id: vram
+ attributes:
+ label: GPU VRAM
+ description: Your GPU's VRAM. If on Apple Silicon, this is your Mac's unified memory. Leave blank if on CPU.
+ placeholder: 8GB
+ validations:
+ required: false
+
+ - type: input
+ id: version-number
+ attributes:
+ label: Version number
+ description: |
+ The version of Invoke you have installed. If it is not the [latest version](https://github.com/invoke-ai/InvokeAI/releases/latest), please update and try again to confirm the issue still exists. If you are testing main, please include the commit hash instead.
+ placeholder: ex. v6.0.2
+ validations:
+ required: true
+
+ - type: input
+ id: browser-version
+ attributes:
+ label: Browser
+ description: Your web browser and version, if you do not use the Launcher's provided GUI.
+ placeholder: ex. Firefox 123.0b3
+ validations:
+ required: false
+
+ - type: textarea
+ id: python-deps
+ attributes:
+ label: System Information
+ description: |
+ Click the gear icon at the bottom left corner, then click "About". Click the copy button and then paste here.
+ validations:
+ required: false
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: |
+ Describe what happened. Include any relevant error messages, stack traces and screenshots here.
+ placeholder: I clicked button X and then Y happened.
+ validations:
+ required: true
+
+ - type: textarea
+ id: what-you-expected
+ attributes:
+ label: What you expected to happen
+ description: Describe what you expected to happen.
+ placeholder: I expected Z to happen.
+ validations:
+ required: true
+
+ - type: textarea
+ id: how-to-repro
+ attributes:
+ label: How to reproduce the problem
+ description: List steps to reproduce the problem.
+ placeholder: Start the app, generate an image with these settings, then click button X.
+ validations:
+ required: false
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: Any other context that might help us to understand the problem.
+ placeholder: Only happens when there is full moon and Friday the 13th on Christmas Eve 🎅🏻
+ validations:
+ required: false
+
+ - type: input
+ id: discord-username
+ attributes:
+ label: Discord username
+ description: If you are on the Invoke discord and would prefer to be contacted there, please provide your username.
+ placeholder: supercoolusername123
+ validations:
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
new file mode 100644
index 00000000000..6d43d447f42
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
@@ -0,0 +1,53 @@
+name: Feature Request
+description: Contribute a idea or request a new feature
+title: '[enhancement]: '
+labels: ['enhancement']
+# assignees:
+# - lstein
+# - tildebyte
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to fill out this feature request!
+
+ - type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: |
+ Please make use of the [search function](https://github.com/invoke-ai/InvokeAI/labels/enhancement)
+ to see if a similar issue already exists for the feature you want to request
+ options:
+ - label: I have searched the existing issues
+ required: true
+
+ - type: input
+ id: contact
+ attributes:
+ label: Contact Details
+ description: __OPTIONAL__ How could we get in touch with you if we need more info (besides this issue)?
+ placeholder: ex. email@example.com, discordname, twitter, ...
+ validations:
+ required: false
+
+ - type: textarea
+ id: whatisexpected
+ attributes:
+ label: What should this feature add?
+ description: Explain the functionality this feature should add. Feature requests should be for single features. Please create multiple requests if you want to request multiple features.
+ placeholder: |
+ I'd like a button that creates an image of banana sushi every time I press it. Each image should be different. There should be a toggle next to the button that enables strawberry mode, in which the images are of strawberry sushi instead.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Alternatives
+ description: Describe alternatives you've considered
+ placeholder: A clear and concise description of any alternative solutions or features you've considered.
+
+ - type: textarea
+ attributes:
+ label: Additional Content
+ description: Add any other context or screenshots about the feature request here.
+ placeholder: This is a mockup of the design how I imagine it
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 5758867874d..00000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,36 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Describe your environment**
-- GPU: [cuda/amd/mps/cpu]
-- VRAM: [if known]
-- CPU arch: [x86/arm]
-- OS: [Linux/Windows/macOS]
-- Python: [Anaconda/miniconda/miniforge/pyenv/other (explain)]
-- Branch: [if `git status` says anything other than "On branch main" paste it here]
-- Commit: [run `git show` and paste the line that starts with "Merge" here]
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000000..6febd0917db
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,14 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Project-Documentation
+ url: https://invoke.ai/
+ about: Should be your first place to go when looking for manuals/FAQs regarding our InvokeAI Toolkit
+ - name: Discord
+ url: https://discord.gg/ZmtBAhwWhy
+ about: Our Discord Community could maybe help you out via live-chat
+ - name: GitHub Community Support
+ url: https://github.com/orgs/community/discussions
+ about: Please ask and answer questions regarding the GitHub Platform here.
+ - name: GitHub Security Bug Bounty
+ url: https://bounty.github.com/
+ about: Please report security vulnerabilities of the GitHub Platform here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index bbcbbe7d615..00000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an idea for this project
-title: ''
-labels: ''
-assignees: ''
-
----
-
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-**Additional context**
-Add any other context or screenshots about the feature request here.
diff --git a/.github/actions/install-frontend-deps/action.yml b/.github/actions/install-frontend-deps/action.yml
new file mode 100644
index 00000000000..1e6d3e6be80
--- /dev/null
+++ b/.github/actions/install-frontend-deps/action.yml
@@ -0,0 +1,33 @@
+name: install frontend dependencies
+description: Installs frontend dependencies with pnpm, with caching
+runs:
+ using: 'composite'
+ steps:
+ - name: setup node 22
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+
+ - name: setup pnpm
+ uses: pnpm/action-setup@v6
+ with:
+ version: 10
+ run_install: false
+
+ - name: get pnpm store directory
+ shell: bash
+ run: |
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+
+ - name: setup cache
+ uses: actions/cache@v5
+ with:
+ path: ${{ env.STORE_PATH }}
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pnpm-store-
+
+ - name: install frontend dependencies
+ run: pnpm install --prefer-frozen-lockfile
+ shell: bash
+ working-directory: invokeai/frontend/web
diff --git a/.github/pr_labels.yml b/.github/pr_labels.yml
new file mode 100644
index 00000000000..fdf11a470fb
--- /dev/null
+++ b/.github/pr_labels.yml
@@ -0,0 +1,59 @@
+root:
+- changed-files:
+ - any-glob-to-any-file: '*'
+
+python-deps:
+- changed-files:
+ - any-glob-to-any-file: 'pyproject.toml'
+
+python:
+- changed-files:
+ - all-globs-to-any-file:
+ - 'invokeai/**'
+ - '!invokeai/frontend/web/**'
+
+python-tests:
+- changed-files:
+ - any-glob-to-any-file: 'tests/**'
+
+ci-cd:
+- changed-files:
+ - any-glob-to-any-file: .github/**
+
+docker:
+- changed-files:
+ - any-glob-to-any-file: docker/**
+
+installer:
+- changed-files:
+ - any-glob-to-any-file: installer/**
+
+docs:
+- changed-files:
+ - any-glob-to-any-file: docs/**
+
+invocations:
+- changed-files:
+ - any-glob-to-any-file: 'invokeai/app/invocations/**'
+
+backend:
+- changed-files:
+ - any-glob-to-any-file: 'invokeai/backend/**'
+
+api:
+- changed-files:
+ - any-glob-to-any-file: 'invokeai/app/api/**'
+
+services:
+- changed-files:
+ - any-glob-to-any-file: 'invokeai/app/services/**'
+
+frontend-deps:
+- changed-files:
+ - any-glob-to-any-file:
+ - '**/*/package.json'
+ - '**/*/pnpm-lock.yaml'
+
+frontend:
+- changed-files:
+ - any-glob-to-any-file: 'invokeai/frontend/web/**'
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000000..84633de6ce1
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,23 @@
+## Summary
+
+
+
+## Related Issues / Discussions
+
+
+
+## QA Instructions
+
+
+
+## Merge Plan
+
+
+
+## Checklist
+
+- [ ] _The PR has a short but descriptive title, suitable for a changelog_
+- [ ] _Tests added / updated (if applicable)_
+- [ ] _❗Changes to a redux slice have a corresponding migration_
+- [ ] _Documentation added / updated (if applicable)_
+- [ ] _Updated `What's New` copy (if doing a release after this PR)_
diff --git a/.github/stale.yaml b/.github/stale.yaml
new file mode 100644
index 00000000000..b9150235fcc
--- /dev/null
+++ b/.github/stale.yaml
@@ -0,0 +1,19 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 28
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 14
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - pinned
+ - security
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Please
+ update the ticket if this is still a problem on the latest release.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: >
+ Due to inactivity, this issue has been automatically closed. If this is
+ still a problem on the latest release, please recreate the issue.
diff --git a/.github/workflows/build-container.yml b/.github/workflows/build-container.yml
new file mode 100644
index 00000000000..b5bd8637acd
--- /dev/null
+++ b/.github/workflows/build-container.yml
@@ -0,0 +1,118 @@
+name: build container image
+on:
+ push:
+ branches:
+ - 'main'
+ paths:
+ - 'pyproject.toml'
+ - '.dockerignore'
+ - 'invokeai/**'
+ - 'docker/Dockerfile'
+ - 'docker/docker-entrypoint.sh'
+ - 'workflows/build-container.yml'
+ tags:
+ - 'v*.*.*'
+ workflow_dispatch:
+ inputs:
+ push-to-registry:
+ description: Push the built image to the container registry
+ required: false
+ type: boolean
+ default: false
+
+permissions:
+ contents: write
+ packages: write
+
+jobs:
+ docker:
+ if: github.event.pull_request.draft == false
+ strategy:
+ fail-fast: false
+ matrix:
+ gpu-driver:
+ - cuda
+ - cpu
+ - rocm
+ runs-on: ubuntu-latest
+ name: ${{ matrix.gpu-driver }}
+ env:
+ # torch/arm64 does not support GPU currently, so arm64 builds
+ # would not be GPU-accelerated.
+ # re-enable arm64 if there is sufficient demand.
+ # PLATFORMS: 'linux/amd64,linux/arm64'
+ PLATFORMS: 'linux/amd64'
+ steps:
+ - name: Free up more disk space on the runner
+ # https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
+ # the /mnt dir has 70GBs of free space
+ # /dev/sda1 74G 28K 70G 1% /mnt
+ # According to some online posts the /mnt is not always there, so checking before setting docker to use it
+ run: |
+ echo "----- Free space before cleanup"
+ df -h
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ if [ -f /mnt/swapfile ]; then
+ sudo swapoff /mnt/swapfile
+ sudo rm -rf /mnt/swapfile
+ fi
+ if [ -d /mnt ]; then
+ sudo chmod -R 777 /mnt
+ echo '{"data-root": "/mnt/docker-root"}' | sudo tee /etc/docker/daemon.json
+ sudo systemctl restart docker
+ fi
+ echo "----- Free space after cleanup"
+ df -h
+
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ images: |
+ ghcr.io/${{ github.repository }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=tag
+ type=pep440,pattern={{version}}
+ type=pep440,pattern={{major}}.{{minor}}
+ type=pep440,pattern={{major}}
+ type=sha,enable=true,prefix=sha-,format=short
+ flavor: |
+ latest=${{ matrix.gpu-driver == 'cuda' && github.ref == 'refs/heads/main' }}
+ suffix=-${{ matrix.gpu-driver }},onlatest=false
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+ with:
+ platforms: ${{ env.PLATFORMS }}
+
+ - name: Login to GitHub Container Registry
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build container
+ timeout-minutes: 40
+ id: docker_build
+ uses: docker/build-push-action@v7
+ with:
+ context: .
+ file: docker/Dockerfile
+ platforms: ${{ env.PLATFORMS }}
+ build-args: |
+ GPU_DRIVER=${{ matrix.gpu-driver }}
+ push: ${{ github.ref == 'refs/heads/main' || github.ref_type == 'tag' || github.event.inputs.push-to-registry }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ # cache-from: |
+ # type=gha,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
+ # type=gha,scope=main-${{ matrix.gpu-driver }}
+ # cache-to: type=gha,mode=max,scope=${{ github.ref_name }}-${{ matrix.gpu-driver }}
diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml
new file mode 100644
index 00000000000..546d1b07088
--- /dev/null
+++ b/.github/workflows/build-wheel.yml
@@ -0,0 +1,38 @@
+# Builds and uploads python build artifacts.
+
+name: build wheel
+
+on:
+ workflow_dispatch:
+ workflow_call:
+
+jobs:
+ build-installer:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5 # expected run time: <2 min
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: setup python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: pyproject.toml
+
+ - name: install pypa/build
+ run: pip install --upgrade build
+
+ - name: setup frontend
+ uses: ./.github/actions/install-frontend-deps
+
+ - name: build wheel
+ id: build_wheel
+ run: ./scripts/build_wheel.sh
+
+ - name: upload python distribution artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: dist
+ path: ${{ steps.build_wheel.outputs.DIST_PATH }}
diff --git a/.github/workflows/clean-caches.yml b/.github/workflows/clean-caches.yml
new file mode 100644
index 00000000000..73d742f3041
--- /dev/null
+++ b/.github/workflows/clean-caches.yml
@@ -0,0 +1,34 @@
+name: cleanup caches by a branch
+on:
+ pull_request:
+ types:
+ - closed
+ workflow_dispatch:
+
+jobs:
+ cleanup:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v6
+
+ - name: Cleanup
+ run: |
+ gh extension install actions/gh-actions-cache
+
+ REPO=${{ github.repository }}
+ BRANCH=${{ github.ref }}
+
+ echo "Fetching list of cache key"
+ cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
+
+ ## Setting this to not fail the workflow while deleting cache keys.
+ set +e
+ echo "Deleting caches..."
+ for cacheKey in $cacheKeysForPR
+ do
+ gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
+ done
+ echo "Done"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml
new file mode 100644
index 00000000000..40f75cebb88
--- /dev/null
+++ b/.github/workflows/close-inactive-issues.yml
@@ -0,0 +1,29 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: "00 4 * * *"
+
+env:
+ DAYS_BEFORE_ISSUE_STALE: 30
+ DAYS_BEFORE_ISSUE_CLOSE: 14
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v10
+ with:
+ days-before-issue-stale: ${{ env.DAYS_BEFORE_ISSUE_STALE }}
+ days-before-issue-close: ${{ env.DAYS_BEFORE_ISSUE_CLOSE }}
+ stale-issue-label: "Inactive Issue"
+ stale-issue-message: "There has been no activity in this issue for ${{ env.DAYS_BEFORE_ISSUE_STALE }} days. If this issue is still being experienced, please reply with an updated confirmation that the issue is still being experienced with the latest release."
+ close-issue-message: "Due to inactivity, this issue was automatically closed. If you are still experiencing the issue, please recreate the issue."
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+ only-labels: "bug"
+ exempt-issue-labels: "Active Issue"
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ operations-per-run: 500
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
new file mode 100644
index 00000000000..0e9c5774ba3
--- /dev/null
+++ b/.github/workflows/deploy-docs.yml
@@ -0,0 +1,149 @@
+name: 'docs'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ workflow_dispatch:
+ inputs:
+ deploy_target:
+ description: 'Deploy target (custom = invoke.ai, ghpages = invoke-ai.github.io/InvokeAI)'
+ type: choice
+ options:
+ - custom
+ - ghpages
+ default: custom
+
+permissions:
+ contents: read
+ pull-requests: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ changes:
+ runs-on: ubuntu-latest
+ outputs:
+ docs: ${{ steps.manual.outputs.docs || steps.filter.outputs.docs }}
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: mark manual run
+ if: github.event_name == 'workflow_dispatch'
+ id: manual
+ run: echo "docs=true" >> "$GITHUB_OUTPUT"
+
+ - name: detect docs-related changes
+ if: github.event_name != 'workflow_dispatch'
+ id: filter
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ docs:
+ - '.github/workflows/deploy-docs.yml'
+ - 'docs/**'
+ - 'scripts/generate_docs_json.py'
+ - 'invokeai/app/**'
+ - 'invokeai/backend/**'
+ - 'pyproject.toml'
+ - 'uv.lock'
+
+ check-and-build:
+ needs: changes
+ if: |
+ github.event_name == 'workflow_dispatch' ||
+ (github.event_name == 'pull_request' &&
+ github.event.pull_request.draft == false &&
+ needs.changes.outputs.docs == 'true') ||
+ (github.event_name == 'push' && needs.changes.outputs.docs == 'true')
+ runs-on: ubuntu-22.04
+ timeout-minutes: 20
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ # Python (needed for generate-docs-data)
+ - name: setup uv
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ version: '0.11.12'
+ enable-cache: true
+ python-version: '3.11'
+
+ - name: setup python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ # generate_docs_json.py only needs the invokeai package importable
+ # (pydantic + invokeai.app/backend). Skip the [test] extra to keep CI fast.
+ - name: install python dependencies
+ run: uv sync --frozen
+
+ # Node (needed for docs build)
+ - name: setup node
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+
+ - name: setup pnpm
+ uses: pnpm/action-setup@v6
+ with:
+ version: 10
+ run_install: false
+
+ - name: install docs dependencies
+ run: pnpm install --prefer-frozen-lockfile
+ working-directory: docs
+
+ # Checks
+ - name: verify generated docs data
+ run: pnpm run check-docs-data
+ working-directory: docs
+
+ - name: build docs
+ run: pnpm build
+ working-directory: docs
+ env:
+ DEPLOY_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.deploy_target || 'custom' }}
+ ENABLE_ANALYTICS: ${{ github.ref == 'refs/heads/main' && (github.event_name != 'workflow_dispatch' || inputs.deploy_target == 'custom') }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: verify deploy output
+ run: pnpm run check-deploy-output
+ working-directory: docs
+ env:
+ DEPLOY_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.deploy_target || 'custom' }}
+
+ # Upload artifact for deploy (main branch only)
+ - name: upload pages artifact
+ if: github.ref == 'refs/heads/main'
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: docs/dist
+
+ deploy:
+ if: github.ref == 'refs/heads/main'
+ needs: check-and-build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.github/workflows/frontend-checks.yml b/.github/workflows/frontend-checks.yml
new file mode 100644
index 00000000000..b36fbeb650b
--- /dev/null
+++ b/.github/workflows/frontend-checks.yml
@@ -0,0 +1,96 @@
+# Runs frontend code quality checks.
+#
+# Checks for changes to frontend files before running the checks.
+# If always_run is true, always runs the checks.
+
+name: 'frontend checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+
+defaults:
+ run:
+ working-directory: invokeai/frontend/web
+
+jobs:
+ frontend-checks:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10 # expected run time: <2 min
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Fail if package-lock.json is added/modified (pnpm only)
+ shell: bash
+ working-directory: .
+ run: |
+ set -euo pipefail
+ git fetch --no-tags --prune --depth=1 origin "${{ github.base_ref }}"
+ if git diff --name-only "origin/${{ github.base_ref }}...HEAD" | grep -E '(^|/)package-lock\.json$'; then
+ echo "::error::package-lock.json was added or modified. This repo uses pnpm only."
+ exit 1
+ fi
+
+ - name: check for changed frontend files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ frontend:
+ - 'invokeai/frontend/web/**'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+
+ - name: tsc
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm lint:tsc'
+ shell: bash
+
+ - name: dpdm
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm lint:dpdm'
+ shell: bash
+
+ - name: eslint
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm lint:eslint'
+ shell: bash
+
+ - name: prettier
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm lint:prettier'
+ shell: bash
+
+ - name: knip
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm lint:knip'
+ shell: bash
diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml
new file mode 100644
index 00000000000..abb1fb8419f
--- /dev/null
+++ b/.github/workflows/frontend-tests.yml
@@ -0,0 +1,65 @@
+# Runs frontend tests.
+#
+# Checks for changes to frontend files before running the tests.
+# If always_run is true, always runs the tests.
+
+name: 'frontend tests'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the tests'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the tests'
+ required: true
+ type: boolean
+ default: true
+
+defaults:
+ run:
+ working-directory: invokeai/frontend/web
+
+jobs:
+ frontend-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10 # expected run time: <2 min
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: check for changed frontend files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ frontend:
+ - 'invokeai/frontend/web/**'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+
+ - name: vitest
+ if: ${{ steps.changed-files.outputs.frontend_any_changed == 'true' || inputs.always_run == true }}
+ run: 'pnpm test:no-watch'
+ shell: bash
diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml
new file mode 100644
index 00000000000..b7689b12021
--- /dev/null
+++ b/.github/workflows/label-pr.yml
@@ -0,0 +1,18 @@
+name: 'label PRs'
+on:
+ - pull_request_target
+
+jobs:
+ labeler:
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: label PRs
+ uses: actions/labeler@v6
+ with:
+ configuration-path: .github/pr_labels.yml
diff --git a/.github/workflows/lfs-checks.yml b/.github/workflows/lfs-checks.yml
new file mode 100644
index 00000000000..a3b845025a8
--- /dev/null
+++ b/.github/workflows/lfs-checks.yml
@@ -0,0 +1,30 @@
+# Checks that large files and LFS-tracked files are properly checked in with pointer format.
+# Uses https://github.com/ppremk/lfs-warning to detect LFS issues.
+
+name: 'lfs checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+
+jobs:
+ lfs-check:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ # Required to label and comment on the PRs
+ pull-requests: write
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: check lfs files
+ uses: ppremk/lfs-warning@v3.3
diff --git a/.github/workflows/openapi-checks.yml b/.github/workflows/openapi-checks.yml
new file mode 100644
index 00000000000..a3512c581c9
--- /dev/null
+++ b/.github/workflows/openapi-checks.yml
@@ -0,0 +1,115 @@
+# Runs OpenAPI schema quality checks.
+# Checked-in OpenAPI schema should match the generated server schema.
+#
+# Checks for changes to files before running the checks.
+# If always_run is true, always runs the checks.
+
+name: 'openapi checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+
+jobs:
+ openapi-checks:
+ env:
+ # uv requires a venv by default - but for this, we can simply use the system python
+ UV_SYSTEM_PYTHON: 1
+ runs-on: ubuntu-22.04
+ timeout-minutes: 15 # expected run time: <5 min
+ steps:
+ - name: checkout
+ uses: actions/checkout@v4
+
+ - name: Free up more disk space on the runner
+ # https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
+ run: |
+ echo "----- Free space before cleanup"
+ df -h
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ if [ -f /mnt/swapfile ]; then
+ sudo swapoff /mnt/swapfile
+ sudo rm -rf /mnt/swapfile
+ fi
+ echo "----- Free space after cleanup"
+ df -h
+
+ - name: check for changed files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
+ with:
+ files_yaml: |
+ src:
+ - 'pyproject.toml'
+ - 'invokeai/**'
+
+ - name: setup uv
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: '0.6.10'
+ enable-cache: true
+ python-version: '3.11'
+
+ - name: setup python
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ env:
+ UV_INDEX: ${{ matrix.extra-index-url }}
+ run: uv pip install --editable .
+
+ - name: install frontend dependencies
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+
+ - name: copy schema
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: cp invokeai/frontend/web/openapi.json invokeai/frontend/web/openapi_orig.json
+ shell: bash
+
+ - name: generate schema
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: cd invokeai/frontend/web && uv run ../../../scripts/generate_openapi_schema.py > openapi.json && pnpm prettier --write openapi.json
+ shell: bash
+
+ - name: compare files
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: |
+ if ! diff invokeai/frontend/web/openapi.json invokeai/frontend/web/openapi_orig.json; then
+ echo "Files are different!";
+ exit 1;
+ fi
+ shell: bash
diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml
new file mode 100644
index 00000000000..b08bc611cec
--- /dev/null
+++ b/.github/workflows/python-checks.yml
@@ -0,0 +1,82 @@
+# Runs python code quality checks.
+#
+# Checks for changes to python files before running the checks.
+# If always_run is true, always runs the checks.
+#
+# TODO: Add mypy or pyright to the checks.
+
+name: 'python checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+
+jobs:
+ python-checks:
+ env:
+ # uv requires a venv by default - but for this, we can simply use the system python
+ UV_SYSTEM_PYTHON: 1
+ runs-on: ubuntu-latest
+ timeout-minutes: 5 # expected run time: <1 min
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: check for changed python files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ python:
+ - 'pyproject.toml'
+ - 'invokeai/**'
+ - '!invokeai/frontend/web/**'
+ - 'tests/**'
+
+ - name: setup uv
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ version: '0.6.10'
+ enable-cache: true
+
+ - name: check pypi classifiers
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ run: uv run --no-project scripts/check_classifiers.py ./pyproject.toml
+
+ - name: ruff check
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ run: uv tool run ruff@0.11.2 check --output-format=github .
+ shell: bash
+
+ - name: ruff format
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ run: uv tool run ruff@0.11.2 format --check .
+ shell: bash
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
new file mode 100644
index 00000000000..67351c2b387
--- /dev/null
+++ b/.github/workflows/python-tests.yml
@@ -0,0 +1,103 @@
+# Runs python tests on a matrix of python versions and platforms.
+#
+# Checks for changes to python files before running the tests.
+# If always_run is true, always runs the tests.
+
+name: 'python tests'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the tests'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the tests'
+ required: true
+ type: boolean
+ default: true
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ matrix:
+ strategy:
+ matrix:
+ python-version:
+ - '3.11'
+ - '3.12'
+ platform:
+ - linux-cpu
+ - macos-default
+ - windows-cpu
+ include:
+ - platform: linux-cpu
+ os: ubuntu-24.04
+ extra-index-url: 'https://download.pytorch.org/whl/cpu'
+ github-env: $GITHUB_ENV
+ - platform: macos-default
+ os: macOS-14
+ github-env: $GITHUB_ENV
+ - platform: windows-cpu
+ os: windows-2022
+ github-env: $env:GITHUB_ENV
+ name: 'py${{ matrix.python-version }}: ${{ matrix.platform }}'
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 15 # expected run time: 2-6 min, depending on platform
+ env:
+ PIP_USE_PEP517: '1'
+
+ steps:
+ - name: checkout
+ # https://github.com/nschloe/action-cached-lfs-checkout
+ uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc
+
+ - name: check for changed python files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ python:
+ - 'pyproject.toml'
+ - 'invokeai/**'
+ - '!invokeai/frontend/web/**'
+ - 'tests/**'
+
+ - name: setup uv
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ version: '0.6.10'
+ enable-cache: true
+ python-version: ${{ matrix.python-version }}
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ env:
+ UV_INDEX: ${{ matrix.extra-index-url }}
+ run: uv sync --no-progress --locked --extra test
+
+ - name: run pytest
+ if: ${{ steps.changed-files.outputs.python_any_changed == 'true' || inputs.always_run == true }}
+ run: uv run --no-sync pytest
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000000..30e87b53dcb
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,108 @@
+# Main release workflow. Triggered on tag push or manual trigger.
+#
+# - Runs all code checks and tests
+# - Verifies the app version matches the tag version.
+# - Builds the installer and build, uploading them as artifacts.
+# - Publishes to TestPyPI and PyPI. Both are conditional on the previous steps passing and require a manual approval.
+#
+# See docs/RELEASE.md for more information on the release process.
+
+name: release
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+
+jobs:
+ check-version:
+ runs-on: ubuntu-latest
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: check python version
+ uses: samuelcolvin/check-python-version@v4
+ id: check-python-version
+ with:
+ version_file_path: invokeai/version/invokeai_version.py
+
+ frontend-checks:
+ uses: ./.github/workflows/frontend-checks.yml
+ with:
+ always_run: true
+
+ frontend-tests:
+ uses: ./.github/workflows/frontend-tests.yml
+ with:
+ always_run: true
+
+ python-checks:
+ uses: ./.github/workflows/python-checks.yml
+ with:
+ always_run: true
+
+ python-tests:
+ uses: ./.github/workflows/python-tests.yml
+ with:
+ always_run: true
+
+ build:
+ uses: ./.github/workflows/build-wheel.yml
+
+ publish-testpypi:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5 # expected run time: <1 min
+ needs:
+ [
+ check-version,
+ frontend-checks,
+ frontend-tests,
+ python-checks,
+ python-tests,
+ build,
+ ]
+ environment:
+ name: testpypi
+ url: https://test.pypi.org/p/invokeai
+ permissions:
+ id-token: write
+ steps:
+ - name: download distribution from build job
+ uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+
+ - name: publish distribution to TestPyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ repository-url: https://test.pypi.org/legacy/
+
+ publish-pypi:
+ runs-on: ubuntu-latest
+ timeout-minutes: 5 # expected run time: <1 min
+ needs:
+ [
+ check-version,
+ frontend-checks,
+ frontend-tests,
+ python-checks,
+ python-tests,
+ build,
+ ]
+ environment:
+ name: pypi
+ url: https://pypi.org/p/invokeai
+ permissions:
+ id-token: write
+ steps:
+ - name: download distribution from build job
+ uses: actions/download-artifact@v8
+ with:
+ name: dist
+ path: dist/
+
+ - name: publish distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/typegen-checks.yml b/.github/workflows/typegen-checks.yml
new file mode 100644
index 00000000000..1f1f0b1042e
--- /dev/null
+++ b/.github/workflows/typegen-checks.yml
@@ -0,0 +1,115 @@
+# Runs typegen schema quality checks.
+# Frontend types should match the server.
+#
+# Checks for changes to files before running the checks.
+# If always_run is true, always runs the checks.
+
+name: 'typegen checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+
+jobs:
+ typegen-checks:
+ env:
+ # uv requires a venv by default - but for this, we can simply use the system python
+ UV_SYSTEM_PYTHON: 1
+ runs-on: ubuntu-22.04
+ timeout-minutes: 15 # expected run time: <5 min
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: Free up more disk space on the runner
+ # https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
+ run: |
+ echo "----- Free space before cleanup"
+ df -h
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ if [ -f /mnt/swapfile ]; then
+ sudo swapoff /mnt/swapfile
+ sudo rm -rf /mnt/swapfile
+ fi
+ echo "----- Free space after cleanup"
+ df -h
+
+ - name: check for changed files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ src:
+ - 'pyproject.toml'
+ - 'invokeai/**'
+
+ - name: setup uv
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ version: '0.6.10'
+ enable-cache: true
+ python-version: '3.11'
+
+ - name: setup python
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: install dependencies
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ env:
+ UV_INDEX: ${{ matrix.extra-index-url }}
+ run: uv pip install --editable .
+
+ - name: install frontend dependencies
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ uses: ./.github/actions/install-frontend-deps
+
+ - name: copy schema
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: cp invokeai/frontend/web/src/services/api/schema.ts invokeai/frontend/web/src/services/api/schema_orig.ts
+ shell: bash
+
+ - name: generate schema
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: cd invokeai/frontend/web && uv run ../../../scripts/generate_openapi_schema.py | pnpm typegen
+ shell: bash
+
+ - name: compare files
+ if: ${{ steps.changed-files.outputs.src_any_changed == 'true' || inputs.always_run == true }}
+ run: |
+ if ! diff invokeai/frontend/web/src/services/api/schema.ts invokeai/frontend/web/src/services/api/schema_orig.ts; then
+ echo "Files are different!";
+ exit 1;
+ fi
+ shell: bash
diff --git a/.github/workflows/uv-lock-checks.yml b/.github/workflows/uv-lock-checks.yml
new file mode 100644
index 00000000000..d57163165fb
--- /dev/null
+++ b/.github/workflows/uv-lock-checks.yml
@@ -0,0 +1,68 @@
+# Check the `uv` lockfile for consistency with `pyproject.toml`.
+#
+# If this check fails, you should run `uv lock` to update the lockfile.
+
+name: 'uv lock checks'
+
+on:
+ push:
+ branches:
+ - 'main'
+ pull_request:
+ types:
+ - 'ready_for_review'
+ - 'opened'
+ - 'synchronize'
+ merge_group:
+ workflow_dispatch:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+ workflow_call:
+ inputs:
+ always_run:
+ description: 'Always run the checks'
+ required: true
+ type: boolean
+ default: true
+
+jobs:
+ uv-lock-checks:
+ env:
+ # uv requires a venv by default - but for this, we can simply use the system python
+ UV_SYSTEM_PYTHON: 1
+ runs-on: ubuntu-latest
+ timeout-minutes: 5 # expected run time: <1 min
+ steps:
+ - name: checkout
+ uses: actions/checkout@v6
+
+ - name: check for changed python files
+ if: ${{ inputs.always_run != true }}
+ id: changed-files
+ # Pinned to the _hash_ for v45.0.9 to prevent supply-chain attacks.
+ # See:
+ # - CVE-2025-30066
+ # - https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
+ # - https://github.com/tj-actions/changed-files/issues/2463
+ uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96
+ with:
+ files_yaml: |
+ uvlock-pyprojecttoml:
+ - 'pyproject.toml'
+ - 'uv.lock'
+
+ - name: setup uv
+ if: ${{ steps.changed-files.outputs.uvlock-pyprojecttoml_any_changed == 'true' || inputs.always_run == true }}
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ version: '0.6.10'
+ enable-cache: true
+
+ - name: check lockfile
+ if: ${{ steps.changed-files.outputs.uvlock-pyprojecttoml_any_changed == 'true' || inputs.always_run == true }}
+ run: uv lock --locked # this will exit with 1 if the lockfile is not consistent with pyproject.toml
+ shell: bash
diff --git a/.gitignore b/.gitignore
index fd75e65a48d..cc037f09abd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,4 @@
-# ignore default image save location and model symbolic link
-outputs/
-models/ldm/stable-diffusion-v1/model.ckpt
-
-# ignore a directory which serves as a place for initial images
-inputs/
+.idea/
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -25,7 +20,6 @@ dist/
downloads/
eggs/
.eggs/
-lib/
lib64/
parts/
sdist/
@@ -52,16 +46,21 @@ pip-delete-this-directory.txt
htmlcov/
.tox/
.nox/
+.coveragerc
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
+cov.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
+.pytest.ini
cover/
+junit/
+notes/
# Translations
*.mo
@@ -80,9 +79,6 @@ instance/
# Scrapy stuff:
.scrapy
-# Sphinx documentation
-docs/_build/
-
# PyBuilder
.pybuilder/
target/
@@ -134,12 +130,10 @@ celerybeat.pid
# Environments
.env
-.venv
+.venv*
env/
venv/
ENV/
-env.bak/
-venv.bak/
# Spyder project settings
.spyderproject
@@ -148,9 +142,6 @@ venv.bak/
# Rope project settings
.ropeproject
-# mkdocs documentation
-/site
-
# mypy
.mypy_cache/
.dmypy.json
@@ -172,14 +163,30 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
-src
**/__pycache__/
-outputs
-# Logs and associated folders
-# created from generated embeddings.
-logs
-testtube
-checkpoints
# If it's a Mac
.DS_Store
+
+# Let the frontend manage its own gitignore
+!invokeai/frontend/web/*
+
+# Scratch folder
+.scratch/
+worktrees/
+.vscode/
+.zed/
+
+# source installer files
+installer/*zip
+installer/install.bat
+installer/install.sh
+installer/update.bat
+installer/update.sh
+installer/InvokeAI-Installer/
+.aider*
+
+.claude/
+
+# Weblate configuration file
+weblate.ini
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000000..517f38666b4
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v22.14.0
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000000..f128557bc08
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,32 @@
+# See https://pre-commit.com/ for usage and config
+repos:
+- repo: local
+ hooks:
+ - id: black
+ name: black
+ stages: [pre-commit]
+ language: system
+ entry: black
+ types: [python]
+
+ - id: flake8
+ name: flake8
+ stages: [pre-commit]
+ language: system
+ entry: flake8
+ types: [python]
+
+ - id: isort
+ name: isort
+ stages: [pre-commit]
+ language: system
+ entry: isort
+ types: [python]
+
+ - id: uvlock
+ name: uv lock
+ stages: [pre-commit]
+ language: system
+ entry: uv lock
+ files: ^pyproject\.toml$
+ pass_filenames: false
\ No newline at end of file
diff --git a/.prettierrc.yaml b/.prettierrc.yaml
new file mode 100644
index 00000000000..3d2ce3b880d
--- /dev/null
+++ b/.prettierrc.yaml
@@ -0,0 +1,13 @@
+endOfLine: lf
+tabWidth: 2
+useTabs: false
+singleQuote: true
+quoteProps: as-needed
+embeddedLanguageFormatting: auto
+overrides:
+ - files: '*.md'
+ options:
+ proseWrap: preserve
+ printWidth: 80
+ parser: markdown
+ cursorOffset: -1
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index b9cd9d6d0da..00000000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,137 +0,0 @@
-# **Changelog**
-
-## v1.13 (in process)
-
-- Supports a Google Colab notebook for a standalone server running on Google hardware [Arturo Mendivil](https://github.com/artmen1516)
-- WebUI supports GFPGAN/ESRGAN facial reconstruction and upscaling [Kevin Gibbons](https://github.com/bakkot)
-- WebUI supports incremental display of in-progress images during generation [Kevin Gibbons](https://github.com/bakkot)
-- Output directory can be specified on the dream> command line.
-- The grid was displaying duplicated images when not enough images to fill the final row [Muhammad Usama](https://github.com/SMUsamaShah)
-- Can specify --grid on dream.py command line as the default.
-- Miscellaneous internal bug and stability fixes.
-
----
-
-## v1.12 (28 August 2022)
-
-- Improved file handling, including ability to read prompts from standard input.
- (kudos to [Yunsaki](https://github.com/yunsaki)
-- The web server is now integrated with the dream.py script. Invoke by adding --web to
- the dream.py command arguments.
-- Face restoration and upscaling via GFPGAN and Real-ESGAN are now automatically
- enabled if the GFPGAN directory is located as a sibling to Stable Diffusion.
- VRAM requirements are modestly reduced. Thanks to both [Blessedcoolant](https://github.com/blessedcoolant) and
- [Oceanswave](https://github.com/oceanswave) for their work on this.
-- You can now swap samplers on the dream> command line. [Blessedcoolant](https://github.com/blessedcoolant)
-
----
-
-## v1.11 (26 August 2022)
-
-- NEW FEATURE: Support upscaling and face enhancement using the GFPGAN module. (kudos to [Oceanswave](https://github.com/Oceanswave)
-- You now can specify a seed of -1 to use the previous image's seed, -2 to use the seed for the image generated before that, etc.
- Seed memory only extends back to the previous command, but will work on all images generated with the -n# switch.
-- Variant generation support temporarily disabled pending more general solution.
-- Created a feature branch named **yunsaki-morphing-dream** which adds experimental support for
- iteratively modifying the prompt and its parameters. Please see[ Pull Request #86](https://github.com/lstein/stable-diffusion/pull/86)
- for a synopsis of how this works. Note that when this feature is eventually added to the main branch, it will may be modified
- significantly.
-
----
-
-## v1.10 (25 August 2022)
-
-- A barebones but fully functional interactive web server for online generation of txt2img and img2img.
-
----
-
-## v1.09 (24 August 2022)
-
-- A new -v option allows you to generate multiple variants of an initial image
- in img2img mode. (kudos to [Oceanswave](https://github.com/Oceanswave). [
- See this discussion in the PR for examples and details on use](https://github.com/lstein/stable-diffusion/pull/71#issuecomment-1226700810))
-- Added ability to personalize text to image generation (kudos to [Oceanswave](https://github.com/Oceanswave) and [nicolai256](https://github.com/nicolai256))
-- Enabled all of the samplers from k_diffusion
-
----
-
-## v1.08 (24 August 2022)
-
-- Escape single quotes on the dream> command before trying to parse. This avoids
- parse errors.
-- Removed instruction to get Python3.8 as first step in Windows install.
- Anaconda3 does it for you.
-- Added bounds checks for numeric arguments that could cause crashes.
-- Cleaned up the copyright and license agreement files.
-
----
-
-## v1.07 (23 August 2022)
-
-- Image filenames will now never fill gaps in the sequence, but will be assigned the
- next higher name in the chosen directory. This ensures that the alphabetic and chronological
- sort orders are the same.
-
----
-
-## v1.06 (23 August 2022)
-
-- Added weighted prompt support contributed by [xraxra](https://github.com/xraxra)
-- Example of using weighted prompts to tweak a demonic figure contributed by [bmaltais](https://github.com/bmaltais)
-
----
-
-## v1.05 (22 August 2022 - after the drop)
-
-- Filenames now use the following formats:
- 000010.95183149.png -- Two files produced by the same command (e.g. -n2),
- 000010.26742632.png -- distinguished by a different seed.
-
- 000011.455191342.01.png -- Two files produced by the same command using
- 000011.455191342.02.png -- a batch size>1 (e.g. -b2). They have the same seed.
-
- 000011.4160627868.grid#1-4.png -- a grid of four images (-g); the whole grid can
- be regenerated with the indicated key
-
-- It should no longer be possible for one image to overwrite another
-- You can use the "cd" and "pwd" commands at the dream> prompt to set and retrieve
- the path of the output directory.
-
----
-
-## v1.04 (22 August 2022 - after the drop)
-
-- Updated README to reflect installation of the released weights.
-- Suppressed very noisy and inconsequential warning when loading the frozen CLIP
- tokenizer.
-
----
-
-## v1.03 (22 August 2022)
-
-- The original txt2img and img2img scripts from the CompViz repository have been moved into
- a subfolder named "orig_scripts", to reduce confusion.
-
----
-
-## v1.02 (21 August 2022)
-
-- A copy of the prompt and all of its switches and options is now stored in the corresponding
- image in a tEXt metadata field named "Dream". You can read the prompt using scripts/images2prompt.py,
- or an image editor that allows you to explore the full metadata.
- **Please run "conda env update -f environment.yaml" to load the k_lms dependencies!!**
-
----
-
-## v1.01 (21 August 2022)
-
-- added k_lms sampling.
- **Please run "conda env update -f environment.yaml" to load the k_lms dependencies!!**
-- use half precision arithmetic by default, resulting in faster execution and lower memory requirements
- Pass argument --full_precision to dream.py to get slower but more accurate image generation
-
----
-
-## Links
-
-- **[Read Me](readme.md)**
diff --git a/InvokeAI_Statement_of_Values.md b/InvokeAI_Statement_of_Values.md
new file mode 100644
index 00000000000..162220769a2
--- /dev/null
+++ b/InvokeAI_Statement_of_Values.md
@@ -0,0 +1,84 @@
+
+
+Invoke-AI is a community of software developers, researchers, and user
+interface experts who have come together on a voluntary basis to build
+software tools which support cutting edge AI text-to-image
+applications. This community is open to anyone who wishes to
+contribute to the effort and has the skill and time to do so.
+
+# Our Values
+
+The InvokeAI team is a diverse community which includes individuals
+from various parts of the world and many walks of life. Despite our
+differences, we share a number of core values which we ask prospective
+contributors to understand and respect. We believe:
+
+1. That Open Source Software is a positive force in the world. We
+create software that can be used, reused, and redistributed, without
+restrictions, under a straightforward Open Source license (MIT). We
+believe that Open Source benefits society as a whole by increasing the
+availability of high quality software to all.
+
+2. That those who create software should receive proper attribution
+for their creative work. While we support the exchange and reuse of
+Open Source Software, we feel strongly that the original authors of a
+piece of code should receive credit for their contribution, and we
+endeavor to do so whenever possible.
+
+3. That there is moral ambiguity surrounding AI-assisted art. We are
+aware of the moral and ethical issues surrounding the release of the
+Stable Diffusion model and similar products. We are aware that, due to
+the composition of their training sets, current AI-generated image
+models are biased against certain ethnic groups, cultural concepts of
+beauty, ethnic stereotypes, and gender roles.
+
+ 1. We recognize the potential for harm to these groups that these biases
+ represent and trust that future AI models will take steps towards
+ reducing or eliminating the biases noted above, respect and give due
+ credit to the artists whose work is sourced, and call on developers
+ and users to favor these models over the older ones as they become
+ available.
+
+4. We are deeply committed to ensuring that this technology benefits
+everyone, including artists. We see AI art not as a replacement for
+the artist, but rather as a tool to empower them. With that
+in mind, we are constantly debating how to build systems that put
+artists’ needs first: tools which can be readily integrated into an
+artist’s existing workflows and practices, enhancing their work and
+helping them to push it further. Every decision we take as a team,
+which includes several artists, aims to build towards that goal.
+
+5. That artificial intelligence can be a force for good in the world,
+but must be used responsibly. Artificial intelligence technologies
+have the potential to improve society, in everything from cancer care,
+to customer service, to creative writing.
+
+ 1. While we do not believe that software should arbitrarily limit what
+ users can do with it, we recognize that when used irresponsibly, AI
+ has the potential to do much harm. Our Discord server is actively
+ moderated in order to minimize the potential of harm from
+ user-contributed images. In addition, we ask users of our software to
+ refrain from using it in any way that would cause mental, emotional or
+ physical harm to individuals and vulnerable populations including (but
+ not limited to) women; minors; ethnic minorities; religious groups;
+ members of LGBTQIA communities; and people with disabilities or
+ impairments.
+
+ 2. Note that some of the image generation AI models which the Invoke-AI
+ toolkit supports carry licensing agreements which impose restrictions
+ on how the model is used. We ask that our users read and agree to
+ these terms if they wish to make use of these models. These agreements
+ are distinct from the MIT license which applies to the InvokeAI
+ software and source code.
+
+6. That mutual respect is key to a healthy software development
+community. Members of the InvokeAI community are expected to treat
+each other with respect, beneficence, and empathy. Each of us has a
+different background and a unique set of skills. We strive to help
+each other grow and gain new skills, and we apportion expectations in
+a way that balances the members' time, skillset, and interest
+area. Disputes are resolved by open and honest communication.
+
+## Signature
+
+This document has been collectively crafted and approved by the current InvokeAI team members, as of 28 Nov 2022: **lstein** (Lincoln Stein), **blessedcoolant**, **hipsterusername** (Kent Keirsey), **Kyle0654** (Kyle Schouviller), **damian0815**, **mauwii** (Matthias Wild), **Netsvetaev** (Artur Netsvetaev), **psychedelicious**, **tildebyte**, **keturn**, and **ebr** (Eugene Brodsky). Although individuals within the group may hold differing views on particular details and/or their implications, we are all in agreement about its fundamental statements, as well as their significance and importance to this project moving forward.
diff --git a/LICENSE b/LICENSE
index b01fca9fad1..fac28ea6b9e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,32 +1,176 @@
-MIT License
-
-Copyright (c) 2022 Lincoln D. Stein (https://github.com/lstein)
-
-This software is derived from a fork of the source code available from
-https://github.com/pesser/stable-diffusion and
-https://github.com/CompViz/stable-diffusion. They carry the following
-copyrights:
-
-Copyright (c) 2022 Machine Vision and Learning Group, LMU Munich
-Copyright (c) 2022 Robin Rombach and Patrick Esser and contributors
-
-Please see individual source code files for copyright and authorship
-attributions.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+
diff --git a/LICENSE-ModelWeights.txt b/LICENSE-SD1+SD2.txt
similarity index 100%
rename from LICENSE-ModelWeights.txt
rename to LICENSE-SD1+SD2.txt
diff --git a/LICENSE-SDXL.txt b/LICENSE-SDXL.txt
new file mode 100644
index 00000000000..05fbe3abb65
--- /dev/null
+++ b/LICENSE-SDXL.txt
@@ -0,0 +1,290 @@
+Copyright (c) 2023 Stability AI
+CreativeML Open RAIL++-M License dated July 26, 2023
+
+Section I: PREAMBLE
+
+Multimodal generative models are being widely adopted and used, and
+have the potential to transform the way artists, among other
+individuals, conceive and benefit from AI or ML technologies as a tool
+for content creation.
+
+Notwithstanding the current and potential benefits that these
+artifacts can bring to society at large, there are also concerns about
+potential misuses of them, either due to their technical limitations
+or ethical considerations.
+
+In short, this license strives for both the open and responsible
+downstream use of the accompanying model. When it comes to the open
+character, we took inspiration from open source permissive licenses
+regarding the grant of IP rights. Referring to the downstream
+responsible use, we added use-based restrictions not permitting the
+use of the model in very specific scenarios, in order for the licensor
+to be able to enforce the license in case potential misuses of the
+Model may occur. At the same time, we strive to promote open and
+responsible research on generative models for art and content
+generation.
+
+Even though downstream derivative versions of the model could be
+released under different licensing terms, the latter will always have
+to include - at minimum - the same use-based restrictions as the ones
+in the original license (this license). We believe in the intersection
+between open and responsible AI development; thus, this agreement aims
+to strike a balance between both in order to enable responsible
+open-science in the field of AI.
+
+This CreativeML Open RAIL++-M License governs the use of the model
+(and its derivatives) and is informed by the model card associated
+with the model.
+
+NOW THEREFORE, You and Licensor agree as follows:
+
+Definitions
+
+"License" means the terms and conditions for use, reproduction, and
+Distribution as defined in this document.
+
+"Data" means a collection of information and/or content extracted from
+the dataset used with the Model, including to train, pretrain, or
+otherwise evaluate the Model. The Data is not licensed under this
+License.
+
+"Output" means the results of operating a Model as embodied in
+informational content resulting therefrom.
+
+"Model" means any accompanying machine-learning based assemblies
+(including checkpoints), consisting of learnt weights, parameters
+(including optimizer states), corresponding to the model architecture
+as embodied in the Complementary Material, that have been trained or
+tuned, in whole or in part on the Data, using the Complementary
+Material.
+
+"Derivatives of the Model" means all modifications to the Model, works
+based on the Model, or any other model which is created or initialized
+by transfer of patterns of the weights, parameters, activations or
+output of the Model, to the other model, in order to cause the other
+model to perform similarly to the Model, including - but not limited
+to - distillation methods entailing the use of intermediate data
+representations or methods based on the generation of synthetic data
+by the Model for training the other model.
+
+"Complementary Material" means the accompanying source code and
+scripts used to define, run, load, benchmark or evaluate the Model,
+and used to prepare data for training or evaluation, if any. This
+includes any accompanying documentation, tutorials, examples, etc, if
+any.
+
+"Distribution" means any transmission, reproduction, publication or
+other sharing of the Model or Derivatives of the Model to a third
+party, including providing the Model as a hosted service made
+available by electronic or other remote means - e.g. API-based or web
+access.
+
+"Licensor" means the copyright owner or entity authorized by the
+copyright owner that is granting the License, including the persons or
+entities that may have rights in the Model and/or distributing the
+Model.
+
+"You" (or "Your") means an individual or Legal Entity exercising
+permissions granted by this License and/or making use of the Model for
+whichever purpose and in any field of use, including usage of the
+Model in an end-use application - e.g. chatbot, translator, image
+generator.
+
+"Third Parties" means individuals or legal entities that are not under
+common control with Licensor or You.
+
+"Contribution" means any work of authorship, including the original
+version of the Model and any modifications or additions to that Model
+or Derivatives of the Model thereof, that is intentionally submitted
+to Licensor for inclusion in the Model by the copyright owner or by an
+individual or Legal Entity authorized to submit on behalf of the
+copyright owner. For the purposes of this definition, "submitted"
+means any form of electronic, verbal, or written communication sent to
+the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control
+systems, and issue tracking systems that are managed by, or on behalf
+of, the Licensor for the purpose of discussing and improving the
+Model, but excluding communication that is conspicuously marked or
+otherwise designated in writing by the copyright owner as "Not a
+Contribution."
+
+"Contributor" means Licensor and any individual or Legal Entity on
+behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Model.
+
+Section II: INTELLECTUAL PROPERTY RIGHTS
+
+Both copyright and patent grants apply to the Model, Derivatives of
+the Model and Complementary Material. The Model and Derivatives of the
+Model are subject to additional terms as described in
+
+Section III.
+
+Grant of Copyright License. Subject to the terms and conditions of
+this License, each Contributor hereby grants to You a perpetual,
+worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+copyright license to reproduce, prepare, publicly display, publicly
+perform, sublicense, and distribute the Complementary Material, the
+Model, and Derivatives of the Model.
+
+Grant of Patent License. Subject to the terms and conditions of this
+License and where and as applicable, each Contributor hereby grants to
+You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this paragraph) patent license to
+make, have made, use, offer to sell, sell, import, and otherwise
+transfer the Model and the Complementary Material, where such license
+applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by
+combination of their Contribution(s) with the Model to which such
+Contribution(s) was submitted. If You institute patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that the Model and/or Complementary Material or a
+Contribution incorporated within the Model and/or Complementary
+Material constitutes direct or contributory patent infringement, then
+any patent licenses granted to You under this License for the Model
+and/or Work shall terminate as of the date such litigation is asserted
+or filed.
+
+Section III: CONDITIONS OF USAGE, DISTRIBUTION AND REDISTRIBUTION
+
+Distribution and Redistribution. You may host for Third Party remote
+access purposes (e.g. software-as-a-service), reproduce and distribute
+copies of the Model or Derivatives of the Model thereof in any medium,
+with or without modifications, provided that You meet the following
+conditions: Use-based restrictions as referenced in paragraph 5 MUST
+be included as an enforceable provision by You in any type of legal
+agreement (e.g. a license) governing the use and/or distribution of
+the Model or Derivatives of the Model, and You shall give notice to
+subsequent users You Distribute to, that the Model or Derivatives of
+the Model are subject to paragraph 5. This provision does not apply to
+the use of Complementary Material. You must give any Third Party
+recipients of the Model or Derivatives of the Model a copy of this
+License; You must cause any modified files to carry prominent notices
+stating that You changed the files; You must retain all copyright,
+patent, trademark, and attribution notices excluding those notices
+that do not pertain to any part of the Model, Derivatives of the
+Model. You may add Your own copyright statement to Your modifications
+and may provide additional or different license terms and conditions -
+respecting paragraph 4.a. - for use, reproduction, or Distribution of
+Your modifications, or for any such Derivatives of the Model as a
+whole, provided Your use, reproduction, and Distribution of the Model
+otherwise complies with the conditions stated in this License.
+
+Use-based restrictions. The restrictions set forth in Attachment A are
+considered Use-based restrictions. Therefore You cannot use the Model
+and the Derivatives of the Model for the specified restricted
+uses. You may use the Model subject to this License, including only
+for lawful purposes and in accordance with the License. Use may
+include creating any content with, finetuning, updating, running,
+training, evaluating and/or reparametrizing the Model. You shall
+require all of Your users who use the Model or a Derivative of the
+Model to comply with the terms of this paragraph (paragraph 5).
+
+The Output You Generate. Except as set forth herein, Licensor claims
+no rights in the Output You generate using the Model. You are
+accountable for the Output you generate and its subsequent uses. No
+use of the output can contravene any provision as stated in the
+License.
+
+Section IV: OTHER PROVISIONS
+
+Updates and Runtime Restrictions. To the maximum extent permitted by
+law, Licensor reserves the right to restrict (remotely or otherwise)
+usage of the Model in violation of this License.
+
+Trademarks and related. Nothing in this License permits You to make
+use of Licensors’ trademarks, trade names, logos or to otherwise
+suggest endorsement or misrepresent the relationship between the
+parties; and any rights not expressly granted herein are reserved by
+the Licensors.
+
+Disclaimer of Warranty. Unless required by applicable law or agreed to
+in writing, Licensor provides the Model and the Complementary Material
+(and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied, including, without limitation, any warranties or conditions
+of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+PARTICULAR PURPOSE. You are solely responsible for determining the
+appropriateness of using or redistributing the Model, Derivatives of
+the Model, and the Complementary Material and assume any risks
+associated with Your exercise of permissions under this License.
+
+Limitation of Liability. In no event and under no legal theory,
+whether in tort (including negligence), contract, or otherwise, unless
+required by applicable law (such as deliberate and grossly negligent
+acts) or agreed to in writing, shall any Contributor be liable to You
+for damages, including any direct, indirect, special, incidental, or
+consequential damages of any character arising as a result of this
+License or out of the use or inability to use the Model and the
+Complementary Material (including but not limited to damages for loss
+of goodwill, work stoppage, computer failure or malfunction, or any
+and all other commercial damages or losses), even if such Contributor
+has been advised of the possibility of such damages.
+
+Accepting Warranty or Additional Liability. While redistributing the
+Model, Derivatives of the Model and the Complementary Material
+thereof, You may choose to offer, and charge a fee for, acceptance of
+support, warranty, indemnity, or other liability obligations and/or
+rights consistent with this License. However, in accepting such
+obligations, You may act only on Your own behalf and on Your sole
+responsibility, not on behalf of any other Contributor, and only if
+You agree to indemnify, defend, and hold each Contributor harmless for
+any liability incurred by, or claims asserted against, such
+Contributor by reason of your accepting any such warranty or
+additional liability.
+
+If any provision of this License is held to be invalid, illegal or
+unenforceable, the remaining provisions shall be unaffected thereby
+and remain valid as if such provision had not been set forth herein.
+
+
+END OF TERMS AND CONDITIONS
+
+Attachment A
+
+Use Restrictions
+
+You agree not to use the Model or Derivatives of the Model:
+
+* In any way that violates any applicable national, federal, state,
+local or international law or regulation;
+
+* For the purpose of exploiting, harming or attempting to exploit or
+harm minors in any way;
+
+* To generate or disseminate verifiably false information and/or
+ content with the purpose of harming others;
+
+* To generate or disseminate personal identifiable information that
+ can be used to harm an individual;
+
+* To defame, disparage or otherwise harass others;
+
+* For fully automated decision making that adversely impacts an
+ individual’s legal rights or otherwise creates or modifies a
+ binding, enforceable obligation;
+
+* For any use intended to or which has the effect of discriminating
+ against or harming individuals or groups based on online or offline
+ social behavior or known or predicted personal or personality
+ characteristics;
+
+* To exploit any of the vulnerabilities of a specific group of persons
+ based on their age, social, physical or mental characteristics, in
+ order to materially distort the behavior of a person pertaining to
+ that group in a manner that causes or is likely to cause that person
+ or another person physical or psychological harm;
+
+* For any use intended to or which has the effect of discriminating
+ against individuals or groups based on legally protected
+ characteristics or categories;
+
+* To provide medical advice and medical results interpretation;
+
+* To generate or disseminate information for the purpose to be used
+ for administration of justice, law enforcement, immigration or
+ asylum processes, such as predicting an individual will commit
+ fraud/crime commitment (e.g. by text profiling, drawing causal
+ relationships between assertions made in documents, indiscriminate
+ and arbitrarily-targeted use).
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000000..9830f7167fc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,108 @@
+# simple Makefile with scripts that are otherwise hard to remember
+# to use, run from the repo root `make `
+
+default: help
+
+help:
+ @echo Developer commands:
+ @echo
+ @echo "ruff Run ruff, fixing any safely-fixable errors and formatting"
+ @echo "ruff-unsafe Run ruff, fixing all fixable errors and formatting"
+ @echo "mypy Run mypy using the config in pyproject.toml to identify type mismatches and other coding errors"
+ @echo "mypy-all Run mypy ignoring the config in pyproject.tom but still ignoring missing imports"
+ @echo "test Run the unit tests."
+ @echo "frontend-install Install the pnpm modules needed for the frontend"
+ @echo "frontend-build Build the frontend for localhost:9090"
+ @echo "frontend-test Run the frontend test suite once"
+ @echo "frontend-dev Run the frontend in developer mode on localhost:5173"
+ @echo "frontend-openapi Generate the OpenAPI schema"
+ @echo "frontend-typegen Generate types for the frontend from the OpenAPI schema"
+ @echo "frontend-lint Run frontend checks and fixable lint/format steps"
+ @echo "wheel Build the wheel for the current version"
+ @echo "tag-release Tag the GitHub repository with the current version (use at release time only!)"
+ @echo "openapi Generate the OpenAPI schema for the app, outputting to stdout"
+ @echo "docs-install Install the pnpm modules needed for the docs site"
+ @echo "docs-dev Serve the astro starlight docs site with live reload"
+ @echo "docs-build Build the docs site for production"
+ @echo "docs-preview Preview the docs site locally"
+
+# Runs ruff, fixing any safely-fixable errors and formatting
+ruff:
+ cd invokeai && uv tool run ruff@0.11.2 format
+
+# Runs ruff, fixing all errors it can fix and formatting
+ruff-unsafe:
+ ruff check . --fix --unsafe-fixes
+ ruff format
+
+# Runs mypy, using the config in pyproject.toml
+mypy:
+ mypy scripts/invokeai-web.py
+
+# Runs mypy, ignoring the config in pyproject.toml but still ignoring missing (untyped) imports
+# (many files are ignored by the config, so this is useful for checking all files)
+mypy-all:
+ mypy scripts/invokeai-web.py --config-file= --ignore-missing-imports
+
+# Run the unit tests
+test:
+ pytest ./tests
+
+# Install the pnpm modules needed for the front end
+frontend-install:
+ rm -rf invokeai/frontend/web/node_modules
+ cd invokeai/frontend/web && pnpm install
+
+# Build the frontend
+frontend-build:
+ cd invokeai/frontend/web && pnpm build
+
+# Run the frontend test suite once
+frontend-test:
+ cd invokeai/frontend/web && pnpm run test:run
+
+# Run the frontend in dev mode
+frontend-dev:
+ cd invokeai/frontend/web && pnpm dev
+
+# Generate the OpenAPI Schema for the app
+frontend-openapi:
+ cd invokeai/frontend/web && \
+ python ../../../scripts/generate_openapi_schema.py > openapi.json && \
+ pnpm prettier --write openapi.json
+
+frontend-typegen:
+ cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
+
+frontend-lint:
+ cd invokeai/frontend/web/src && \
+ pnpm lint:tsc && \
+ pnpm lint:dpdm && \
+ pnpm lint:eslint --fix && \
+ pnpm lint:prettier --write
+
+# Tag the release
+wheel:
+ cd scripts && ./build_wheel.sh
+
+# Tag the release
+tag-release:
+ cd scripts && ./tag_release.sh
+
+# Generate the OpenAPI Schema for the app
+openapi:
+ python scripts/generate_openapi_schema.py
+
+# Install the pnpm modules needed for the docs site
+docs-install:
+ cd docs && pnpm install
+
+# Serve the astro starlight docs site w/ live reload
+docs-dev:
+ cd docs && pnpm run dev
+
+docs-build:
+ cd docs && DEPLOY_TARGET='custom' pnpm run build
+
+docs-preview:
+ cd docs && pnpm run preview
diff --git a/README-CompViz.md b/README-CompViz.md
deleted file mode 100644
index b2f75bbaf1b..00000000000
--- a/README-CompViz.md
+++ /dev/null
@@ -1,210 +0,0 @@
-# Original README from CompViz/stable-diffusion
-*Stable Diffusion was made possible thanks to a collaboration with [Stability AI](https://stability.ai/) and [Runway](https://runwayml.com/) and builds upon our previous work:*
-
-[**High-Resolution Image Synthesis with Latent Diffusion Models**](https://ommer-lab.com/research/latent-diffusion-models/)
-[Robin Rombach](https://github.com/rromb)\*,
-[Andreas Blattmann](https://github.com/ablattmann)\*,
-[Dominik Lorenz](https://github.com/qp-qp)\,
-[Patrick Esser](https://github.com/pesser),
-[Björn Ommer](https://hci.iwr.uni-heidelberg.de/Staff/bommer)
-
-**CVPR '22 Oral**
-
-which is available on [GitHub](https://github.com/CompVis/latent-diffusion). PDF at [arXiv](https://arxiv.org/abs/2112.10752). Please also visit our [Project page](https://ommer-lab.com/research/latent-diffusion-models/).
-
-
-[Stable Diffusion](#stable-diffusion-v1) is a latent text-to-image diffusion
-model.
-Thanks to a generous compute donation from [Stability AI](https://stability.ai/) and support from [LAION](https://laion.ai/), we were able to train a Latent Diffusion Model on 512x512 images from a subset of the [LAION-5B](https://laion.ai/blog/laion-5b/) database.
-Similar to Google's [Imagen](https://arxiv.org/abs/2205.11487),
-this model uses a frozen CLIP ViT-L/14 text encoder to condition the model on text prompts.
-With its 860M UNet and 123M text encoder, the model is relatively lightweight and runs on a GPU with at least 10GB VRAM.
-See [this section](#stable-diffusion-v1) below and the [model card](https://huggingface.co/CompVis/stable-diffusion).
-
-
-## Requirements
-
-A suitable [conda](https://conda.io/) environment named `ldm` can be created
-and activated with:
-
-```
-conda env create -f environment.yaml
-conda activate ldm
-```
-
-You can also update an existing [latent diffusion](https://github.com/CompVis/latent-diffusion) environment by running
-
-```
-conda install pytorch torchvision -c pytorch
-pip install transformers==4.19.2
-pip install -e .
-```
-
-## Stable Diffusion v1
-
-Stable Diffusion v1 refers to a specific configuration of the model
-architecture that uses a downsampling-factor 8 autoencoder with an 860M UNet
-and CLIP ViT-L/14 text encoder for the diffusion model. The model was pretrained on 256x256 images and
-then finetuned on 512x512 images.
-
-*Note: Stable Diffusion v1 is a general text-to-image diffusion model and therefore mirrors biases and (mis-)conceptions that are present
-in its training data.
-Details on the training procedure and data, as well as the intended use of the model can be found in the corresponding [model card](https://huggingface.co/CompVis/stable-diffusion).
-Research into the safe deployment of general text-to-image models is an ongoing effort. To prevent misuse and harm, we currently provide access to the checkpoints only for [academic research purposes upon request](https://stability.ai/academia-access-form).
-**This is an experiment in safe and community-driven publication of a capable and general text-to-image model. We are working on a public release with a more permissive license that also incorporates ethical considerations.***
-
-[Request access to Stable Diffusion v1 checkpoints for academic research](https://stability.ai/academia-access-form)
-
-### Weights
-
-We currently provide three checkpoints, `sd-v1-1.ckpt`, `sd-v1-2.ckpt` and `sd-v1-3.ckpt`,
-which were trained as follows,
-
-- `sd-v1-1.ckpt`: 237k steps at resolution `256x256` on [laion2B-en](https://huggingface.co/datasets/laion/laion2B-en).
- 194k steps at resolution `512x512` on [laion-high-resolution](https://huggingface.co/datasets/laion/laion-high-resolution) (170M examples from LAION-5B with resolution `>= 1024x1024`).
-- `sd-v1-2.ckpt`: Resumed from `sd-v1-1.ckpt`.
- 515k steps at resolution `512x512` on "laion-improved-aesthetics" (a subset of laion2B-en,
-filtered to images with an original size `>= 512x512`, estimated aesthetics score `> 5.0`, and an estimated watermark probability `< 0.5`. The watermark estimate is from the LAION-5B metadata, the aesthetics score is estimated using an [improved aesthetics estimator](https://github.com/christophschuhmann/improved-aesthetic-predictor)).
-- `sd-v1-3.ckpt`: Resumed from `sd-v1-2.ckpt`. 195k steps at resolution `512x512` on "laion-improved-aesthetics" and 10\% dropping of the text-conditioning to improve [classifier-free guidance sampling](https://arxiv.org/abs/2207.12598).
-
-Evaluations with different classifier-free guidance scales (1.5, 2.0, 3.0, 4.0,
-5.0, 6.0, 7.0, 8.0) and 50 PLMS sampling
-steps show the relative improvements of the checkpoints:
-
-
-
-
-### Text-to-Image with Stable Diffusion
-
-
-
-Stable Diffusion is a latent diffusion model conditioned on the (non-pooled) text embeddings of a CLIP ViT-L/14 text encoder.
-
-
-#### Sampling Script
-
-After [obtaining the weights](#weights), link them
-```
-mkdir -p models/ldm/stable-diffusion-v1/
-ln -s models/ldm/stable-diffusion-v1/model.ckpt
-```
-and sample with
-```
-python scripts/txt2img.py --prompt "a photograph of an astronaut riding a horse" --plms
-```
-
-By default, this uses a guidance scale of `--scale 7.5`, [Katherine Crowson's implementation](https://github.com/CompVis/latent-diffusion/pull/51) of the [PLMS](https://arxiv.org/abs/2202.09778) sampler,
-and renders images of size 512x512 (which it was trained on) in 50 steps. All supported arguments are listed below (type `python scripts/txt2img.py --help`).
-
-```commandline
-usage: txt2img.py [-h] [--prompt [PROMPT]] [--outdir [OUTDIR]] [--skip_grid] [--skip_save] [--ddim_steps DDIM_STEPS] [--plms] [--laion400m] [--fixed_code] [--ddim_eta DDIM_ETA] [--n_iter N_ITER] [--H H] [--W W] [--C C] [--f F] [--n_samples N_SAMPLES] [--n_rows N_ROWS]
- [--scale SCALE] [--from-file FROM_FILE] [--config CONFIG] [--ckpt CKPT] [--seed SEED] [--precision {full,autocast}]
-
-optional arguments:
- -h, --help show this help message and exit
- --prompt [PROMPT] the prompt to render
- --outdir [OUTDIR] dir to write results to
- --skip_grid do not save a grid, only individual samples. Helpful when evaluating lots of samples
- --skip_save do not save individual samples. For speed measurements.
- --ddim_steps DDIM_STEPS
- number of ddim sampling steps
- --plms use plms sampling
- --laion400m uses the LAION400M model
- --fixed_code if enabled, uses the same starting code across samples
- --ddim_eta DDIM_ETA ddim eta (eta=0.0 corresponds to deterministic sampling
- --n_iter N_ITER sample this often
- --H H image height, in pixel space
- --W W image width, in pixel space
- --C C latent channels
- --f F downsampling factor
- --n_samples N_SAMPLES
- how many samples to produce for each given prompt. A.k.a. batch size
- (note that the seeds for each image in the batch will be unavailable)
- --n_rows N_ROWS rows in the grid (default: n_samples)
- --scale SCALE unconditional guidance scale: eps = eps(x, empty) + scale * (eps(x, cond) - eps(x, empty))
- --from-file FROM_FILE
- if specified, load prompts from this file
- --config CONFIG path to config which constructs model
- --ckpt CKPT path to checkpoint of model
- --seed SEED the seed (for reproducible sampling)
- --precision {full,autocast}
- evaluate at this precision
-
-```
-Note: The inference config for all v1 versions is designed to be used with EMA-only checkpoints.
-For this reason `use_ema=False` is set in the configuration, otherwise the code will try to switch from
-non-EMA to EMA weights. If you want to examine the effect of EMA vs no EMA, we provide "full" checkpoints
-which contain both types of weights. For these, `use_ema=False` will load and use the non-EMA weights.
-
-
-#### Diffusers Integration
-
-Another way to download and sample Stable Diffusion is by using the [diffusers library](https://github.com/huggingface/diffusers/tree/main#new--stable-diffusion-is-now-fully-compatible-with-diffusers)
-```py
-# make sure you're logged in with `huggingface-cli login`
-from torch import autocast
-from diffusers import StableDiffusionPipeline, LMSDiscreteScheduler
-
-pipe = StableDiffusionPipeline.from_pretrained(
- "CompVis/stable-diffusion-v1-3-diffusers",
- use_auth_token=True
-)
-
-prompt = "a photo of an astronaut riding a horse on mars"
-with autocast("cuda"):
- image = pipe(prompt)["sample"][0]
-
-image.save("astronaut_rides_horse.png")
-```
-
-
-
-### Image Modification with Stable Diffusion
-
-By using a diffusion-denoising mechanism as first proposed by [SDEdit](https://arxiv.org/abs/2108.01073), the model can be used for different
-tasks such as text-guided image-to-image translation and upscaling. Similar to the txt2img sampling script,
-we provide a script to perform image modification with Stable Diffusion.
-
-The following describes an example where a rough sketch made in [Pinta](https://www.pinta-project.com/) is converted into a detailed artwork.
-```
-python scripts/img2img.py --prompt "A fantasy landscape, trending on artstation" --init-img --strength 0.8
-```
-Here, 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. See the following example.
-
-**Input**
-
-
-
-**Outputs**
-
-
-
-
-This procedure can, for example, also be used to upscale samples from the base model.
-
-
-## Comments
-
-- Our codebase for the diffusion models builds heavily on [OpenAI's ADM codebase](https://github.com/openai/guided-diffusion)
-and [https://github.com/lucidrains/denoising-diffusion-pytorch](https://github.com/lucidrains/denoising-diffusion-pytorch).
-Thanks for open-sourcing!
-
-- The implementation of the transformer encoder is from [x-transformers](https://github.com/lucidrains/x-transformers) by [lucidrains](https://github.com/lucidrains?tab=repositories).
-
-
-## BibTeX
-
-```
-@misc{rombach2021highresolution,
- title={High-Resolution Image Synthesis with Latent Diffusion Models},
- author={Robin Rombach and Andreas Blattmann and Dominik Lorenz and Patrick Esser and Björn Ommer},
- year={2021},
- eprint={2112.10752},
- archivePrefix={arXiv},
- primaryClass={cs.CV}
-}
-
-```
-
-
diff --git a/README-Mac-MPS.md b/README-Mac-MPS.md
deleted file mode 100644
index ef103d6b45a..00000000000
--- a/README-Mac-MPS.md
+++ /dev/null
@@ -1,343 +0,0 @@
-# macOS Instructions
-
-Requirements
-
-- macOS 12.3 Monterey or later
-- Python
-- Patience
-- Apple Silicon*
-
-*I haven't tested any of this on Intel Macs but I have read that one person got
-it to work, so Apple Silicon might not be requried.
-
-Things have moved really fast and so these instructions change often and are
-often out-of-date. One of the problems is that there are so many different ways to
-run this.
-
-We are trying to build a testing setup so that when we make changes it doesn't
-always break.
-
-How to (this hasn't been 100% tested yet):
-
-First get the weights checkpoint download started - it's big:
-
-1. Sign up at https://huggingface.co
-2. Go to the [Stable diffusion diffusion model page](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original)
-3. Accept the terms and click Access Repository:
-4. Download [sd-v1-4.ckpt (4.27 GB)](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/blob/main/sd-v1-4.ckpt) and note where you have saved it (probably the Downloads folder)
-
-While that is downloading, open Terminal and run the following commands one at a time.
-
-```
-# install brew (and Xcode command line tools):
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
-#
-# Now there are two different routes to get the Python (miniconda) environment up and running:
-# 1. Alongside pyenv
-# 2. No pyenv
-#
-# If you don't know what we are talking about, choose 2.
-#
-# NOW EITHER DO
-# 1. Installing alongside pyenv
-
-brew install pyenv-virtualenv # you might have this from before, no problem
-pyenv install anaconda3-latest
-pyenv virtualenv anaconda3-latest lstein-stable-diffusion
-pyenv activate lstein-stable-diffusion
-
-# OR,
-# 2. Installing standalone
-# install python 3, git, cmake, protobuf:
-brew install cmake protobuf rust
-
-# install miniconda (M1 arm64 version):
-curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o Miniconda3-latest-MacOSX-arm64.sh
-/bin/bash Miniconda3-latest-MacOSX-arm64.sh
-
-
-# EITHER WAY,
-# continue from here
-
-# clone the repo
-git clone https://github.com/lstein/stable-diffusion.git
-cd stable-diffusion
-
-#
-# wait until the checkpoint file has downloaded, then proceed
-#
-
-# create symlink to checkpoint
-mkdir -p models/ldm/stable-diffusion-v1/
-
-PATH_TO_CKPT="$HOME/Downloads" # or wherever you saved sd-v1-4.ckpt
-
-ln -s "$PATH_TO_CKPT/sd-v1-4.ckpt" models/ldm/stable-diffusion-v1/model.ckpt
-
-# install packages
-PIP_EXISTS_ACTION=w CONDA_SUBDIR=osx-arm64 conda env create -f environment-mac.yaml
-conda activate ldm
-
-# only need to do this once
-python scripts/preload_models.py
-
-# run SD!
-python scripts/dream.py --full_precision # half-precision requires autocast and won't work
-```
-
-The original scripts should work as well.
-
-```
-python scripts/orig_scripts/txt2img.py --prompt "a photograph of an astronaut riding a horse" --plms
-```
-
-Note, `export PIP_EXISTS_ACTION=w` is a precaution to fix `conda env create -f environment-mac.yaml`
-never finishing in some situations. So it isn't required but wont hurt.
-
-After you follow all the instructions and run dream.py you might get several
-errors. Here's the errors I've seen and found solutions for.
-
-### Is it slow?
-
-Be sure to specify 1 sample and 1 iteration.
-
- python ./scripts/orig_scripts/txt2img.py --prompt "ocean" --ddim_steps 5 --n_samples 1 --n_iter 1
-
-### Doesn't work anymore?
-
-PyTorch nightly includes support for MPS. Because of this, this setup is
-inherently unstable. One morning I woke up and it no longer worked no matter
-what I did until I switched to miniforge. However, I have another Mac that works
-just fine with Anaconda. If you can't get it to work, please search a little
-first because many of the errors will get posted and solved. If you can't find
-a solution please [create an issue](https://github.com/lstein/stable-diffusion/issues).
-
-One debugging step is to update to the latest version of PyTorch nightly.
-
- conda install pytorch torchvision torchaudio -c pytorch-nightly
-
-If `conda env create -f environment-mac.yaml` takes forever run this.
-
- git clean -f
-
-And run this.
-
- conda clean --yes --all
-
-Or you could reset Anaconda.
-
- conda update --force-reinstall -y -n base -c defaults conda
-
-### "No module named cv2", torch, 'ldm', 'transformers', 'taming', etc.
-
-There are several causes of these errors.
-
-First, did you remember to `conda activate ldm`? If your terminal prompt
-begins with "(ldm)" then you activated it. If it begins with "(base)"
-or something else you haven't.
-
-Second, you might've run `./scripts/preload_models.py` or `./scripts/dream.py`
-instead of `python ./scripts/preload_models.py` or `python ./scripts/dream.py`.
-The cause of this error is long so it's below.
-
-Third, if it says you're missing taming you need to rebuild your virtual
-environment.
-
- conda env remove -n ldm
- conda env create -f environment-mac.yaml
-
-Fourth, If you have activated the ldm virtual environment and tried rebuilding
-it, maybe the problem could be that I have something installed that
-you don't and you'll just need to manually install it. Make sure you
-activate the virtual environment so it installs there instead of
-globally.
-
- conda activate ldm
- pip install *name*
-
-You might also need to install Rust (I mention this again below).
-
-### How many snakes are living in your computer?
-
-Here's the reason why you have to specify which python to use.
-There are several versions of python on macOS and the computer is
-picking the wrong one. More specifically, preload_models.py and dream.py says to
-find the first `python3` in the path environment variable. You can see which one
-it is picking with `which python3`. These are the mostly likely paths you'll see.
-
- % which python3
- /usr/bin/python3
-
-The above path is part of the OS. However, that path is a stub that asks you if
-you want to install Xcode. If you have Xcode installed already,
-/usr/bin/python3 will execute /Library/Developer/CommandLineTools/usr/bin/python3 or
-/Applications/Xcode.app/Contents/Developer/usr/bin/python3 (depending on which
-Xcode you've selected with `xcode-select`).
-
- % which python3
- /opt/homebrew/bin/python3
-
-If you installed python3 with Homebrew and you've modified your path to search
-for Homebrew binaries before system ones, you'll see the above path.
-
- % which python
- /opt/anaconda3/bin/python
-
-If you drop the "3" you get an entirely different python. Note: starting in
-macOS 12.3, /usr/bin/python no longer exists (it was python 2 anyway).
-
-If you have Anaconda installed, this is what you'll see. There is a
-/opt/anaconda3/bin/python3 also.
-
- (ldm) % which python
- /Users/name/miniforge3/envs/ldm/bin/python
-
-This is what you'll see if you have miniforge and you've correctly activated
-the ldm environment. This is the goal.
-
-It's all a mess and you should know [how to modify the path environment variable](https://support.apple.com/guide/terminal/use-environment-variables-apd382cc5fa-4f58-4449-b20a-41c53c006f8f/mac)
-if you want to fix it. Here's a brief hint of all the ways you can modify it
-(don't really have the time to explain it all here).
-
-- ~/.zshrc
-- ~/.bash_profile
-- ~/.bashrc
-- /etc/paths.d
-- /etc/path
-
-Which one you use will depend on what you have installed except putting a file
-in /etc/paths.d is what I prefer to do.
-
-### Debugging?
-
-Tired of waiting for your renders to finish before you can see if it
-works? Reduce the steps! The image quality will be horrible but at least you'll
-get quick feedback.
-
- python ./scripts/txt2img.py --prompt "ocean" --ddim_steps 5 --n_samples 1 --n_iter 1
-
-### OSError: Can't load tokenizer for 'openai/clip-vit-large-patch14'...
-
- python scripts/preload_models.py
-
-### "The operator [name] is not current implemented for the MPS device." (sic)
-
-Example error.
-
-```
-...
-NotImplementedError: The operator 'aten::_index_put_impl_' is not current implemented for the MPS device. If you want this op to be added in priority during the prototype phase of this feature, please comment on [https://github.com/pytorch/pytorch/issues/77764](https://github.com/pytorch/pytorch/issues/77764). As a temporary fix, you can set the environment variable `PYTORCH_ENABLE_MPS_FALLBACK=1` to use the CPU as a fallback for this op. WARNING: this will be slower than running natively on MPS.
-```
-
-The lstein branch includes this fix in [environment-mac.yaml](https://github.com/lstein/stable-diffusion/blob/main/environment-mac.yaml).
-
-### "Could not build wheels for tokenizers"
-
-I have not seen this error because I had Rust installed on my computer before I started playing with Stable Diffusion. The fix is to install Rust.
-
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
-### How come `--seed` doesn't work?
-
-First this:
-
-> Completely reproducible results are not guaranteed across PyTorch
-releases, individual commits, or different platforms. Furthermore,
-results may not be reproducible between CPU and GPU executions, even
-when using identical seeds.
-
-[PyTorch docs](https://pytorch.org/docs/stable/notes/randomness.html)
-
-Second, we might have a fix that at least gets a consistent seed sort of. We're
-still working on it.
-
-### libiomp5.dylib error?
-
- OMP: Error #15: Initializing libiomp5.dylib, but found libomp.dylib already initialized.
-
-You are likely using an Intel package by mistake. Be sure to run conda with
-the environment variable `CONDA_SUBDIR=osx-arm64`, like so:
-
-`CONDA_SUBDIR=osx-arm64 conda install ...`
-
-This error happens with Anaconda on Macs when the Intel-only `mkl` is pulled in by
-a dependency. [nomkl](https://stackoverflow.com/questions/66224879/what-is-the-nomkl-python-package-used-for)
-is a metapackage designed to prevent this, by making it impossible to install
-`mkl`, but if your environment is already broken it may not work.
-
-Do *not* use `os.environ['KMP_DUPLICATE_LIB_OK']='True'` or equivalents as this
-masks the underlying issue of using Intel packages.
-
-### Not enough memory.
-
-This seems to be a common problem and is probably the underlying
-problem for a lot of symptoms (listed below). The fix is to lower your
-image size or to add `model.half()` right after the model is loaded. I
-should probably test it out. I've read that the reason this fixes
-problems is because it converts the model from 32-bit to 16-bit and
-that leaves more RAM for other things. I have no idea how that would
-affect the quality of the images though.
-
-See [this issue](https://github.com/CompVis/stable-diffusion/issues/71).
-
-### "Error: product of dimension sizes > 2**31'"
-
-This error happens with img2img, which I haven't played with too much
-yet. But I know it's because your image is too big or the resolution
-isn't a multiple of 32x32. Because the stable-diffusion model was
-trained on images that were 512 x 512, it's always best to use that
-output size (which is the default). However, if you're using that size
-and you get the above error, try 256 x 256 or 512 x 256 or something
-as the source image.
-
-BTW, 2**31-1 = [2,147,483,647](https://en.wikipedia.org/wiki/2,147,483,647#In_computing), which is also 32-bit signed [LONG_MAX](https://en.wikipedia.org/wiki/C_data_types) in C.
-
-### I just got Rickrolled! Do I have a virus?
-
-You don't have a virus. It's part of the project. Here's
-[Rick](https://github.com/lstein/stable-diffusion/blob/main/assets/rick.jpeg)
-and here's [the
-code](https://github.com/lstein/stable-diffusion/blob/69ae4b35e0a0f6ee1af8bb9a5d0016ccb27e36dc/scripts/txt2img.py#L79)
-that swaps him in. It's a NSFW filter, which IMO, doesn't work very
-good (and we call this "computer vision", sheesh).
-
-Actually, this could be happening because there's not enough RAM. You could try the `model.half()` suggestion or specify smaller output images.
-
-### My images come out black
-
-We might have this fixed, we are still testing.
-
-There's a [similar issue](https://github.com/CompVis/stable-diffusion/issues/69)
-on CUDA GPU's where the images come out green. Maybe it's the same issue?
-Someone in that issue says to use "--precision full", but this fork
-actually disables that flag. I don't know why, someone else provided
-that code and I don't know what it does. Maybe the `model.half()`
-suggestion above would fix this issue too. I should probably test it.
-
-### "view size is not compatible with input tensor's size and stride"
-
-```
- File "/opt/anaconda3/envs/ldm/lib/python3.10/site-packages/torch/nn/functional.py", line 2511, in layer_norm
- return torch.layer_norm(input, normalized_shape, weight, bias, eps, torch.backends.cudnn.enabled)
-RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead.
-```
-
-Update to the latest version of lstein/stable-diffusion. We were
-patching pytorch but we found a file in stable-diffusion that we could
-change instead. This is a 32-bit vs 16-bit problem.
-
-### The processor must support the Intel bla bla bla
-
-What? Intel? On an Apple Silicon?
-
- Intel MKL FATAL ERROR: This system does not meet the minimum requirements for use of the Intel(R) Math Kernel Library.
- The processor must support the Intel(R) Supplemental Streaming SIMD Extensions 3 (Intel(R) SSSE3) instructions.
- The processor must support the Intel(R) Streaming SIMD Extensions 4.2 (Intel(R) SSE4.2) instructions.
- The processor must support the Intel(R) Advanced Vector Extensions (Intel(R) AVX) instructions.
-
-This is due to the Intel `mkl` package getting picked up when you try to install
-something that depends on it-- Rosetta can translate some Intel instructions but
-not the specialized ones here. To avoid this, make sure to use the environment
-variable `CONDA_SUBDIR=osx-arm64`, which restricts the Conda environment to only
-use ARM packages, and use `nomkl` as described above.
diff --git a/README.md b/README.md
index a171a2bea7f..afc0211bd91 100644
--- a/README.md
+++ b/README.md
@@ -1,804 +1,136 @@
-Stable Diffusion Dream Script
-
-
-
-
-
-
-
-
-
-
-
-
-
-This is a fork of CompVis/stable-diffusion, the wonderful open source
-text-to-image generator. This fork supports:
-
-1. An interactive command-line interface that accepts the same prompt
- and switches as the Discord bot.
-
-2. A basic Web interface that allows you to run a local web server for
- generating images in your browser.
-
-3. Support for img2img in which you provide a seed image to guide the
- image creation. (inpainting & masking coming soon)
-
-4. A notebook for running the code on Google Colab.
-
-5. Upscaling and face fixing using the optional ESRGAN and GFPGAN
- packages.
-
-6. Weighted subprompts for prompt tuning.
-
-7. [Image variations](VARIATIONS.md) which allow you to systematically
-generate variations of an image you like and combine two or more
-images together to combine the best features of both.
-
-8. Textual inversion for customization of the prompt language and images.
-
-8. ...and more!
-
-This fork is rapidly evolving, so use the Issues panel to report bugs
-and make feature requests, and check back periodically for
-improvements and bug fixes.
-
-# Table of Contents
-
-1. [Major Features](#features)
-2. [Changelog](#latest-changes)
-3. [Installation](#installation)
- 1. [Linux](#linux)
- 1. [Windows](#windows)
- 1. [MacOS](README-Mac-MPS.md)
-4. [Troubleshooting](#troubleshooting)
-5. [Contributing](#contributing)
-6. [Support](#support)
-
-# Features
-
-## Interactive command-line interface similar to the Discord bot
-
-The _dream.py_ script, located in scripts/dream.py,
-provides an interactive interface to image generation similar to
-the "dream mothership" bot that Stable AI provided on its Discord
-server. Unlike the txt2img.py and img2img.py scripts provided in the
-original CompViz/stable-diffusion source code repository, the
-time-consuming initialization of the AI model
-initialization only happens once. After that image generation
-from the command-line interface is very fast.
-
-The script uses the readline library to allow for in-line editing,
-command history (up and down arrows), autocompletion, and more. To help
-keep track of which prompts generated which images, the script writes a
-log file of image names and prompts to the selected output directory.
-In addition, as of version 1.02, it also writes the prompt into the PNG
-file's metadata where it can be retrieved using scripts/images2prompt.py
+
-The script is confirmed to work on Linux and Windows systems. It should
-work on MacOSX as well, but this is not confirmed. Note that this script
-runs from the command-line (CMD or Terminal window), and does not have a GUI.
+
-```
-(ldm) ~/stable-diffusion$ python3 ./scripts/dream.py
-* Initializing, be patient...
-Loading model from models/ldm/text2img-large/model.ckpt
-(...more initialization messages...)
+# Invoke - Professional Creative AI Tools for Visual Media
-* Initialization done! Awaiting your command...
-dream> ashley judd riding a camel -n2 -s150
-Outputs:
- outputs/img-samples/00009.png: "ashley judd riding a camel" -n2 -s150 -S 416354203
- outputs/img-samples/00010.png: "ashley judd riding a camel" -n2 -s150 -S 1362479620
+[![discord badge]][discord link] [![latest release badge]][latest release link] [![github stars badge]][github stars link] [![github forks badge]][github forks link] [![CI checks on main badge]][CI checks on main link] [![latest commit to main badge]][latest commit to main link] [![github open issues badge]][github open issues link] [![github open prs badge]][github open prs link] [![translation status badge]][translation status link]
-dream> "there's a fly in my soup" -n6 -g
- outputs/img-samples/00011.png: "there's a fly in my soup" -n6 -g -S 2685670268
- seeds for individual rows: [2685670268, 1216708065, 2335773498, 822223658, 714542046, 3395302430]
-dream> q
+
-# this shows how to retrieve the prompt stored in the saved image's metadata
-(ldm) ~/stable-diffusion$ python3 ./scripts/images2prompt.py outputs/img_samples/*.png
-00009.png: "ashley judd riding a camel" -s150 -S 416354203
-00010.png: "ashley judd riding a camel" -s150 -S 1362479620
-00011.png: "there's a fly in my soup" -n6 -g -S 2685670268
-```
+Invoke is a leading creative engine built to empower professionals and enthusiasts alike. Generate and create stunning visual media using the latest AI-driven technologies. Invoke offers an industry leading web-based UI, and serves as the foundation for multiple commercial products.
-
-
-
+- Free to use under a commercially-friendly license
+- Download and install on compatible hardware
+- Generate, refine, iterate on images, and build workflows
-The dream> prompt's arguments are pretty much identical to those used
-in the Discord bot, except you don't need to type "!dream" (it doesn't
-hurt if you do). A significant change is that creation of individual
-images is now the default unless --grid (-g) is given. For backward
-compatibility, the -i switch is recognized. For command-line help
-type -h (or --help) at the dream> prompt.
-
-The script itself also recognizes a series of command-line switches
-that will change important global defaults, such as the directory for
-image outputs and the location of the model weight files.
-
-## Image-to-Image
-
-This script also provides an img2img feature that lets you seed your
-creations with a drawing or photo. This is a really cool feature that tells
-stable diffusion to build the prompt on top of the image you provide, preserving
-the original's basic shape and layout. To use it, provide the --init_img
-option as shown here:
-
-```
-dream> "waterfall and rainbow" --init_img=./init-images/crude_drawing.png --strength=0.5 -s100 -n4
-```
-
-The --init_img (-I) option gives the path to the seed picture. --strength (-f) controls how much
-the original will be modified, ranging from 0.0 (keep the original intact), to 1.0 (ignore the original
-completely). The default is 0.75, and ranges from 0.25-0.75 give interesting results.
-
-You may also pass a -v option to generate count variants on the original image. This is done by
-passing the first generated image back into img2img the requested number of times. It generates interesting
-variants.
-
-## GFPGAN and Real-ESRGAN Support
-
-The script also provides the ability to do face restoration and
-upscaling with the help of GFPGAN and Real-ESRGAN respectively.
-
-To use the ability, clone the **[GFPGAN
-repository](https://github.com/TencentARC/GFPGAN)** and follow their
-installation instructions. By default, we expect GFPGAN to be
-installed in a 'GFPGAN' sibling directory. Be sure that the `"ldm"`
-conda environment is active as you install GFPGAN.
-
-You can use the `--gfpgan_dir` argument with `dream.py` to set a
-custom path to your GFPGAN directory. _There are other GFPGAN related
-boot arguments if you wish to customize further._
-
-You can install **Real-ESRGAN** by typing the following command.
-
-```
-pip install realesrgan
-```
-
-**Note: Internet connection needed:**
-Users whose GPU machines are isolated from the Internet (e.g. on a
-University cluster) should be aware that the first time you run
-dream.py with GFPGAN and Real-ESRGAN turned on, it will try to
-download model files from the Internet. To rectify this, you may run
-`python3 scripts/preload_models.py` after you have installed GFPGAN
-and all its dependencies.
-
-**Usage**
-
-You will now have access to two new prompt arguments.
-
-**Upscaling**
-
-`-U : `
-
-The upscaling prompt argument takes two values. The first value is a
-scaling factor and should be set to either `2` or `4` only. This will
-either scale the image 2x or 4x respectively using different models.
-
-You can set the scaling stength between `0` and `1.0` to control
-intensity of the of the scaling. This is handy because AI upscalers
-generally tend to smooth out texture details. If you wish to retain
-some of those for natural looking results, we recommend using values
-between `0.5 to 0.8`.
-
-If you do not explicitly specify an upscaling_strength, it will
-default to 0.75.
-
-**Face Restoration**
-
-`-G : `
-
-This prompt argument controls the strength of the face restoration
-that is being applied. Similar to upscaling, values between `0.5 to 0.8` are recommended.
-
-You can use either one or both without any conflicts. In cases where
-you use both, the image will be first upscaled and then the face
-restoration process will be executed to ensure you get the highest
-quality facial features.
-
-`--save_orig`
-
-When you use either `-U` or `-G`, the final result you get is upscaled
-or face modified. If you want to save the original Stable Diffusion
-generation, you can use the `-save_orig` prompt argument to save the
-original unaffected version too.
-
-**Example Usage**
-
-```
-dream > superman dancing with a panda bear -U 2 0.6 -G 0.4
-```
-
-This also works with img2img:
-
-```
-dream> a man wearing a pineapple hat -I path/to/your/file.png -U 2 0.5 -G 0.6
-```
-
-**Note**
-
-GFPGAN and Real-ESRGAN are both memory intensive. In order to avoid
-crashes and memory overloads during the Stable Diffusion process,
-these effects are applied after Stable Diffusion has completed its
-work.
-
-In single image generations, you will see the output right away but
-when you are using multiple iterations, the images will first be
-generated and then upscaled and face restored after that process is
-complete. While the image generation is taking place, you will still
-be able to preview the base images.
-
-If you wish to stop during the image generation but want to upscale or
-face restore a particular generated image, pass it again with the same
-prompt and generated seed along with the `-U` and `-G` prompt
-arguments to perform those actions.
-
-## Google Colab
-
-Stable Diffusion AI Notebook:
-Open and follow instructions to use an isolated environment running Dream.
-
-Output example:
-
-
-## Barebones Web Server
-
-As of version 1.10, this distribution comes with a bare bones web
-server (see screenshot). To use it, run the _dream.py_ script by
-adding the **--web** option.
-
-```
-(ldm) ~/stable-diffusion$ python3 scripts/dream.py --web
-```
-
-You can then connect to the server by pointing your web browser at
-http://localhost:9090, or to the network name or IP address of the server.
-
-Kudos to [Tesseract Cat](https://github.com/TesseractCat) for
-contributing this code, and to [dagf2101](https://github.com/dagf2101)
-for refining it.
-
-
-
-## Reading Prompts from a File
-
-You can automate dream.py by providing a text file with the prompts
-you want to run, one line per prompt. The text file must be composed
-with a text editor (e.g. Notepad) and not a word processor. Each line
-should look like what you would type at the dream> prompt:
-
-```
-a beautiful sunny day in the park, children playing -n4 -C10
-stormy weather on a mountain top, goats grazing -s100
-innovative packaging for a squid's dinner -S137038382
-```
-
-Then pass this file's name to dream.py when you invoke it:
-
-```
-(ldm) ~/stable-diffusion$ python3 scripts/dream.py --from_file "path/to/prompts.txt"
-```
-
-You may read a series of prompts from standard input by providing a filename of "-":
-
-```
-(ldm) ~/stable-diffusion$ echo "a beautiful day" | python3 scripts/dream.py --from_file -
-```
-
-## Shortcut for reusing seeds from the previous command
-
-Since it is so common to reuse seeds while refining a prompt, there is
-now a shortcut as of version 1.11. Provide a **-S** (or **--seed**)
-switch of -1 to use the seed of the most recent image generated. If
-you produced multiple images with the **-n** switch, then you can go
-back further using -2, -3, etc. up to the first image generated by the
-previous command. Sorry, but you can't go back further than one
-command.
-
-Here's an example of using this to do a quick refinement. It also
-illustrates using the new **-G** switch to turn on upscaling and
-face enhancement (see previous section):
-
-```
-dream> a cute child playing hopscotch -G0.5
-[...]
-outputs/img-samples/000039.3498014304.png: "a cute child playing hopscotch" -s50 -W512 -H512 -C7.5 -mk_lms -S3498014304
-
-# I wonder what it will look like if I bump up the steps and set facial enhancement to full strength?
-dream> a cute child playing hopscotch -G1.0 -s100 -S -1
-reusing previous seed 3498014304
-[...]
-outputs/img-samples/000040.3498014304.png: "a cute child playing hopscotch" -G1.0 -s100 -W512 -H512 -C7.5 -mk_lms -S3498014304
-```
-
-## Weighted Prompts
-
-You may weight different sections of the prompt to tell the sampler to attach different levels of
-priority to them, by adding :(number) to the end of the section you wish to up- or downweight.
-For example consider this prompt:
-
-```
- tabby cat:0.25 white duck:0.75 hybrid
-```
-
-This will tell the sampler to invest 25% of its effort on the tabby
-cat aspect of the image and 75% on the white duck aspect
-(surprisingly, this example actually works). The prompt weights can
-use any combination of integers and floating point numbers, and they
-do not need to add up to 1.
-
-## Personalizing Text-to-Image Generation
-
-You may personalize the generated images to provide your own styles or objects by training a new LDM checkpoint
-and introducing a new vocabulary to the fixed model.
-
-To train, prepare a folder that contains images sized at 512x512 and execute the following:
-
-
-WINDOWS: As the default backend is not available on Windows, if you're using that platform, set the environment variable `PL_TORCH_DISTRIBUTED_BACKEND=gloo`
-
-```
-(ldm) ~/stable-diffusion$ python3 ./main.py --base ./configs/stable-diffusion/v1-finetune.yaml \
- -t \
- --actual_resume ./models/ldm/stable-diffusion-v1/model.ckpt \
- -n my_cat \
- --gpus 0, \
- --data_root D:/textual-inversion/my_cat \
- --init_word 'cat'
-```
-
-During the training process, files will be created in /logs/[project][time][project]/
-where you can see the process.
-
-conditioning\* contains the training prompts
-inputs, reconstruction the input images for the training epoch
-samples, samples scaled for a sample of the prompt and one with the init word provided
-
-On a RTX3090, the process for SD will take ~1h @1.6 iterations/sec.
-
-Note: According to the associated paper, the optimal number of images
-is 3-5. Your model may not converge if you use more images than that.
-
-Training will run indefinately, but you may wish to stop it before the
-heat death of the universe, when you find a low loss epoch or around
-~5000 iterations.
-
-Once the model is trained, specify the trained .pt file when starting
-dream using
-
-```
-(ldm) ~/stable-diffusion$ python3 ./scripts/dream.py --embedding_path /path/to/embedding.pt --full_precision
-```
-
-Then, to utilize your subject at the dream prompt
-
-```
-dream> "a photo of *"
-```
-
-this also works with image2image
-
-```
-dream> "waterfall and rainbow in the style of *" --init_img=./init-images/crude_drawing.png --strength=0.5 -s100 -n4
-```
-
-It's also possible to train multiple tokens (modify the placeholder string in configs/stable-diffusion/v1-finetune.yaml) and combine LDM checkpoints using:
-
-```
-(ldm) ~/stable-diffusion$ python3 ./scripts/merge_embeddings.py \
- --manager_ckpts /path/to/first/embedding.pt /path/to/second/embedding.pt [...] \
- --output_path /path/to/output/embedding.pt
-```
-
-Credit goes to @rinongal and the repository located at
-https://github.com/rinongal/textual_inversion Please see the
-repository and associated paper for details and limitations.
-
-# Latest Changes
-
-- v1.13 (3 September 2022)
-
- - Support image variations (see [VARIATIONS](VARIATIONS.md) ([Kevin Gibbons](https://github.com/bakkot) and many contributors and reviewers)
- - Supports a Google Colab notebook for a standalone server running on Google hardware [Arturo Mendivil](https://github.com/artmen1516)
- - WebUI supports GFPGAN/ESRGAN facial reconstruction and upscaling [Kevin Gibbons](https://github.com/bakkot)
- - WebUI supports incremental display of in-progress images during generation [Kevin Gibbons](https://github.com/bakkot)
- - A new configuration file scheme that allows new models (including upcoming stable-diffusion-v1.5)
- to be added without altering the code. ([David Wager](https://github.com/maddavid12))
- - Can specify --grid on dream.py command line as the default.
- - Miscellaneous internal bug and stability fixes.
- - Works on M1 Apple hardware.
- - Multiple bug fixes.
-
-For older changelogs, please visit **[CHANGELOGS](CHANGELOG.md)**.
-
-# Installation
-
-There are separate installation walkthroughs for [Linux](#linux), [Windows](#windows) and [Macintosh](#Macintosh)
-
-## Linux
-
-1. You will need to install the following prerequisites if they are not already available. Use your
- operating system's preferred installer
-
-- Python (version 3.8.5 recommended; higher may work)
-- git
-
-2. Install the Python Anaconda environment manager.
-
-```
-~$ wget https://repo.anaconda.com/archive/Anaconda3-2022.05-Linux-x86_64.sh
-~$ chmod +x Anaconda3-2022.05-Linux-x86_64.sh
-~$ ./Anaconda3-2022.05-Linux-x86_64.sh
-```
-
-After installing anaconda, you should log out of your system and log back in. If the installation
-worked, your command prompt will be prefixed by the name of the current anaconda environment, "(base)".
-
-3. Copy the stable-diffusion source code from GitHub:
-
-```
-(base) ~$ git clone https://github.com/lstein/stable-diffusion.git
-```
-
-This will create stable-diffusion folder where you will follow the rest of the steps.
-
-4. Enter the newly-created stable-diffusion folder. From this step forward make sure that you are working in the stable-diffusion directory!
-
-```
-(base) ~$ cd stable-diffusion
-(base) ~/stable-diffusion$
-```
-
-5. Use anaconda to copy necessary python packages, create a new python environment named "ldm",
- and activate the environment.
-
-```
-(base) ~/stable-diffusion$ conda env create -f environment.yaml
-(base) ~/stable-diffusion$ conda activate ldm
-(ldm) ~/stable-diffusion$
-```
-
-After these steps, your command prompt will be prefixed by "(ldm)" as shown above.
-
-6. Load a couple of small machine-learning models required by stable diffusion:
-
-```
-(ldm) ~/stable-diffusion$ python3 scripts/preload_models.py
-```
-
-Note that this step is necessary because I modified the original
-just-in-time model loading scheme to allow the script to work on GPU
-machines that are not internet connected. See [Workaround for machines with limited internet connectivity](#workaround-for-machines-with-limited-internet-connectivity)
-
-7. Now you need to install the weights for the stable diffusion model.
-
-For running with the released weights, you will first need to set up an acount with Hugging Face (https://huggingface.co).
-Use your credentials to log in, and then point your browser at https://huggingface.co/CompVis/stable-diffusion-v-1-4-original.
-You may be asked to sign a license agreement at this point.
-
-Click on "Files and versions" near the top of the page, and then click on the file named "sd-v1-4.ckpt". You'll be taken
-to a page that prompts you to click the "download" link. Save the file somewhere safe on your local machine.
-
-Now run the following commands from within the stable-diffusion directory. This will create a symbolic
-link from the stable-diffusion model.ckpt file, to the true location of the sd-v1-4.ckpt file.
-
-```
-(ldm) ~/stable-diffusion$ mkdir -p models/ldm/stable-diffusion-v1
-(ldm) ~/stable-diffusion$ ln -sf /path/to/sd-v1-4.ckpt models/ldm/stable-diffusion-v1/model.ckpt
-```
-
-8. Start generating images!
-
-```
-# for the pre-release weights use the -l or --liaon400m switch
-(ldm) ~/stable-diffusion$ python3 scripts/dream.py -l
-
-# for the post-release weights do not use the switch
-(ldm) ~/stable-diffusion$ python3 scripts/dream.py
-
-# for additional configuration switches and arguments, use -h or --help
-(ldm) ~/stable-diffusion$ python3 scripts/dream.py -h
-```
-
-9. Subsequently, to relaunch the script, be sure to run "conda activate ldm" (step 5, second command), enter the "stable-diffusion"
- directory, and then launch the dream script (step 8). If you forget to activate the ldm environment, the script will fail with multiple ModuleNotFound errors.
-
-### Updating to newer versions of the script
-
-This distribution is changing rapidly. If you used the "git clone" method (step 5) to download the stable-diffusion directory, then to update to the latest and greatest version, launch the Anaconda window, enter "stable-diffusion", and type:
-
-```
-(ldm) ~/stable-diffusion$ git pull
-```
-
-This will bring your local copy into sync with the remote one.
-
-## Windows
-
-### Notebook install (semi-automated)
-
-We have a
-[Jupyter notebook](https://github.com/lstein/stable-diffusion/blob/main/Stable-Diffusion-local-Windows.ipynb)
-with cell-by-cell installation steps. It will download the code in this repo as
-one of the steps, so instead of cloning this repo, simply download the notebook
-from the link above and load it up in VSCode (with the
-appropriate extensions installed)/Jupyter/JupyterLab and start running the cells one-by-one.
-
-Note that you will need NVIDIA drivers, Python 3.10, and Git installed
-beforehand - simplified
-[step-by-step instructions](https://github.com/lstein/stable-diffusion/wiki/Easy-peasy-Windows-install)
-are available in the wiki (you'll only need steps 1, 2, & 3 ).
-
-### Manual installs
-
-#### pip
-
-See
-[Easy-peasy Windows install](https://github.com/lstein/stable-diffusion/wiki/Easy-peasy-Windows-install)
-in the wiki
-
-#### Conda
-
-1. Install Anaconda3 (miniconda3 version) from here: https://docs.anaconda.com/anaconda/install/windows/
-
-2. Install Git from here: https://git-scm.com/download/win
-
-3. Launch Anaconda from the Windows Start menu. This will bring up a command window. Type all the remaining commands in this window.
-
-4. Run the command:
-
-```
-git clone https://github.com/lstein/stable-diffusion.git
-```
-
-This will create stable-diffusion folder where you will follow the rest of the steps.
-
-5. Enter the newly-created stable-diffusion folder. From this step forward make sure that you are working in the stable-diffusion directory!
-
-```
-cd stable-diffusion
-```
-
-6. Run the following two commands:
-
-```
-conda env create -f environment.yaml (step 6a)
-conda activate ldm (step 6b)
-```
-
-This will install all python requirements and activate the "ldm" environment which sets PATH and other environment variables properly.
-
-7. Run the command:
-
-```
-python scripts\preload_models.py
-```
-
-This installs several machine learning models that stable diffusion
-requires. (Note that this step is required. I created it because some people
-are using GPU systems that are behind a firewall and the models can't be
-downloaded just-in-time)
-
-8. Now you need to install the weights for the big stable diffusion model.
-
-For running with the released weights, you will first need to set up
-an acount with Hugging Face (https://huggingface.co). Use your
-credentials to log in, and then point your browser at
-https://huggingface.co/CompVis/stable-diffusion-v-1-4-original. You
-may be asked to sign a license agreement at this point.
-
-Click on "Files and versions" near the top of the page, and then click
-on the file named "sd-v1-4.ckpt". You'll be taken to a page that
-prompts you to click the "download" link. Now save the file somewhere
-safe on your local machine. The weight file is >4 GB in size, so
-downloading may take a while.
-
-Now run the following commands from **within the stable-diffusion
-directory** to copy the weights file to the right place:
-
-```
-mkdir -p models\ldm\stable-diffusion-v1
-copy C:\path\to\sd-v1-4.ckpt models\ldm\stable-diffusion-v1\model.ckpt
-```
-
-Please replace "C:\path\to\sd-v1.4.ckpt" with the correct path to wherever
-you stashed this file. If you prefer not to copy or move the .ckpt file,
-you may instead create a shortcut to it from within
-"models\ldm\stable-diffusion-v1\".
-
-9. Start generating images!
-
-```
-# for the pre-release weights
-python scripts\dream.py -l
-
-# for the post-release weights
-python scripts\dream.py
-```
-
-10. Subsequently, to relaunch the script, first activate the Anaconda
-command window (step 3), enter the stable-diffusion directory (step 5,
-"cd \path\to\stable-diffusion"), run "conda activate ldm" (step 6b),
-and then launch the dream script (step 9).
-
-**Note:** Tildebyte has written an alternative ["Easy peasy Windows
-install"](https://github.com/lstein/stable-diffusion/wiki/Easy-peasy-Windows-install)
-which uses the Windows Powershell and pew. If you are having trouble
-with Anaconda on Windows, give this a try (or try it first!)
-
-### Updating to newer versions of the script
-
-This distribution is changing rapidly. If you used the "git clone"
-method (step 5) to download the stable-diffusion directory, then to
-update to the latest and greatest version, launch the Anaconda window,
-enter "stable-diffusion", and type:
-
-```
-git pull
-```
-
-This will bring your local copy into sync with the remote one.
-
-## Macintosh
-
-See [README-Mac-MPS](README-Mac-MPS.md) for instructions.
-
-# Simplified API for text to image generation
-
-For programmers who wish to incorporate stable-diffusion into other
-products, this repository includes a simplified API for text to image
-generation, which lets you create images from a prompt in just three
-lines of code:
-
-```
-from ldm.simplet2i import T2I
-model = T2I()
-outputs = model.txt2img("a unicorn in manhattan")
-```
-
-Outputs is a list of lists in the format [[filename1,seed1],[filename2,seed2]...]
-Please see ldm/simplet2i.py for more information. A set of example scripts is
-coming RSN.
-
-# Workaround for machines with limited internet connectivity
-
-My development machine is a GPU node in a high-performance compute
-cluster which has no connection to the internet. During model
-initialization, stable-diffusion tries to download the Bert tokenizer
-and a file needed by the kornia library. This obviously didn't work
-for me.
-
-To work around this, I have modified ldm/modules/encoders/modules.py
-to look for locally cached Bert files rather than attempting to
-download them. For this to work, you must run
-"scripts/preload_models.py" once from an internet-connected machine
-prior to running the code on an isolated one. This assumes that both
-machines share a common network-mounted filesystem with a common
-.cache directory.
-
-```
-(ldm) ~/stable-diffusion$ python3 ./scripts/preload_models.py
-preloading bert tokenizer...
-Downloading: 100%|██████████████████████████████████| 28.0/28.0 [00:00<00:00, 49.3kB/s]
-Downloading: 100%|██████████████████████████████████| 226k/226k [00:00<00:00, 2.79MB/s]
-Downloading: 100%|██████████████████████████████████| 455k/455k [00:00<00:00, 4.36MB/s]
-Downloading: 100%|██████████████████████████████████| 570/570 [00:00<00:00, 477kB/s]
-...success
-preloading kornia requirements...
-Downloading: "https://github.com/DagnyT/hardnet/raw/master/pretrained/train_liberty_with_aug/checkpoint_liberty_with_aug.pth" to /u/lstein/.cache/torch/hub/checkpoints/checkpoint_liberty_with_aug.pth
-100%|███████████████████████████████████████████████| 5.10M/5.10M [00:00<00:00, 101MB/s]
-...success
-```
-
-# Troubleshooting
-
-Here are a few common installation problems and their solutions. Often
-these are caused by incomplete installations or crashes during the
-install process.
-
-- PROBLEM: During "conda env create -f environment.yaml", conda
- hangs indefinitely.
-
-- SOLUTION: Enter the stable-diffusion directory and completely
- remove the "src" directory and all its contents. The safest way
- to do this is to enter the stable-diffusion directory and
- give the command "git clean -f". If this still doesn't fix
- the problem, try "conda clean -all" and then restart at the
- "conda env create" step.
+
---
-
-- PROBLEM: dream.py crashes with the complaint that it can't find
- ldm.simplet2i.py. Or it complains that function is being passed
- incorrect parameters.
-
-- SOLUTION: Reinstall the stable diffusion modules. Enter the
- stable-diffusion directory and give the command "pip install -e ."
+> ## 📣 Are you a new or returning InvokeAI user?
+> Take our first annual [User's Survey](https://forms.gle/rCE5KuQ7Wfrd1UnS7)
---
-- PROBLEM: dream.py dies, complaining of various missing modules, none
- of which starts with "ldm".
-
-- SOLUTION: From within the stable-diffusion directory, run "conda env
- update -f environment.yaml" This is also frequently the solution to
- complaints about an unknown function in a module.
-
----
-
-- PROBLEM: There's a feature or bugfix in the Stable Diffusion GitHub
- that you want to try out.
-
-- SOLUTION: If the fix/feature is on the "main" branch, enter the stable-diffusion
- directory and do a "git pull". Usually this will be sufficient, but if
- you start to see errors about missing or incorrect modules, use the
- command "pip install -e ." and/or "conda env update -f environment.yaml"
- (These commands won't break anything.)
-
-- If the feature/fix is on a branch (e.g. "foo-bugfix"), the recipe is similar, but
- do a "git pull ".
-
-- If the feature/fix is in a pull request that has not yet been made
- part of the main branch or a feature/bugfix branch, then from the page
- for the desired pull request, look for the line at the top that reads
- "xxxx wants to merge xx commits into lstein:main from YYYYYY". Copy
- the URL in YYYY. It should have the format
- https://github.com//stable-diffusion/tree/
+# Documentation
-- Then **go to the directory above stable-diffusion**, and rename the
- directory to "stable-diffusion.lstein", "stable-diffusion.old", or
- whatever. You can then git clone the branch that contains the
- pull request:
+| **Quick Links** |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] |
-```
-git clone https://github.com//stable-diffusion/tree/
-```
-
-You will need to go through the install procedure again, but it should
-be fast because all the dependencies are already loaded.
-
-# Contributing
-
-Anyone who wishes to contribute to this project, whether
-documentation, features, bug fixes, code cleanup, testing, or code
-reviews, is very much encouraged to do so. If you are unfamiliar with
-how to contribute to GitHub projects, here is a [Getting Started
-Guide](https://opensource.com/article/19/7/create-pull-request-github).
-
-A full set of contribution guidelines, along with templates, are in
-progress, but for now the most important thing is to **make your pull
-request against the "development" branch**, and not against
-"main". This will help keep public breakage to a minimum and will
-allow you to propose more radical changes.
-
-# Support
-
-For support,
-please use this repository's GitHub Issues tracking service. Feel free
-to send me an email if you use and like the script.
-
-_Original Author:_ Lincoln D. Stein
-
-_Contributions by:_
-[Peter Kowalczyk](https://github.com/slix), [Henry Harrison](https://github.com/hwharrison),
-[xraxra](https://github.com/xraxra), [bmaltais](https://github.com/bmaltais), [Sean McLellan](https://github.com/Oceanswave),
-[nicolai256](https://github.com/nicolai256), [Benjamin Warner](https://github.com/warner-benjamin),
-[tildebyte](https://github.com/tildebyte),[yunsaki](https://github.com/yunsaki), [James Reynolds][https://github.com/magnusviri],
-[Tesseract Cat](https://github.com/TesseractCat), and many more!
-
-(If you have contributed and don't see your name on the list of
-contributors, please let lstein know about the omission, or make a
-pull request)
-
-Original portions of the software are Copyright (c) 2020 Lincoln D. Stein (https://github.com/lstein)
+# Installation
-# Further Reading
+To get started with Invoke, [Download the Launcher](https://github.com/invoke-ai/launcher/releases/latest).
-Please see the original README for more information on this software
-and underlying algorithm, located in the file [README-CompViz.md](README-CompViz.md).
+## Troubleshooting, FAQ and Support
+
+Please review our [FAQ][faq] for solutions to common installation problems and other issues.
+
+For more help, please join our [Discord][discord link].
+
+## Features
+
+Full details on features can be found in [our documentation][features docs].
+
+### Web Server & UI
+
+Invoke runs a locally hosted web server & React UI with an industry-leading user experience.
+
+### Unified Canvas
+
+The Unified Canvas is a fully integrated canvas implementation with support for all core generation capabilities, in/out-painting, brush tools, and more. This creative tool unlocks the capability for artists to create with AI as a creative collaborator, and can be used to augment AI-generated imagery, sketches, photography, renders, and more.
+
+### Workflows & Nodes
+
+Invoke offers a fully featured workflow management solution, enabling users to combine the power of node-based workflows with the ease of a UI. This allows for customizable generation pipelines to be developed and shared by users looking to create specific workflows to support their production use-cases.
+
+### Board & Gallery Management
+
+Invoke features an organized gallery system for easily storing, accessing, and remixing your content in the Invoke workspace. Images can be dragged/dropped onto any Image-base UI element in the application, and rich metadata within the Image allows for easy recall of key prompts or settings used in your workflow.
+
+### Model Support
+- SD 1.5
+- SD 2.0
+- SDXL
+- SD 3.5 Medium
+- SD 3.5 Large
+- CogView 4
+- Flux.1 Dev
+- Flux.1 Schnell
+- Flux.1 Kontext
+- Flux.1 Krea
+- Flux Redux
+- Flux Fill
+- Flux.2 Klein 4B
+- Flux.2 Klein 9B
+- Z-Image Turbo
+- Z-Image Base
+- Anima
+- Qwen Image
+- Qwen Image Edit
+- Nano Banana (API Only)
+- GPT Image (API Only)
+- Wan (API Only)
+
+### Other features
+
+- Support for ckpt, diffusers, and some gguf models
+- Upscaling Tools
+- Embedding Manager & Support
+- Model Manager & Support
+- Workflow creation & management
+- Node-Based Architecture
+- Object Segmentation & Selection Models (SAM / SAM2)
+
+## Contributing
+
+Anyone who wishes to contribute to this project - whether documentation, features, bug fixes, code cleanup, testing, or code reviews - is very much encouraged to do so.
+
+Get started with contributing by reading our [contribution documentation][contributing docs], joining the [#dev-chat] or the GitHub discussion board.
+
+We hope you enjoy using Invoke as much as we enjoy creating it, and we hope you will elect to become part of our community.
+
+## Thanks
+
+Invoke is a combined effort of [passionate and talented people from across the world][contributors]. We thank them for their time, hard work and effort.
+
+Original portions of the software are Copyright © 2024 by respective contributors.
+
+[features docs]: https://invoke.ai/
+[faq]: https://invoke.ai/troubleshooting/faq/
+[contributors]: https://invoke.ai/contributing/contributors/
+[github issues]: https://github.com/invoke-ai/InvokeAI/issues
+[docs home]: https://invoke.ai
+[installation docs]: https://invoke.ai/start-here/installation/
+[#dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
+[contributing docs]: https://invoke.ai/contributing/
+[CI checks on main badge]: https://flat.badgen.net/github/checks/invoke-ai/InvokeAI/main?label=CI%20status%20on%20main&cache=900&icon=github
+[CI checks on main link]: https://github.com/invoke-ai/InvokeAI/actions?query=branch%3Amain
+[discord badge]: https://flat.badgen.net/discord/members/ZmtBAhwWhy?icon=discord
+[discord link]: https://discord.gg/ZmtBAhwWhy
+[github forks badge]: https://flat.badgen.net/github/forks/invoke-ai/InvokeAI?icon=github
+[github forks link]: https://useful-forks.github.io/?repo=invoke-ai%2FInvokeAI
+[github open issues badge]: https://flat.badgen.net/github/open-issues/invoke-ai/InvokeAI?icon=github
+[github open issues link]: https://github.com/invoke-ai/InvokeAI/issues?q=is%3Aissue+is%3Aopen
+[github open prs badge]: https://flat.badgen.net/github/open-prs/invoke-ai/InvokeAI?icon=github
+[github open prs link]: https://github.com/invoke-ai/InvokeAI/pulls?q=is%3Apr+is%3Aopen
+[github stars badge]: https://flat.badgen.net/github/stars/invoke-ai/InvokeAI?icon=github
+[github stars link]: https://github.com/invoke-ai/InvokeAI/stargazers
+[latest commit to main badge]: https://flat.badgen.net/github/last-commit/invoke-ai/InvokeAI/main?icon=github&color=yellow&label=last%20dev%20commit&cache=900
+[latest commit to main link]: https://github.com/invoke-ai/InvokeAI/commits/main
+[latest release badge]: https://flat.badgen.net/github/release/invoke-ai/InvokeAI/development?icon=github
+[latest release link]: https://github.com/invoke-ai/InvokeAI/releases/latest
+[translation status badge]: https://hosted.weblate.org/widgets/invokeai/-/svg-badge.svg
+[translation status link]: https://hosted.weblate.org/engage/invokeai/
+[nvidia docker docs]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
+[amd docker docs]: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000..5b3275535a5
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,14 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest version of Invoke will receive security updates.
+We do not currently maintain multiple versions of the application with updates.
+
+## Reporting a Vulnerability
+
+To report a vulnerability, contact the Invoke team directly at security@invoke.ai
+
+At this time, we do not maintain a formal bug bounty program.
+
+You can also share identified security issues with our team on huntr.com
diff --git a/Stable-Diffusion-local-Windows.ipynb b/Stable-Diffusion-local-Windows.ipynb
deleted file mode 100644
index f4cea1503d4..00000000000
--- a/Stable-Diffusion-local-Windows.ipynb
+++ /dev/null
@@ -1,259 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Easy-peasy Windows install"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Note that you will need NVIDIA drivers, Python 3.10, and Git installed\n",
- "beforehand - simplified\n",
- "[step-by-step instructions](https://github.com/lstein/stable-diffusion/wiki/Easy-peasy-Windows-install)\n",
- "are available in the wiki (you'll only need steps 1, 2, & 3 )"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Run each cell in turn. In VSCode, either hit SHIFT-ENTER, or click on the little ▶️ to the left of the cell. In Jupyter/JupyterLab, you **must** hit SHIFT-ENTER"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%pip install pew"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%cmd\n",
- "git clone https://github.com/lstein/stable-diffusion.git"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%cd stable-diffusion"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%writefile requirements.txt\n",
- "albumentations==0.4.3\n",
- "einops==0.3.0\n",
- "huggingface-hub==0.8.1\n",
- "imageio-ffmpeg==0.4.2\n",
- "imageio==2.9.0\n",
- "kornia==0.6.0\n",
- "omegaconf==2.1.1\n",
- "opencv-python==4.6.0.66\n",
- "pillow==9.2.0\n",
- "pudb==2019.2\n",
- "pytorch-lightning==1.4.2\n",
- "streamlit==1.12.0\n",
- "# Regular \"taming-transformers\" doesn't seem to work\n",
- "taming-transformers-rom1504==0.0.6\n",
- "test-tube>=0.7.5\n",
- "torch-fidelity==0.3.0\n",
- "torchmetrics==0.6.0\n",
- "torchvision==0.12.0\n",
- "transformers==4.19.2\n",
- "git+https://github.com/openai/CLIP.git@main#egg=clip\n",
- "git+https://github.com/lstein/k-diffusion.git@master#egg=k-diffusion\n",
- "# No CUDA in PyPi builds\n",
- "torch@https://download.pytorch.org/whl/cu113/torch-1.11.0%2Bcu113-cp310-cp310-win_amd64.whl\n",
- "# No MKL in PyPi builds (faster, more robust than OpenBLAS)\n",
- "numpy@https://download.lfd.uci.edu/pythonlibs/archived/numpy-1.22.4+mkl-cp310-cp310-win_amd64.whl\n",
- "-e .\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%cmd\n",
- "pew new --python 3.10 -r requirements.txt --dont-activate ldm"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Switch the notebook kernel to the new 'ldm' environment!\n",
- "\n",
- "## VSCode: restart VSCode and come back to this cell\n",
- "\n",
- "1. Ctrl+Shift+P\n",
- "1. Type \"Select Interpreter\" and select \"Jupyter: Select Interpreter to Start Jupyter Server\"\n",
- "1. VSCode will say that it needs to install packages. Click the \"Install\" button.\n",
- "1. Once the install is finished, do 1 & 2 again\n",
- "1. Pick 'ldm'\n",
- "1. Run the following cell"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%cd stable-diffusion"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "## Jupyter/JupyterLab\n",
- "\n",
- "1. Run the cell below\n",
- "1. Click on the toolbar where it says \"(ipyknel)\" ↗️. You should get a pop-up asking you to \"Select Kernel\". Pick 'ldm' from the drop-down.\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### DO NOT RUN THE FOLLOWING CELL IF YOU ARE USING VSCODE!!"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# DO NOT RUN THIS CELL IF YOU ARE USING VSCODE!!\n",
- "%%cmd\n",
- "pew workon ldm\n",
- "pip3 install ipykernel\n",
- "python -m ipykernel install --name=ldm"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#### When running the next cell, Jupyter/JupyterLab users might get a warning saying \"IProgress not found\". This can be ignored."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%run \"scripts/preload_models.py\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%%cmd\n",
- "mkdir \"models/ldm/stable-diffusion-v1\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Now copy the SD model you downloaded from Hugging Face into the above new directory, and (if necessary) rename it to 'model.ckpt'"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Now go create some magic!\n",
- "\n",
- "VSCode\n",
- "\n",
- "- The actual input box for the 'dream' prompt will appear at the very top of the VSCode window. Type in your commands and hit 'ENTER'.\n",
- "- To quit, hit the 'Interrupt' button in the toolbar up there ⬆️ a couple of times, then hit ENTER (you'll probably see a terrifying traceback from Python - just ignore it).\n",
- "\n",
- "Jupyter/JupyterLab\n",
- "\n",
- "- The input box for the 'dream' prompt will appear below. Type in your commands and hit 'ENTER'.\n",
- "- To quit, hit the interrupt button (⏹️) in the toolbar up there ⬆️ a couple of times, then hit ENTER (you'll probably see a terrifying traceback from Python - just ignore it)."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "%run \"scripts/dream.py\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Once this seems to be working well, you can try opening a terminal\n",
- "\n",
- "- VSCode: type ('CTRL+`')\n",
- "- Jupyter/JupyterLab: File|New Terminal\n",
- "- Or jump out of the notebook entirely, and open Powershell/Command Prompt\n",
- "\n",
- "Now:\n",
- "\n",
- "1. `cd` to wherever the 'stable-diffusion' directory is\n",
- "1. Run `pew workon ldm`\n",
- "1. Run `winpty python scripts\\dream.py`"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3.10.6 ('ldm')",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.6"
- },
- "vscode": {
- "interpreter": {
- "hash": "a05e4574567b7bc2c98f7f9aa579f9ea5b8739b54844ab610ac85881c4be2659"
- }
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/Stable_Diffusion_AI_Notebook.ipynb b/Stable_Diffusion_AI_Notebook.ipynb
deleted file mode 100644
index defc158346a..00000000000
--- a/Stable_Diffusion_AI_Notebook.ipynb
+++ /dev/null
@@ -1,256 +0,0 @@
-{
- "nbformat": 4,
- "nbformat_minor": 0,
- "metadata": {
- "colab": {
- "name": "Stable_Diffusion_AI_Notebook.ipynb",
- "provenance": [],
- "collapsed_sections": [],
- "private_outputs": true
- },
- "kernelspec": {
- "name": "python3",
- "display_name": "Python 3"
- },
- "language_info": {
- "name": "python"
- },
- "accelerator": "GPU",
- "gpuClass": "standard"
- },
- "cells": [
- {
- "cell_type": "markdown",
- "source": [
- "# Stable Diffusion AI Notebook\n",
- "\n",
- " \n",
- "#### Instructions:\n",
- "1. Execute each cell in order to mount a Dream bot and create images from text. \n",
- "2. Once cells 1-8 were run correctly you'll be executing a terminal in cell #9, you'll to enter `pipenv run scripts/dream.py` command to run Dream bot. \n",
- "3. After launching dream bot, you'll see: `Dream > ` in terminal. Insert a command, eg. `Dream > Astronaut floating in a distant galaxy`, or type `-h` for help.\n",
- "3. After completion you'll see your generated images in path `stable-diffusion/outputs/img-samples/`, you can also display images in cell #10.\n",
- "4. To quit Dream bot use `q` command. \n",
- "---\n",
- "Note: It takes some time to load, but after installing all dependencies you can use the bot all time you want while colab instance is up. \n",
- "Requirements: For this notebook to work you need to have [Stable-Diffusion-v-1-4](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original) stored in your Google Drive, it will be needed in cell #6\n",
- "##### For more details visit Github repository: [lstein/stable-diffusion](https://github.com/lstein/stable-diffusion)\n",
- "---\n"
- ],
- "metadata": {
- "id": "ycYWcsEKc6w7"
- }
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 1. Check current GPU assigned\n",
- "!nvidia-smi -L\n",
- "!nvidia-smi"
- ],
- "metadata": {
- "cellView": "form",
- "id": "a2Z5Qu_o8VtQ"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "cellView": "form",
- "id": "vbI9ZsQHzjqF"
- },
- "outputs": [],
- "source": [
- "#@title 2. Download stable-diffusion Repository\n",
- "from os.path import exists\n",
- "\n",
- "if exists(\"/content/stable-diffusion/\")==True:\n",
- " print(\"Already downloaded repo\")\n",
- "else:\n",
- " !git clone --quiet https://github.com/lstein/stable-diffusion.git # Original repo\n",
- " %cd stable-diffusion/\n",
- " !git checkout --quiet tags/release-1.09\n",
- " "
- ]
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 3. Install Python 3.8 \n",
- "%%capture --no-stderr\n",
- "import gc\n",
- "!apt-get -qq install python3.8\n",
- "gc.collect()"
- ],
- "metadata": {
- "id": "daHlozvwKesj",
- "cellView": "form"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 4. Install dependencies from file in a VirtualEnv\n",
- "#@markdown Be patient, it takes ~ 5 - 7min \n",
- "%%capture --no-stderr\n",
- "#Virtual environment\n",
- "!pip install pipenv -q\n",
- "!pip install colab-xterm\n",
- "%load_ext colabxterm\n",
- "!pipenv --python 3.8\n",
- "!pipenv install -r requirements.txt --skip-lock\n",
- "gc.collect()\n"
- ],
- "metadata": {
- "cellView": "form",
- "id": "QbXcGXYEFSNB"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 5. Mount google Drive\n",
- "from google.colab import drive\n",
- "drive.mount('/content/drive')"
- ],
- "metadata": {
- "cellView": "form",
- "id": "YEWPV-sF1RDM"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 6. Drive Path to model\n",
- "#@markdown Path should start with /content/drive/path-to-your-file \n",
- "#@markdown Note: Model should be downloaded from https://huggingface.co \n",
- "#@markdown Lastest release: [Stable-Diffusion-v-1-4](https://huggingface.co/CompVis/stable-diffusion-v-1-4-original)\n",
- "from os.path import exists\n",
- "\n",
- "model_path = \"\" #@param {type:\"string\"}\n",
- "if exists(model_path)==True:\n",
- " print(\"✅ Valid directory\")\n",
- "else: \n",
- " print(\"❌ File doesn't exist\")"
- ],
- "metadata": {
- "cellView": "form",
- "id": "zRTJeZ461WGu"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 7. Symlink to model\n",
- "\n",
- "from os.path import exists\n",
- "import os \n",
- "\n",
- "# Folder creation if it doesn't exist\n",
- "if exists(\"/content/stable-diffusion/models/ldm/stable-diffusion-v1\")==True:\n",
- " print(\"❗ Dir stable-diffusion-v1 already exists\")\n",
- "else:\n",
- " %mkdir /content/stable-diffusion/models/ldm/stable-diffusion-v1\n",
- " print(\"✅ Dir stable-diffusion-v1 created\")\n",
- "\n",
- "# Symbolic link if it doesn't exist\n",
- "if exists(\"/content/stable-diffusion/models/ldm/stable-diffusion-v1/model.ckpt\")==True:\n",
- " print(\"❗ Symlink already created\")\n",
- "else: \n",
- " src = model_path\n",
- " dst = '/content/stable-diffusion/models/ldm/stable-diffusion-v1/model.ckpt'\n",
- " os.symlink(src, dst) \n",
- " print(\"✅ Symbolic link created successfully\")"
- ],
- "metadata": {
- "id": "UY-NNz4I8_aG",
- "cellView": "form"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 8. Load small ML models required\n",
- "%%capture --no-stderr\n",
- "!pipenv run scripts/preload_models.py\n",
- "gc.collect()"
- ],
- "metadata": {
- "cellView": "form",
- "id": "ChIDWxLVHGGJ"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 9. Run Terminal and Execute Dream bot\n",
- "#@markdown Steps: \n",
- "#@markdown 1. Execute command `pipenv run scripts/dream.py` to run dream bot. \n",
- "#@markdown 2. After initialized you'll see `Dream>` line. \n",
- "#@markdown 3. Example text: `Astronaut floating in a distant galaxy` \n",
- "#@markdown 4. To quit Dream bot use: `q` command. \n",
- "\n",
- "#Run from virtual env\n",
- "\n",
- "%xterm\n",
- "gc.collect()"
- ],
- "metadata": {
- "id": "ir4hCrMIuUpl",
- "cellView": "form"
- },
- "execution_count": null,
- "outputs": []
- },
- {
- "cell_type": "code",
- "source": [
- "#@title 10. Show generated images\n",
- "\n",
- "import glob\n",
- "import matplotlib.pyplot as plt\n",
- "import matplotlib.image as mpimg\n",
- "%matplotlib inline\n",
- "\n",
- "images = []\n",
- "for img_path in glob.glob('/content/stable-diffusion/outputs/img-samples/*.png'):\n",
- " images.append(mpimg.imread(img_path))\n",
- "\n",
- "# Remove ticks and labels on x-axis and y-axis both\n",
- "\n",
- "plt.figure(figsize=(20,10))\n",
- "\n",
- "columns = 5\n",
- "for i, image in enumerate(images):\n",
- " ax = plt.subplot(len(images) / columns + 1, columns, i + 1)\n",
- " ax.axes.xaxis.set_visible(False)\n",
- " ax.axes.yaxis.set_visible(False)\n",
- " ax.axis('off')\n",
- " plt.imshow(image)\n",
- " gc.collect()\n",
- "\n"
- ],
- "metadata": {
- "cellView": "form",
- "id": "qnLohSHmKoGk"
- },
- "execution_count": null,
- "outputs": []
- }
- ]
-}
\ No newline at end of file
diff --git a/Stable_Diffusion_v1_Model_Card.md b/Stable_Diffusion_v1_Model_Card.md
index 2cbf99bd2fa..4ebebc8b83b 100644
--- a/Stable_Diffusion_v1_Model_Card.md
+++ b/Stable_Diffusion_v1_Model_Card.md
@@ -21,7 +21,7 @@ This model card focuses on the model associated with the Stable Diffusion model,
# Uses
-## Direct Use
+## Direct Use
The model is intended for research purposes only. Possible research areas and
tasks include
@@ -68,11 +68,11 @@ Using the model to generate content that is cruel to individuals is a misuse of
considerations.
### Bias
-While the capabilities of image generation models are impressive, they can also reinforce or exacerbate social biases.
-Stable Diffusion v1 was trained on subsets of [LAION-2B(en)](https://laion.ai/blog/laion-5b/),
-which consists of images that are primarily limited to English descriptions.
-Texts and images from communities and cultures that use other languages are likely to be insufficiently accounted for.
-This affects the overall output of the model, as white and western cultures are often set as the default. Further, the
+While the capabilities of image generation models are impressive, they can also reinforce or exacerbate social biases.
+Stable Diffusion v1 was trained on subsets of [LAION-2B(en)](https://laion.ai/blog/laion-5b/),
+which consists of images that are primarily limited to English descriptions.
+Texts and images from communities and cultures that use other languages are likely to be insufficiently accounted for.
+This affects the overall output of the model, as white and western cultures are often set as the default. Further, the
ability of the model to generate content with non-English prompts is significantly worse than with English-language prompts.
@@ -84,7 +84,7 @@ The model developers used the following dataset for training the model:
- LAION-2B (en) and subsets thereof (see next section)
**Training Procedure**
-Stable Diffusion v1 is a latent diffusion model which combines an autoencoder with a diffusion model that is trained in the latent space of the autoencoder. During training,
+Stable Diffusion v1 is a latent diffusion model which combines an autoencoder with a diffusion model that is trained in the latent space of the autoencoder. During training,
- Images are encoded through an encoder, which turns images into latent representations. The autoencoder uses a relative downsampling factor of 8 and maps images of shape H x W x 3 to latents of shape H/f x W/f x 4
- Text prompts are encoded through a ViT-L/14 text-encoder.
@@ -108,12 +108,12 @@ filtered to images with an original size `>= 512x512`, estimated aesthetics scor
- **Batch:** 32 x 8 x 2 x 4 = 2048
- **Learning rate:** warmup to 0.0001 for 10,000 steps and then kept constant
-## Evaluation Results
+## Evaluation Results
Evaluations with different classifier-free guidance scales (1.5, 2.0, 3.0, 4.0,
5.0, 6.0, 7.0, 8.0) and 50 PLMS sampling
steps show the relative improvements of the checkpoints:
-
+
Evaluated using 50 PLMS steps and 10000 random prompts from the COCO2017 validation set, evaluated at 512x512 resolution. Not optimized for FID scores.
## Environmental Impact
diff --git a/USER_ISOLATION_IMPLEMENTATION.md b/USER_ISOLATION_IMPLEMENTATION.md
new file mode 100644
index 00000000000..324c40db562
--- /dev/null
+++ b/USER_ISOLATION_IMPLEMENTATION.md
@@ -0,0 +1,169 @@
+# User Isolation Implementation Summary
+
+This document describes the implementation of user isolation features in the InvokeAI session queue and processing system to address issues identified in the enhancement request.
+
+## Issues Addressed
+
+### 1. Cross-User Image/Preview Visibility
+**Problem:** When two users are logged in simultaneously and one initiates a generation, the generation preview shows up in both users' browsers and the generated image gets saved to both users' image boards.
+
+**Solution:** Implemented socket-level event filtering based on user authentication:
+
+#### Backend Changes (`invokeai/app/api/sockets.py`):
+- Added socket authentication middleware in `_handle_connect()` method
+- Extracts JWT token from socket auth data or HTTP headers
+- Verifies token using existing `verify_token()` function
+- Stores `user_id` and `is_admin` in socket session for later use
+- Modified `_handle_queue_event()` to filter events by user:
+ - For `QueueItemEventBase` events, only emit to:
+ - The user who owns the queue item (`user_id` matches)
+ - Admin users (`is_admin` is True)
+ - For general queue events, emit to all subscribers
+
+#### Event System Changes (`invokeai/app/services/events/events_common.py`):
+- Added `user_id` field to `QueueItemEventBase` class
+- Updated all event builders to include `user_id` from queue items:
+ - `InvocationStartedEvent.build()`
+ - `InvocationProgressEvent.build()`
+ - `InvocationCompleteEvent.build()`
+ - `InvocationErrorEvent.build()`
+ - `QueueItemStatusChangedEvent.build()`
+
+### 2. Batch Field Values Privacy
+**Problem:** Users can see batch field values from generation processes launched by other users.
+
+**Solution:** Implemented field value sanitization at the API level:
+
+#### API Router Changes (`invokeai/app/api/routers/session_queue.py`):
+- Created `sanitize_queue_item_for_user()` helper function
+ - Clears `field_values` for non-admin users viewing other users' items
+ - Admins and item owners can see all field values
+- Updated endpoints to require authentication and sanitize responses:
+ - `list_all_queue_items()` - Added `CurrentUser` dependency
+ - `get_queue_items_by_item_ids()` - Added `CurrentUser` dependency
+ - `get_queue_item()` - Added `CurrentUser` dependency
+
+### 3. Queue Updates Across Browser Windows
+**Problem:** When the job queue tab is open in multiple browsers and a generation is begun in one browser window, the queue does not update in the other window.
+
+**Status:** This issue is likely resolved by the socket authentication and event filtering changes. The existing socket subscription mechanism (`subscribe_queue` event) already supports multiple connections per user. Testing is required to confirm this works correctly with the new authentication flow.
+
+### 4. User Information Display
+**Problem:** Queue table lacks user identification, making it difficult to know who launched which job.
+
+**Solution:** Added user information to queue items and UI:
+
+#### Database Layer (`invokeai/app/services/session_queue/session_queue_sqlite.py`):
+- Updated SQL queries to JOIN with `users` table
+- Modified methods to fetch user information:
+ - `get_queue_item()` - Now selects `display_name` and `email` from users table
+ - `dequeue()` - Includes user info
+ - `get_next()` - Includes user info
+ - `get_current()` - Includes user info
+ - `list_all_queue_items()` - Includes user info
+
+#### Data Model Changes (`invokeai/app/services/session_queue/session_queue_common.py`):
+- Added optional fields to `SessionQueueItem`:
+ - `user_display_name: Optional[str]` - Display name from users table
+ - `user_email: Optional[str]` - Email from users table
+ - Note: `user_id` field already existed from Migration 25
+
+#### Frontend UI Changes:
+- **Constants** (`constants.ts`): Added `user: '8rem'` column width
+- **Header** (`QueueListHeader.tsx`): Added "User" column header
+- **Item Component** (`QueueItemComponent.tsx`):
+ - Added logic to display user information (display_name → email → user_id)
+ - Added user column to queue item row
+ - Added tooltip with full username on hover
+ - Added "Hidden for privacy" message when field_values are null for non-owned items
+- **Localization** (`en.json`): Added translations:
+ - `"user": "User"`
+ - `"fieldValuesHidden": "Hidden for privacy"`
+
+## Security Considerations
+
+### Token Verification
+- Tokens are verified using the existing `verify_token()` function from `invokeai.app.services.auth.token_service`
+- Invalid or missing tokens default to "system" user with non-admin privileges
+- Socket connections without valid tokens are still accepted for backward compatibility but have limited access
+
+### Data Privacy
+- Field values are only visible to:
+ - The user who created the queue item
+ - Admin users
+- Non-admin users viewing other users' queue items see "Hidden for privacy" instead of field values
+
+### Admin Privileges
+- Admin users can see all queue events and field values across all users
+- Admin status is determined from the JWT token's `is_admin` field
+
+## Migration Notes
+
+No database migration is required. The changes leverage:
+- Existing `user_id` column in `session_queue` table (added in Migration 25)
+- Existing `users` table (added in Migration 25)
+- SQL LEFT JOINs to fetch user information (gracefully handles missing user records)
+
+## Testing Requirements
+
+### Backend Testing
+1. **Socket Authentication:**
+ - Verify valid tokens are accepted and user context is stored
+ - Verify invalid tokens default to system user
+ - Verify expired tokens are rejected
+
+2. **Event Filtering:**
+ - User A should only receive events for their own queue items
+ - Admin users should receive all events
+ - Non-admin users should not receive events from other users
+
+3. **Field Value Sanitization:**
+ - Non-admin users should see null field_values for other users' items
+ - Admins should see all field values
+ - Users should see their own field values
+
+### Frontend Testing
+1. **UI Display:**
+ - User column should display in queue list
+ - Display name should be shown when available
+ - Email should be shown as fallback when display name is missing
+ - User ID should be shown when both display name and email are missing
+ - Tooltip should show full username on hover
+
+2. **Field Values Display:**
+ - "Hidden for privacy" message should appear when viewing other users' items
+ - Own items should show field values normally
+
+3. **Multi-Browser Testing:**
+ - Open queue tab in two browsers with different users
+ - Start generation in one browser
+ - Verify other browser doesn't see the preview/progress
+ - Verify admin user can see all generations
+
+### Integration Testing
+1. Multi-user scenarios with simultaneous generations
+2. Queue updates across multiple browser windows
+3. Admin vs. non-admin privilege differentiation
+4. Socket reconnection handling
+
+## Known Limitations
+
+1. **TypeScript Types:**
+ - The OpenAPI schema needs to be regenerated to include new fields
+ - Run: `cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen`
+
+2. **Backward Compatibility:**
+ - System user ("system") entries will not have display name or email
+ - Existing queue items from before Migration 25 will have user_id="system"
+
+3. **Socket.IO Session Storage:**
+ - Socket.IO's in-memory session storage may not persist across server restarts
+ - Consider implementing persistent session storage if needed for production
+
+## Future Enhancements
+
+1. Add user filtering to queue list (show only my items vs. all items)
+2. Add permission system for queue management operations (cancel, retry, delete)
+3. Implement queue item ownership transfer for administrative purposes
+4. Add audit logging for queue operations with user attribution
+5. Consider implementing user-specific queue limits or quotas
diff --git a/VARIATIONS.md b/VARIATIONS.md
deleted file mode 100644
index cb42ddfd0ed..00000000000
--- a/VARIATIONS.md
+++ /dev/null
@@ -1,113 +0,0 @@
-# Cheat Sheat for Generating Variations
-
-Release 1.13 of SD-Dream adds support for image variations. There are two things that you can do:
-
-1. Generate a series of systematic variations of an image, given a
-prompt. The amount of variation from one image to the next can be
-controlled.
-
-2. Given two or more variations that you like, you can combine them in
-a weighted fashion
-
-This cheat sheet provides a quick guide for how this works in
-practice, using variations to create the desired image of Xena,
-Warrior Princess.
-
-## Step 1 -- find a base image that you like
-
-The prompt we will use throughout is "lucy lawless as xena, warrior
-princess, character portrait, high resolution." This will be indicated
-as "prompt" in the examples below.
-
-First we let SD create a series of images in the usual way, in this case
-requesting six iterations:
-
-~~~
-dream> lucy lawless as xena, warrior princess, character portrait, high resolution -n6
-...
-Outputs:
-./outputs/Xena/000001.1579445059.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S1579445059
-./outputs/Xena/000001.1880768722.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S1880768722
-./outputs/Xena/000001.332057179.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S332057179
-./outputs/Xena/000001.2224800325.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S2224800325
-./outputs/Xena/000001.465250761.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S465250761
-./outputs/Xena/000001.3357757885.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -S3357757885
-~~~
-
-The one with seed 3357757885 looks nice:
-
-
-
-Let's try to generate some variations. Using the same seed, we pass
-the argument -v0.1 (or --variant_amount), which generates a series of
-variations each differing by a variation amount of 0.2. This number
-ranges from 0 to 1.0, with higher numbers being larger amounts of
-variation.
-
-~~~
-dream> "prompt" -n6 -S3357757885 -v0.2
-...
-Outputs:
-./outputs/Xena/000002.784039624.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 784039624:0.2 -S3357757885
-./outputs/Xena/000002.3647897225.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.2 -S3357757885
-./outputs/Xena/000002.917731034.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 917731034:0.2 -S3357757885
-./outputs/Xena/000002.4116285959.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 4116285959:0.2 -S3357757885
-./outputs/Xena/000002.1614299449.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 1614299449:0.2 -S3357757885
-./outputs/Xena/000002.1335553075.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 1335553075:0.2 -S3357757885
-~~~
-
-Note that the output for each image has a -V option giving the
-"variant subseed" for that image, consisting of a seed followed by the
-variation amount used to generate it.
-
-This gives us a series of closely-related variations, including the
-two shown here.
-
-
-
-
-
-I like the expression on Xena's face in the first one (subseed
-3647897225), and the armor on her shoulder in the second one (subseed
-1614299449). Can we combine them to get the best of both worlds?
-
-We combine the two variations using -V (--with_variations). Again, we
-must provide the seed for the originally-chosen image in order for
-this to work.
-
-~~~
-dream> "prompt" -S3357757885 -V3647897225,0.1;1614299449,0.1
-Outputs:
-./outputs/Xena/000003.1614299449.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1 -S3357757885
-~~~
-
-Here we are providing equal weights (0.1 and 0.1) for both the
-subseeds. The resulting image is close, but not exactly what I
-wanted:
-
-
-
-We could either try combining the images with different weights, or we
-can generate more variations around the almost-but-not-quite image. We
-do the latter, using both the -V (combining) and -v (variation
-strength) options. Note that we use -n6 to generate 6 variations:
-
-~~~~
-dream> "prompt" -S3357757885 -V3647897225,0.1;1614299449,0.1 -v0.05 -n6
-Outputs:
-./outputs/Xena/000004.3279757577.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,3279757577:0.05 -S3357757885
-./outputs/Xena/000004.2853129515.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,2853129515:0.05 -S3357757885
-./outputs/Xena/000004.3747154981.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,3747154981:0.05 -S3357757885
-./outputs/Xena/000004.2664260391.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,2664260391:0.05 -S3357757885
-./outputs/Xena/000004.1642517170.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,1642517170:0.05 -S3357757885
-./outputs/Xena/000004.2183375608.png: "prompt" -s50 -W512 -H512 -C7.5 -Ak_lms -V 3647897225:0.1,1614299449:0.1,2183375608:0.05 -S3357757885
-~~~~
-
-This produces six images, all slight variations on the combination of
-the chosen two images. Here's the one I like best:
-
-
-
-As you can see, this is a very powerful too, which when combined with
-subprompt weighting, gives you great control over the content and
-quality of your generated images.
diff --git a/assets/stable-samples/img2img/mountains-2.png b/assets/stable-samples/img2img/mountains-2.png
deleted file mode 100644
index e9f4e708535..00000000000
Binary files a/assets/stable-samples/img2img/mountains-2.png and /dev/null differ
diff --git a/assets/stable-samples/img2img/mountains-3.png b/assets/stable-samples/img2img/mountains-3.png
deleted file mode 100644
index 017de3012c2..00000000000
Binary files a/assets/stable-samples/img2img/mountains-3.png and /dev/null differ
diff --git a/assets/stable-samples/img2img/sketch-mountains-input.jpg b/assets/stable-samples/img2img/sketch-mountains-input.jpg
deleted file mode 100644
index 79d652b8003..00000000000
Binary files a/assets/stable-samples/img2img/sketch-mountains-input.jpg and /dev/null differ
diff --git a/assets/stable-samples/txt2img/merged-0005.png b/assets/stable-samples/txt2img/merged-0005.png
deleted file mode 100644
index ca0a1af2065..00000000000
Binary files a/assets/stable-samples/txt2img/merged-0005.png and /dev/null differ
diff --git a/assets/stable-samples/txt2img/merged-0006.png b/assets/stable-samples/txt2img/merged-0006.png
deleted file mode 100644
index 999f3703230..00000000000
Binary files a/assets/stable-samples/txt2img/merged-0006.png and /dev/null differ
diff --git a/assets/stable-samples/txt2img/merged-0007.png b/assets/stable-samples/txt2img/merged-0007.png
deleted file mode 100644
index af390acaf60..00000000000
Binary files a/assets/stable-samples/txt2img/merged-0007.png and /dev/null differ
diff --git a/assets/v1-variants-scores.jpg b/assets/v1-variants-scores.jpg
deleted file mode 100644
index 9201b985d45..00000000000
Binary files a/assets/v1-variants-scores.jpg and /dev/null differ
diff --git a/configs/autoencoder/autoencoder_kl_16x16x16.yaml b/configs/autoencoder/autoencoder_kl_16x16x16.yaml
deleted file mode 100644
index 5f1d10ec75e..00000000000
--- a/configs/autoencoder/autoencoder_kl_16x16x16.yaml
+++ /dev/null
@@ -1,54 +0,0 @@
-model:
- base_learning_rate: 4.5e-6
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- monitor: "val/rec_loss"
- embed_dim: 16
- lossconfig:
- target: ldm.modules.losses.LPIPSWithDiscriminator
- params:
- disc_start: 50001
- kl_weight: 0.000001
- disc_weight: 0.5
-
- ddconfig:
- double_z: True
- z_channels: 16
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult: [ 1,1,2,2,4] # num_down = len(ch_mult)-1
- num_res_blocks: 2
- attn_resolutions: [16]
- dropout: 0.0
-
-
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 12
- wrap: True
- train:
- target: ldm.data.imagenet.ImageNetSRTrain
- params:
- size: 256
- degradation: pil_nearest
- validation:
- target: ldm.data.imagenet.ImageNetSRValidation
- params:
- size: 256
- degradation: pil_nearest
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 1000
- max_images: 8
- increase_log_steps: True
-
- trainer:
- benchmark: True
- accumulate_grad_batches: 2
diff --git a/configs/autoencoder/autoencoder_kl_32x32x4.yaml b/configs/autoencoder/autoencoder_kl_32x32x4.yaml
deleted file mode 100644
index ab8b36fe6e3..00000000000
--- a/configs/autoencoder/autoencoder_kl_32x32x4.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-model:
- base_learning_rate: 4.5e-6
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- monitor: "val/rec_loss"
- embed_dim: 4
- lossconfig:
- target: ldm.modules.losses.LPIPSWithDiscriminator
- params:
- disc_start: 50001
- kl_weight: 0.000001
- disc_weight: 0.5
-
- ddconfig:
- double_z: True
- z_channels: 4
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult: [ 1,2,4,4 ] # num_down = len(ch_mult)-1
- num_res_blocks: 2
- attn_resolutions: [ ]
- dropout: 0.0
-
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 12
- wrap: True
- train:
- target: ldm.data.imagenet.ImageNetSRTrain
- params:
- size: 256
- degradation: pil_nearest
- validation:
- target: ldm.data.imagenet.ImageNetSRValidation
- params:
- size: 256
- degradation: pil_nearest
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 1000
- max_images: 8
- increase_log_steps: True
-
- trainer:
- benchmark: True
- accumulate_grad_batches: 2
diff --git a/configs/autoencoder/autoencoder_kl_64x64x3.yaml b/configs/autoencoder/autoencoder_kl_64x64x3.yaml
deleted file mode 100644
index 5e3db5c4e28..00000000000
--- a/configs/autoencoder/autoencoder_kl_64x64x3.yaml
+++ /dev/null
@@ -1,54 +0,0 @@
-model:
- base_learning_rate: 4.5e-6
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- monitor: "val/rec_loss"
- embed_dim: 3
- lossconfig:
- target: ldm.modules.losses.LPIPSWithDiscriminator
- params:
- disc_start: 50001
- kl_weight: 0.000001
- disc_weight: 0.5
-
- ddconfig:
- double_z: True
- z_channels: 3
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult: [ 1,2,4 ] # num_down = len(ch_mult)-1
- num_res_blocks: 2
- attn_resolutions: [ ]
- dropout: 0.0
-
-
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 12
- wrap: True
- train:
- target: ldm.data.imagenet.ImageNetSRTrain
- params:
- size: 256
- degradation: pil_nearest
- validation:
- target: ldm.data.imagenet.ImageNetSRValidation
- params:
- size: 256
- degradation: pil_nearest
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 1000
- max_images: 8
- increase_log_steps: True
-
- trainer:
- benchmark: True
- accumulate_grad_batches: 2
diff --git a/configs/autoencoder/autoencoder_kl_8x8x64.yaml b/configs/autoencoder/autoencoder_kl_8x8x64.yaml
deleted file mode 100644
index 5ccd09d38e4..00000000000
--- a/configs/autoencoder/autoencoder_kl_8x8x64.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-model:
- base_learning_rate: 4.5e-6
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- monitor: "val/rec_loss"
- embed_dim: 64
- lossconfig:
- target: ldm.modules.losses.LPIPSWithDiscriminator
- params:
- disc_start: 50001
- kl_weight: 0.000001
- disc_weight: 0.5
-
- ddconfig:
- double_z: True
- z_channels: 64
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult: [ 1,1,2,2,4,4] # num_down = len(ch_mult)-1
- num_res_blocks: 2
- attn_resolutions: [16,8]
- dropout: 0.0
-
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 12
- wrap: True
- train:
- target: ldm.data.imagenet.ImageNetSRTrain
- params:
- size: 256
- degradation: pil_nearest
- validation:
- target: ldm.data.imagenet.ImageNetSRValidation
- params:
- size: 256
- degradation: pil_nearest
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 1000
- max_images: 8
- increase_log_steps: True
-
- trainer:
- benchmark: True
- accumulate_grad_batches: 2
diff --git a/configs/latent-diffusion/celebahq-ldm-vq-4.yaml b/configs/latent-diffusion/celebahq-ldm-vq-4.yaml
deleted file mode 100644
index 89b3df4fe18..00000000000
--- a/configs/latent-diffusion/celebahq-ldm-vq-4.yaml
+++ /dev/null
@@ -1,86 +0,0 @@
-model:
- base_learning_rate: 2.0e-06
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0195
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- image_size: 64
- channels: 3
- monitor: val/loss_simple_ema
-
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 64
- in_channels: 3
- out_channels: 3
- model_channels: 224
- attention_resolutions:
- # note: this isn\t actually the resolution but
- # the downsampling factor, i.e. this corresnponds to
- # attention on spatial resolution 8,16,32, as the
- # spatial reolution of the latents is 64 for f4
- - 8
- - 4
- - 2
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 3
- - 4
- num_head_channels: 32
- first_stage_config:
- target: ldm.models.autoencoder.VQModelInterface
- params:
- embed_dim: 3
- n_embed: 8192
- ckpt_path: models/first_stage_models/vq-f4/model.ckpt
- ddconfig:
- double_z: false
- z_channels: 3
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
- cond_stage_config: __is_unconditional__
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 48
- num_workers: 5
- wrap: false
- train:
- target: taming.data.faceshq.CelebAHQTrain
- params:
- size: 256
- validation:
- target: taming.data.faceshq.CelebAHQValidation
- params:
- size: 256
-
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 5000
- max_images: 8
- increase_log_steps: False
-
- trainer:
- benchmark: True
\ No newline at end of file
diff --git a/configs/latent-diffusion/cin-ldm-vq-f8.yaml b/configs/latent-diffusion/cin-ldm-vq-f8.yaml
deleted file mode 100644
index b8cd9e2ef5d..00000000000
--- a/configs/latent-diffusion/cin-ldm-vq-f8.yaml
+++ /dev/null
@@ -1,98 +0,0 @@
-model:
- base_learning_rate: 1.0e-06
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0195
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- cond_stage_key: class_label
- image_size: 32
- channels: 4
- cond_stage_trainable: true
- conditioning_key: crossattn
- monitor: val/loss_simple_ema
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 32
- in_channels: 4
- out_channels: 4
- model_channels: 256
- attention_resolutions:
- #note: this isn\t actually the resolution but
- # the downsampling factor, i.e. this corresnponds to
- # attention on spatial resolution 8,16,32, as the
- # spatial reolution of the latents is 32 for f8
- - 4
- - 2
- - 1
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 4
- num_head_channels: 32
- use_spatial_transformer: true
- transformer_depth: 1
- context_dim: 512
- first_stage_config:
- target: ldm.models.autoencoder.VQModelInterface
- params:
- embed_dim: 4
- n_embed: 16384
- ckpt_path: configs/first_stage_models/vq-f8/model.yaml
- ddconfig:
- double_z: false
- z_channels: 4
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions:
- - 32
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
- cond_stage_config:
- target: ldm.modules.encoders.modules.ClassEmbedder
- params:
- embed_dim: 512
- key: class_label
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 64
- num_workers: 12
- wrap: false
- train:
- target: ldm.data.imagenet.ImageNetTrain
- params:
- config:
- size: 256
- validation:
- target: ldm.data.imagenet.ImageNetValidation
- params:
- config:
- size: 256
-
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 5000
- max_images: 8
- increase_log_steps: False
-
- trainer:
- benchmark: True
\ No newline at end of file
diff --git a/configs/latent-diffusion/cin256-v2.yaml b/configs/latent-diffusion/cin256-v2.yaml
deleted file mode 100644
index b7c1aa240c7..00000000000
--- a/configs/latent-diffusion/cin256-v2.yaml
+++ /dev/null
@@ -1,68 +0,0 @@
-model:
- base_learning_rate: 0.0001
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0195
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- cond_stage_key: class_label
- image_size: 64
- channels: 3
- cond_stage_trainable: true
- conditioning_key: crossattn
- monitor: val/loss
- use_ema: False
-
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 64
- in_channels: 3
- out_channels: 3
- model_channels: 192
- attention_resolutions:
- - 8
- - 4
- - 2
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 3
- - 5
- num_heads: 1
- use_spatial_transformer: true
- transformer_depth: 1
- context_dim: 512
-
- first_stage_config:
- target: ldm.models.autoencoder.VQModelInterface
- params:
- embed_dim: 3
- n_embed: 8192
- ddconfig:
- double_z: false
- z_channels: 3
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
-
- cond_stage_config:
- target: ldm.modules.encoders.modules.ClassEmbedder
- params:
- n_classes: 1001
- embed_dim: 512
- key: class_label
diff --git a/configs/latent-diffusion/ffhq-ldm-vq-4.yaml b/configs/latent-diffusion/ffhq-ldm-vq-4.yaml
deleted file mode 100644
index 1899e30f772..00000000000
--- a/configs/latent-diffusion/ffhq-ldm-vq-4.yaml
+++ /dev/null
@@ -1,85 +0,0 @@
-model:
- base_learning_rate: 2.0e-06
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0195
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- image_size: 64
- channels: 3
- monitor: val/loss_simple_ema
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 64
- in_channels: 3
- out_channels: 3
- model_channels: 224
- attention_resolutions:
- # note: this isn\t actually the resolution but
- # the downsampling factor, i.e. this corresnponds to
- # attention on spatial resolution 8,16,32, as the
- # spatial reolution of the latents is 64 for f4
- - 8
- - 4
- - 2
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 3
- - 4
- num_head_channels: 32
- first_stage_config:
- target: ldm.models.autoencoder.VQModelInterface
- params:
- embed_dim: 3
- n_embed: 8192
- ckpt_path: configs/first_stage_models/vq-f4/model.yaml
- ddconfig:
- double_z: false
- z_channels: 3
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
- cond_stage_config: __is_unconditional__
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 42
- num_workers: 5
- wrap: false
- train:
- target: taming.data.faceshq.FFHQTrain
- params:
- size: 256
- validation:
- target: taming.data.faceshq.FFHQValidation
- params:
- size: 256
-
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 5000
- max_images: 8
- increase_log_steps: False
-
- trainer:
- benchmark: True
\ No newline at end of file
diff --git a/configs/latent-diffusion/lsun_bedrooms-ldm-vq-4.yaml b/configs/latent-diffusion/lsun_bedrooms-ldm-vq-4.yaml
deleted file mode 100644
index c4ca66c16c0..00000000000
--- a/configs/latent-diffusion/lsun_bedrooms-ldm-vq-4.yaml
+++ /dev/null
@@ -1,85 +0,0 @@
-model:
- base_learning_rate: 2.0e-06
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0195
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- image_size: 64
- channels: 3
- monitor: val/loss_simple_ema
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 64
- in_channels: 3
- out_channels: 3
- model_channels: 224
- attention_resolutions:
- # note: this isn\t actually the resolution but
- # the downsampling factor, i.e. this corresnponds to
- # attention on spatial resolution 8,16,32, as the
- # spatial reolution of the latents is 64 for f4
- - 8
- - 4
- - 2
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 3
- - 4
- num_head_channels: 32
- first_stage_config:
- target: ldm.models.autoencoder.VQModelInterface
- params:
- ckpt_path: configs/first_stage_models/vq-f4/model.yaml
- embed_dim: 3
- n_embed: 8192
- ddconfig:
- double_z: false
- z_channels: 3
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
- cond_stage_config: __is_unconditional__
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 48
- num_workers: 5
- wrap: false
- train:
- target: ldm.data.lsun.LSUNBedroomsTrain
- params:
- size: 256
- validation:
- target: ldm.data.lsun.LSUNBedroomsValidation
- params:
- size: 256
-
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 5000
- max_images: 8
- increase_log_steps: False
-
- trainer:
- benchmark: True
\ No newline at end of file
diff --git a/configs/latent-diffusion/lsun_churches-ldm-kl-8.yaml b/configs/latent-diffusion/lsun_churches-ldm-kl-8.yaml
deleted file mode 100644
index 18dc8c2d9cf..00000000000
--- a/configs/latent-diffusion/lsun_churches-ldm-kl-8.yaml
+++ /dev/null
@@ -1,91 +0,0 @@
-model:
- base_learning_rate: 5.0e-5 # set to target_lr by starting main.py with '--scale_lr False'
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.0155
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- loss_type: l1
- first_stage_key: "image"
- cond_stage_key: "image"
- image_size: 32
- channels: 4
- cond_stage_trainable: False
- concat_mode: False
- scale_by_std: True
- monitor: 'val/loss_simple_ema'
-
- scheduler_config: # 10000 warmup steps
- target: ldm.lr_scheduler.LambdaLinearScheduler
- params:
- warm_up_steps: [10000]
- cycle_lengths: [10000000000000]
- f_start: [1.e-6]
- f_max: [1.]
- f_min: [ 1.]
-
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 32
- in_channels: 4
- out_channels: 4
- model_channels: 192
- attention_resolutions: [ 1, 2, 4, 8 ] # 32, 16, 8, 4
- num_res_blocks: 2
- channel_mult: [ 1,2,2,4,4 ] # 32, 16, 8, 4, 2
- num_heads: 8
- use_scale_shift_norm: True
- resblock_updown: True
-
- first_stage_config:
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- embed_dim: 4
- monitor: "val/rec_loss"
- ckpt_path: "models/first_stage_models/kl-f8/model.ckpt"
- ddconfig:
- double_z: True
- z_channels: 4
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult: [ 1,2,4,4 ] # num_down = len(ch_mult)-1
- num_res_blocks: 2
- attn_resolutions: [ ]
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
-
- cond_stage_config: "__is_unconditional__"
-
-data:
- target: main.DataModuleFromConfig
- params:
- batch_size: 96
- num_workers: 5
- wrap: False
- train:
- target: ldm.data.lsun.LSUNChurchesTrain
- params:
- size: 256
- validation:
- target: ldm.data.lsun.LSUNChurchesValidation
- params:
- size: 256
-
-lightning:
- callbacks:
- image_logger:
- target: main.ImageLogger
- params:
- batch_frequency: 5000
- max_images: 8
- increase_log_steps: False
-
-
- trainer:
- benchmark: True
\ No newline at end of file
diff --git a/configs/latent-diffusion/txt2img-1p4B-eval.yaml b/configs/latent-diffusion/txt2img-1p4B-eval.yaml
deleted file mode 100644
index 8e331cbfdff..00000000000
--- a/configs/latent-diffusion/txt2img-1p4B-eval.yaml
+++ /dev/null
@@ -1,71 +0,0 @@
-model:
- base_learning_rate: 5.0e-05
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.00085
- linear_end: 0.012
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: image
- cond_stage_key: caption
- image_size: 32
- channels: 4
- cond_stage_trainable: true
- conditioning_key: crossattn
- monitor: val/loss_simple_ema
- scale_factor: 0.18215
- use_ema: False
-
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 32
- in_channels: 4
- out_channels: 4
- model_channels: 320
- attention_resolutions:
- - 4
- - 2
- - 1
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 4
- - 4
- num_heads: 8
- use_spatial_transformer: true
- transformer_depth: 1
- context_dim: 1280
- use_checkpoint: true
- legacy: False
-
- first_stage_config:
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- embed_dim: 4
- monitor: val/rec_loss
- ddconfig:
- double_z: true
- z_channels: 4
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
-
- cond_stage_config:
- target: ldm.modules.encoders.modules.BERTEmbedder
- params:
- n_embed: 1280
- n_layer: 32
diff --git a/configs/models.yaml b/configs/models.yaml
deleted file mode 100644
index a3c929d29fb..00000000000
--- a/configs/models.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-# This file describes the alternative machine learning models
-# available to the dream script.
-#
-# To add a new model, follow the examples below. Each
-# model requires a model config file, a weights file,
-# and the width and height of the images it
-# was trained on.
-
-laion400m:
- config: configs/latent-diffusion/txt2img-1p4B-eval.yaml
- weights: models/ldm/text2img-large/model.ckpt
- width: 256
- height: 256
-stable-diffusion-1.4:
- config: configs/stable-diffusion/v1-inference.yaml
- weights: models/ldm/stable-diffusion-v1/model.ckpt
- width: 512
- height: 512
diff --git a/configs/retrieval-augmented-diffusion/768x768.yaml b/configs/retrieval-augmented-diffusion/768x768.yaml
deleted file mode 100644
index b51b1d8373c..00000000000
--- a/configs/retrieval-augmented-diffusion/768x768.yaml
+++ /dev/null
@@ -1,68 +0,0 @@
-model:
- base_learning_rate: 0.0001
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.0015
- linear_end: 0.015
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: jpg
- cond_stage_key: nix
- image_size: 48
- channels: 16
- cond_stage_trainable: false
- conditioning_key: crossattn
- monitor: val/loss_simple_ema
- scale_by_std: false
- scale_factor: 0.22765929
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 48
- in_channels: 16
- out_channels: 16
- model_channels: 448
- attention_resolutions:
- - 4
- - 2
- - 1
- num_res_blocks: 2
- channel_mult:
- - 1
- - 2
- - 3
- - 4
- use_scale_shift_norm: false
- resblock_updown: false
- num_head_channels: 32
- use_spatial_transformer: true
- transformer_depth: 1
- context_dim: 768
- use_checkpoint: true
- first_stage_config:
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- monitor: val/rec_loss
- embed_dim: 16
- ddconfig:
- double_z: true
- z_channels: 16
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 1
- - 2
- - 2
- - 4
- num_res_blocks: 2
- attn_resolutions:
- - 16
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
- cond_stage_config:
- target: torch.nn.Identity
\ No newline at end of file
diff --git a/configs/stable-diffusion/v1-inference.yaml b/configs/stable-diffusion/v1-inference.yaml
deleted file mode 100644
index 59d8f331250..00000000000
--- a/configs/stable-diffusion/v1-inference.yaml
+++ /dev/null
@@ -1,79 +0,0 @@
-model:
- base_learning_rate: 1.0e-04
- target: ldm.models.diffusion.ddpm.LatentDiffusion
- params:
- linear_start: 0.00085
- linear_end: 0.0120
- num_timesteps_cond: 1
- log_every_t: 200
- timesteps: 1000
- first_stage_key: "jpg"
- cond_stage_key: "txt"
- image_size: 64
- channels: 4
- cond_stage_trainable: false # Note: different from the one we trained before
- conditioning_key: crossattn
- monitor: val/loss_simple_ema
- scale_factor: 0.18215
- use_ema: False
-
- scheduler_config: # 10000 warmup steps
- target: ldm.lr_scheduler.LambdaLinearScheduler
- params:
- warm_up_steps: [ 10000 ]
- cycle_lengths: [ 10000000000000 ] # incredibly large number to prevent corner cases
- f_start: [ 1.e-6 ]
- f_max: [ 1. ]
- f_min: [ 1. ]
-
- personalization_config:
- target: ldm.modules.embedding_manager.EmbeddingManager
- params:
- placeholder_strings: ["*"]
- initializer_words: ["sculpture"]
- per_image_tokens: false
- num_vectors_per_token: 1
- progressive_words: False
-
- unet_config:
- target: ldm.modules.diffusionmodules.openaimodel.UNetModel
- params:
- image_size: 32 # unused
- in_channels: 4
- out_channels: 4
- model_channels: 320
- attention_resolutions: [ 4, 2, 1 ]
- num_res_blocks: 2
- channel_mult: [ 1, 2, 4, 4 ]
- num_heads: 8
- use_spatial_transformer: True
- transformer_depth: 1
- context_dim: 768
- use_checkpoint: True
- legacy: False
-
- first_stage_config:
- target: ldm.models.autoencoder.AutoencoderKL
- params:
- embed_dim: 4
- monitor: val/rec_loss
- ddconfig:
- double_z: true
- z_channels: 4
- resolution: 256
- in_channels: 3
- out_ch: 3
- ch: 128
- ch_mult:
- - 1
- - 2
- - 4
- - 4
- num_res_blocks: 2
- attn_resolutions: []
- dropout: 0.0
- lossconfig:
- target: torch.nn.Identity
-
- cond_stage_config:
- target: ldm.modules.encoders.modules.FrozenCLIPEmbedder
diff --git a/coverage/.gitignore b/coverage/.gitignore
new file mode 100644
index 00000000000..86d0cb2726c
--- /dev/null
+++ b/coverage/.gitignore
@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore
\ No newline at end of file
diff --git a/docker/.env.sample b/docker/.env.sample
new file mode 100644
index 00000000000..7b10af936e1
--- /dev/null
+++ b/docker/.env.sample
@@ -0,0 +1,31 @@
+## Make a copy of this file named `.env` and fill in the values below.
+## Any environment variables supported by InvokeAI can be specified here,
+## in addition to the examples below.
+
+## INVOKEAI_ROOT is the path *on the host system* where Invoke will store its data.
+## It is mounted into the container and allows both containerized and non-containerized usage of Invoke.
+# Usually this is the only variable you need to set. It can be relative or absolute.
+# INVOKEAI_ROOT=~/invokeai
+
+## HOST_INVOKEAI_ROOT and CONTAINER_INVOKEAI_ROOT can be used to control the on-host
+## and in-container paths separately, if needed.
+## HOST_INVOKEAI_ROOT is the path on the docker host's filesystem where Invoke will store data.
+## If relative, it will be relative to the docker directory in which the docker-compose.yml file is located
+## CONTAINER_INVOKEAI_ROOT is the path within the container where Invoke will expect to find the runtime directory.
+## It MUST be absolute. There is usually no need to change this.
+# HOST_INVOKEAI_ROOT=../../invokeai-data
+# CONTAINER_INVOKEAI_ROOT=/invokeai
+
+## INVOKEAI_PORT is the port on which the InvokeAI web interface will be available
+# INVOKEAI_PORT=9090
+
+## GPU_DRIVER can be set to either `cuda` or `rocm` to enable GPU support in the container accordingly.
+# GPU_DRIVER=cuda #| rocm
+
+## If you are using ROCM, you will need to ensure that the render group within the container and the host system use the same group ID.
+## To obtain the group ID of the render group on the host system, run `getent group render` and grab the number.
+# RENDER_GROUP_ID=
+
+## CONTAINER_UID can be set to the UID of the user on the host system that should own the files in the container.
+## It is usually not necessary to change this. Use `id -u` on the host system to find the UID.
+# CONTAINER_UID=1000
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000000..b1b709d54df
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,107 @@
+# syntax=docker/dockerfile:1.4
+
+#### Web UI ------------------------------------
+
+FROM docker.io/node:22-slim AS web-builder
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack use pnpm@10.x && corepack enable
+
+WORKDIR /build
+COPY invokeai/frontend/web/ ./
+RUN --mount=type=cache,target=/pnpm/store \
+ pnpm install --frozen-lockfile
+RUN npx vite build
+
+## Backend ---------------------------------------
+
+FROM library/ubuntu:24.04
+
+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 --mount=type=cache,target=/var/cache/apt \
+ --mount=type=cache,target=/var/lib/apt \
+ apt update && apt install -y --no-install-recommends \
+ ca-certificates \
+ git \
+ gosu \
+ libglib2.0-0 \
+ libgl1 \
+ libglx-mesa0 \
+ build-essential \
+ libopencv-dev \
+ libstdc++-10-dev
+
+ENV \
+ PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ VIRTUAL_ENV=/opt/venv \
+ INVOKEAI_SRC=/opt/invokeai \
+ PYTHON_VERSION=3.12 \
+ UV_PYTHON=3.12 \
+ UV_COMPILE_BYTECODE=1 \
+ UV_MANAGED_PYTHON=1 \
+ UV_LINK_MODE=copy \
+ UV_PROJECT_ENVIRONMENT=/opt/venv \
+ INVOKEAI_ROOT=/invokeai \
+ INVOKEAI_HOST=0.0.0.0 \
+ INVOKEAI_PORT=9090 \
+ PATH="/opt/venv/bin:$PATH" \
+ CONTAINER_UID=${CONTAINER_UID:-1000} \
+ CONTAINER_GID=${CONTAINER_GID:-1000}
+
+ARG GPU_DRIVER=cuda
+
+# Install `uv` for package management
+COPY --from=ghcr.io/astral-sh/uv:0.6.9 /uv /uvx /bin/
+
+# Install python & allow non-root user to use it by traversing the /root dir without read permissions
+RUN --mount=type=cache,target=/root/.cache/uv \
+ uv python install ${PYTHON_VERSION} && \
+ # chmod --recursive a+rX /root/.local/share/uv/python
+ chmod 711 /root
+
+WORKDIR ${INVOKEAI_SRC}
+
+# Install project's dependencies as a separate layer so they aren't rebuilt every commit.
+# bind-mount instead of copy to defer adding sources to the image until next layer.
+#
+# NOTE: there are no pytorch builds for arm64 + cuda, only cpu
+# x86_64/CUDA is the default
+RUN --mount=type=cache,target=/root/.cache/uv \
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
+ --mount=type=bind,source=uv.lock,target=uv.lock \
+ # this is just to get the package manager to recognize that the project exists, without making changes to the docker layer
+ --mount=type=bind,source=invokeai/version,target=invokeai/version \
+ ulimit -n 30000 && \
+ uv sync --extra $GPU_DRIVER --frozen
+
+# Link amdgpu.ids for ROCm builds
+# contributed by https://github.com/Rubonnek
+RUN mkdir -p "/opt/amdgpu/share/libdrm" &&\
+ ln -s "/usr/share/libdrm/amdgpu.ids" "/opt/amdgpu/share/libdrm/amdgpu.ids" && groupadd render
+
+# build patchmatch
+RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
+RUN python -c "from patchmatch import patch_match"
+
+RUN mkdir -p ${INVOKEAI_ROOT} && chown -R ${CONTAINER_UID}:${CONTAINER_GID} ${INVOKEAI_ROOT}
+
+COPY docker/docker-entrypoint.sh ./
+ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"]
+CMD ["invokeai-web"]
+
+# --link requires buldkit w/ dockerfile syntax 1.4, does not work with podman
+COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
+
+# add sources last to minimize image changes on code changes
+COPY invokeai ${INVOKEAI_SRC}/invokeai
+
+# this should not increase image size because we've already installed dependencies
+# in a previous layer
+RUN --mount=type=cache,target=/root/.cache/uv \
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
+ --mount=type=bind,source=uv.lock,target=uv.lock \
+ ulimit -n 30000 && \
+ uv pip install -e .[$GPU_DRIVER]
+
diff --git a/docker/Dockerfile-rocm-full b/docker/Dockerfile-rocm-full
new file mode 100644
index 00000000000..087864b3f2f
--- /dev/null
+++ b/docker/Dockerfile-rocm-full
@@ -0,0 +1,136 @@
+# syntax=docker/dockerfile:1.4
+
+#### Web UI ------------------------------------
+
+FROM docker.io/node:22-slim AS web-builder
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack use pnpm@8.x
+RUN corepack enable
+
+WORKDIR /build
+COPY invokeai/frontend/web/ ./
+RUN --mount=type=cache,target=/pnpm/store \
+ pnpm install --frozen-lockfile
+RUN npx vite build
+
+## Backend ---------------------------------------
+
+FROM library/ubuntu:24.04
+
+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 --mount=type=cache,target=/var/cache/apt \
+ --mount=type=cache,target=/var/lib/apt \
+ apt update && apt install -y --no-install-recommends \
+ ca-certificates \
+ git \
+ gosu \
+ libglib2.0-0 \
+ libgl1 \
+ libglx-mesa0 \
+ build-essential \
+ libopencv-dev \
+ libstdc++-10-dev \
+ wget
+
+ENV \
+ PYTHONUNBUFFERED=1 \
+ PYTHONDONTWRITEBYTECODE=1 \
+ VIRTUAL_ENV=/opt/venv \
+ INVOKEAI_SRC=/opt/invokeai \
+ PYTHON_VERSION=3.12 \
+ UV_PYTHON=3.12 \
+ UV_COMPILE_BYTECODE=1 \
+ UV_MANAGED_PYTHON=1 \
+ UV_LINK_MODE=copy \
+ UV_PROJECT_ENVIRONMENT=/opt/venv \
+ INVOKEAI_ROOT=/invokeai \
+ INVOKEAI_HOST=0.0.0.0 \
+ INVOKEAI_PORT=9090 \
+ PATH="/opt/venv/bin:$PATH" \
+ CONTAINER_UID=${CONTAINER_UID:-1000} \
+ CONTAINER_GID=${CONTAINER_GID:-1000}
+
+ARG GPU_DRIVER=cuda
+
+# Install `uv` for package management
+COPY --from=ghcr.io/astral-sh/uv:0.6.9 /uv /uvx /bin/
+
+# Install python & allow non-root user to use it by traversing the /root dir without read permissions
+RUN --mount=type=cache,target=/root/.cache/uv \
+ uv python install ${PYTHON_VERSION} && \
+ # chmod --recursive a+rX /root/.local/share/uv/python
+ chmod 711 /root
+
+WORKDIR ${INVOKEAI_SRC}
+
+# Install project's dependencies as a separate layer so they aren't rebuilt every commit.
+# bind-mount instead of copy to defer adding sources to the image until next layer.
+#
+# NOTE: there are no pytorch builds for arm64 + cuda, only cpu
+# x86_64/CUDA is the default
+RUN --mount=type=cache,target=/root/.cache/uv \
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
+ --mount=type=bind,source=uv.lock,target=uv.lock \
+ # this is just to get the package manager to recognize that the project exists, without making changes to the docker layer
+ --mount=type=bind,source=invokeai/version,target=invokeai/version \
+ ulimit -n 30000 && \
+ uv sync --extra $GPU_DRIVER --frozen
+
+RUN --mount=type=cache,target=/var/cache/apt \
+ --mount=type=cache,target=/var/lib/apt \
+ if [ "$GPU_DRIVER" = "rocm" ]; then \
+ wget -O /tmp/amdgpu-install.deb \
+ https://repo.radeon.com/amdgpu-install/6.3.4/ubuntu/noble/amdgpu-install_6.3.60304-1_all.deb && \
+ apt install -y /tmp/amdgpu-install.deb && \
+ apt update && \
+ amdgpu-install --usecase=rocm -y && \
+ apt-get autoclean && \
+ apt clean && \
+ rm -rf /tmp/* /var/tmp/* && \
+ usermod -a -G render ubuntu && \
+ usermod -a -G video ubuntu && \
+ echo "\\n/opt/rocm/lib\\n/opt/rocm/lib64" >> /etc/ld.so.conf.d/rocm.conf && \
+ ldconfig && \
+ update-alternatives --auto rocm; \
+ fi
+
+## Heathen711: Leaving this for review input, will remove before merge
+# RUN --mount=type=cache,target=/var/cache/apt \
+# --mount=type=cache,target=/var/lib/apt \
+# if [ "$GPU_DRIVER" = "rocm" ]; then \
+# groupadd render && \
+# usermod -a -G render ubuntu && \
+# usermod -a -G video ubuntu; \
+# fi
+
+## Link amdgpu.ids for ROCm builds
+## contributed by https://github.com/Rubonnek
+# RUN mkdir -p "/opt/amdgpu/share/libdrm" &&\
+# ln -s "/usr/share/libdrm/amdgpu.ids" "/opt/amdgpu/share/libdrm/amdgpu.ids"
+
+# build patchmatch
+RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
+RUN python -c "from patchmatch import patch_match"
+
+RUN mkdir -p ${INVOKEAI_ROOT} && chown -R ${CONTAINER_UID}:${CONTAINER_GID} ${INVOKEAI_ROOT}
+
+COPY docker/docker-entrypoint.sh ./
+ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"]
+CMD ["invokeai-web"]
+
+# --link requires buldkit w/ dockerfile syntax 1.4, does not work with podman
+COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
+
+# add sources last to minimize image changes on code changes
+COPY invokeai ${INVOKEAI_SRC}/invokeai
+
+# this should not increase image size because we've already installed dependencies
+# in a previous layer
+RUN --mount=type=cache,target=/root/.cache/uv \
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
+ --mount=type=bind,source=uv.lock,target=uv.lock \
+ ulimit -n 30000 && \
+ uv pip install -e .[$GPU_DRIVER]
+
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 00000000000..b9c7c010f4a
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,117 @@
+# Invoke in Docker
+
+First things first:
+
+- Ensure that Docker can use your [NVIDIA][nvidia docker docs] or [AMD][amd docker docs] GPU.
+- This document assumes a Linux system, but should work similarly under Windows with WSL2.
+- We don't recommend running Invoke in Docker on macOS at this time. It works, but very slowly.
+
+## Quickstart
+
+No `docker compose`, no persistence, single command, using the official images:
+
+**CUDA (NVIDIA GPU):**
+
+```bash
+docker run --runtime=nvidia --gpus=all --publish 9090:9090 ghcr.io/invoke-ai/invokeai
+```
+
+**ROCm (AMD GPU):**
+
+```bash
+docker run --device /dev/kfd --device /dev/dri --publish 9090:9090 ghcr.io/invoke-ai/invokeai:main-rocm
+```
+
+Open `http://localhost:9090` in your browser once the container finishes booting, install some models, and generate away!
+
+### Data persistence
+
+To persist your generated images and downloaded models outside of the container, add a `--volume/-v` flag to the above command, e.g.:
+
+```bash
+docker run --volume /some/local/path:/invokeai {...etc...}
+```
+
+`/some/local/path/invokeai` will contain all your data.
+It can *usually* be reused between different installs of Invoke. Tread with caution and read the release notes!
+
+## Customize the container
+
+The included `run.sh` script is a convenience wrapper around `docker compose`. It can be helpful for passing additional build arguments to `docker compose`. Alternatively, the familiar `docker compose` commands work just as well.
+
+```bash
+cd docker
+cp .env.sample .env
+# edit .env to your liking if you need to; it is well commented.
+./run.sh
+```
+
+It will take a few minutes to build the image the first time. Once the application starts up, open `http://localhost:9090` in your browser to invoke!
+
+>[!TIP]
+>When using the `run.sh` script, the container will continue running after Ctrl+C. To shut it down, use the `docker compose down` command.
+
+## Docker setup in detail
+
+#### Linux
+
+1. Ensure buildkit 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).
+ - The deprecated `docker-compose` (hyphenated) CLI probably won't work. Update to a recent version.
+3. Ensure docker daemon is able to access the GPU.
+ - [NVIDIA docs](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
+ - [AMD docs](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html)
+
+#### macOS
+
+> [!TIP]
+> You'll be better off installing Invoke directly on your system, because Docker can not use the GPU on macOS.
+
+If you are still reading:
+
+1. Ensure Docker has at least 16GB RAM
+2. Enable VirtioFS for file sharing
+3. Enable `docker compose` V2 support
+
+This is done via Docker Desktop preferences.
+
+### Configure the Invoke Environment
+
+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 the desired location of the InvokeAI runtime directory. It may be an existing directory from a previous installation (post 4.0.0).
+1. Execute `run.sh`
+
+The image will be built automatically if needed.
+
+The runtime directory (holding models and outputs) will be created in the location specified by `INVOKEAI_ROOT`. The default location is `~/invokeai`. Navigate to the Model Manager tab and install some models before generating.
+
+### Use a GPU
+
+- Linux is *recommended* for GPU support in Docker.
+- WSL2 is *required* for Windows.
+- only `x86_64` architecture is supported.
+
+The Docker daemon on the system must be already set up to use the GPU. In case of Linux, this involves installing `nvidia-docker-runtime` and configuring the `nvidia` runtime as default. Steps will be different for AMD. Please see Docker/NVIDIA/AMD documentation for the most up-to-date instructions for using your GPU with Docker.
+
+To use an AMD GPU, set `GPU_DRIVER=rocm` in your `.env` file before running `./run.sh`.
+
+## Customize
+
+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 `run.sh`, 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.
+
+Values are optional, but setting `INVOKEAI_ROOT` is highly recommended. The default is `~/invokeai`. Example:
+
+```bash
+INVOKEAI_ROOT=/Volumes/WorkDrive/invokeai
+HUGGINGFACE_TOKEN=the_actual_token
+CONTAINER_UID=1000
+GPU_DRIVER=cuda
+```
+
+Any environment variables supported by InvokeAI can be set here. See the [Configuration docs](https://invoke.ai/configuration/invokeai-yaml/) for further detail.
+
+---
+
+[nvidia docker docs]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
+[amd docker docs]: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
new file mode 100644
index 00000000000..2e5bc91f260
--- /dev/null
+++ b/docker/docker-compose.yml
@@ -0,0 +1,55 @@
+# Copyright (c) 2023 Eugene Brodsky https://github.com/ebr
+
+x-invokeai: &invokeai
+ image: "ghcr.io/invoke-ai/invokeai:latest"
+ build:
+ context: ..
+ dockerfile: docker/Dockerfile
+
+ # Create a .env file in the same directory as this docker-compose.yml file
+ # and populate it with environment variables. See .env.sample
+ env_file:
+ - .env
+
+ # variables without a default will automatically inherit from the host environment
+ environment:
+ # if set, CONTAINER_INVOKEAI_ROOT will override the Invoke runtime directory location *inside* the container
+ - INVOKEAI_ROOT=${CONTAINER_INVOKEAI_ROOT:-/invokeai}
+ - HF_HOME
+ ports:
+ - "${INVOKEAI_PORT:-9090}:${INVOKEAI_PORT:-9090}"
+ volumes:
+ - type: bind
+ source: ${HOST_INVOKEAI_ROOT:-${INVOKEAI_ROOT:-~/invokeai}}
+ target: ${CONTAINER_INVOKEAI_ROOT:-/invokeai}
+ bind:
+ create_host_path: true
+ - ${HF_HOME:-~/.cache/huggingface}:${HF_HOME:-/invokeai/.cache/huggingface}
+ tty: true
+ stdin_open: true
+
+
+services:
+ invokeai-cuda:
+ <<: *invokeai
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: 1
+ capabilities: [gpu]
+
+ invokeai-cpu:
+ <<: *invokeai
+ profiles:
+ - cpu
+
+ invokeai-rocm:
+ <<: *invokeai
+ environment:
+ - AMD_VISIBLE_DEVICES=all
+ - RENDER_GROUP_ID=${RENDER_GROUP_ID}
+ runtime: amd
+ profiles:
+ - rocm
diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh
new file mode 100755
index 00000000000..12f24a93527
--- /dev/null
+++ b/docker/docker-entrypoint.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+set -e -o pipefail
+
+### Container entrypoint
+# Runs the CMD as defined by the Dockerfile or passed to `docker run`
+# Can be used to configure the runtime dir
+# Bypass by using ENTRYPOINT or `--entrypoint`
+
+### Set INVOKEAI_ROOT pointing to a valid runtime directory
+# Otherwise configure the runtime dir first.
+
+### Set the CONTAINER_UID envvar to match your user.
+# Ensures files created in the container are owned by you:
+# docker run --rm -it -v /some/path:/invokeai -e CONTAINER_UID=$(id -u)
+# Default UID: 1000 chosen due to popularity on Linux systems. Possibly 501 on MacOS.
+
+USER_ID=${CONTAINER_UID:-1000}
+USER=ubuntu
+# if the user does not exist, create it. It is expected to be present on ubuntu >=24.x
+_=$(id ${USER} 2>&1) || useradd -u ${USER_ID} ${USER}
+# ensure the UID is correct
+usermod -u ${USER_ID} ${USER} 1>/dev/null
+
+## ROCM specific configuration
+# render group within the container must match the host render group
+# otherwise the container will not be able to access the host GPU.
+if [[ -v "RENDER_GROUP_ID" ]] && [[ ! -z "${RENDER_GROUP_ID}" ]]; then
+ # ensure the render group exists
+ groupmod -g ${RENDER_GROUP_ID} render
+ usermod -a -G render ${USER}
+ usermod -a -G video ${USER}
+fi
+
+
+### Set the $PUBLIC_KEY env var to enable SSH access.
+# We do not install openssh-server in the image by default to avoid bloat.
+# but it is useful to have the full SSH server e.g. on Runpod.
+# (use SCP to copy files to/from the image, etc)
+if [[ -v "PUBLIC_KEY" ]] && [[ ! -d "${HOME}/.ssh" ]]; then
+ apt-get update
+ apt-get install -y openssh-server
+ pushd "$HOME"
+ mkdir -p .ssh
+ echo "${PUBLIC_KEY}" >.ssh/authorized_keys
+ chmod -R 700 .ssh
+ popd
+ service ssh start
+fi
+
+mkdir -p "${INVOKEAI_ROOT}"
+chown --recursive ${USER} "${INVOKEAI_ROOT}" || true
+cd "${INVOKEAI_ROOT}"
+export HF_HOME=${HF_HOME:-$INVOKEAI_ROOT/.cache/huggingface}
+export MPLCONFIGDIR=${MPLCONFIGDIR:-$INVOKEAI_ROOT/.matplotlib}
+
+# Run the CMD as the Container User (not root).
+exec gosu ${USER} "$@"
diff --git a/docker/run.sh b/docker/run.sh
new file mode 100755
index 00000000000..272d4e44a5c
--- /dev/null
+++ b/docker/run.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -e -o pipefail
+
+run() {
+ local scriptdir=$(dirname "${BASH_SOURCE[0]}")
+ cd "$scriptdir" || exit 1
+
+ local build_args=""
+ local profile=""
+
+ # create .env file if it doesn't exist, otherwise docker compose will fail
+ touch .env
+
+ # parse .env file for build args
+ build_args=$(awk '$1 ~ /=[^$]/ && $0 !~ /^#/ {print "--build-arg " $0 " "}' .env) &&
+ profile="$(awk -F '=' '/GPU_DRIVER=/ {print $2}' .env)"
+
+ # default to 'cuda' profile
+ [[ -z "$profile" ]] && profile="cuda"
+
+ local service_name="invokeai-$profile"
+
+ if [[ ! -z "$build_args" ]]; then
+ printf "%s\n" "docker compose build args:"
+ printf "%s\n" "$build_args"
+ fi
+
+ docker compose build $build_args $service_name
+ unset build_args
+
+ printf "%s\n" "starting service $service_name"
+ docker compose --profile "$profile" up -d "$service_name"
+ docker compose --profile "$profile" logs -f
+}
+
+run
diff --git a/docker/runpod-readme.md b/docker/runpod-readme.md
new file mode 100644
index 00000000000..c464480d46d
--- /dev/null
+++ b/docker/runpod-readme.md
@@ -0,0 +1,60 @@
+# InvokeAI - A Stable Diffusion Toolkit
+
+Stable Diffusion distribution by InvokeAI: https://github.com/invoke-ai
+
+The Docker image tracks the `main` branch of the InvokeAI project, which means it includes the latest features, but may contain some bugs.
+
+Your working directory is mounted under the `/workspace` path inside the pod. The models are in `/workspace/invokeai/models`, and outputs are in `/workspace/invokeai/outputs`.
+
+> **Only the /workspace directory will persist between pod restarts!**
+
+> **If you _terminate_ (not just _stop_) the pod, the /workspace will be lost.**
+
+## Quickstart
+
+1. Launch a pod from this template. **It will take about 5-10 minutes to run through the initial setup**. Be patient.
+1. Wait for the application to load.
+ - TIP: you know it's ready when the CPU usage goes idle
+ - You can also check the logs for a line that says "_Point your browser at..._"
+1. Open the Invoke AI web UI: click the `Connect` => `connect over HTTP` button.
+1. Generate some art!
+
+## Other things you can do
+
+At any point you may edit the pod configuration and set an arbitrary Docker command. For example, you could run a command to downloads some models using `curl`, or fetch some images and place them into your outputs to continue a working session.
+
+If you need to run *multiple commands*, define them in the Docker Command field like this:
+
+`bash -c "cd ${INVOKEAI_ROOT}/outputs; wormhole receive 2-foo-bar; invoke.py --web --host 0.0.0.0"`
+
+### Copying your data in and out of the pod
+
+This image includes a couple of handy tools to help you get the data into the pod (such as your custom models or embeddings), and out of the pod (such as downloading your outputs). Here are your options for getting your data in and out of the pod:
+
+- **SSH server**:
+ 1. Make sure to create and set your Public Key in the RunPod settings (follow the official instructions)
+ 1. Add an exposed port 22 (TCP) in the pod settings!
+ 1. When your pod restarts, you will see a new entry in the `Connect` dialog. Use this SSH server to `scp` or `sftp` your files as necessary, or SSH into the pod using the fully fledged SSH server.
+
+- [**Magic Wormhole**](https://magic-wormhole.readthedocs.io/en/latest/welcome.html):
+ 1. On your computer, `pip install magic-wormhole` (see above instructions for details)
+ 1. Connect to the command line **using the "light" SSH client** or the browser-based console. _Currently there's a bug where `wormhole` isn't available when connected to "full" SSH server, as described above_.
+ 1. `wormhole send /workspace/invokeai/outputs` will send the entire `outputs` directory. You can also send individual files.
+ 1. Once packaged, you will see a `wormhole receive <123-some-words>` command. Copy it
+ 1. Paste this command into the terminal on your local machine to securely download the payload.
+ 1. It works the same in reverse: you can `wormhole send` some models from your computer to the pod. Again, save your files somewhere in `/workspace` or they will be lost when the pod is stopped.
+
+- **RunPod's Cloud Sync feature** may be used to sync the persistent volume to cloud storage. You could, for example, copy the entire `/workspace` to S3, add some custom models to it, and copy it back from S3 when launching new pod configurations. Follow the Cloud Sync instructions.
+
+
+### Disable the NSFW checker
+
+The NSFW checker is enabled by default. To disable it, edit the pod configuration and set the following command:
+
+```
+invoke --web --host 0.0.0.0 --no-nsfw_checker
+```
+
+---
+
+Template ©2023 Eugene Brodsky [ebr](https://github.com/ebr)
\ No newline at end of file
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..6240da8b10b
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,21 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000000..591a5c353f9
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1 @@
+# Invoke AI Documentation
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
new file mode 100644
index 00000000000..ebb59d36115
--- /dev/null
+++ b/docs/astro.config.mjs
@@ -0,0 +1,92 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+
+// Plugins
+import starlightLinksValidator from 'starlight-links-validator';
+import starlightLlmsText from 'starlight-llms-txt';
+import starlightChangelogs from 'starlight-changelogs';
+import { rehypePrefixBaseToRootLinks } from './plugins/rehype-prefix-base-to-root-links.mjs';
+import starlightContextualMenu from 'starlight-contextual-menu';
+
+// Configs
+import {
+ createHeadConfig,
+ createRedirects,
+ sidebarConfig,
+ socialConfig,
+} from './src/config';
+
+// Deployment target: 'custom' (default, custom domain at invoke.ai) or 'ghpages'
+// (GitHub Pages project URL at invoke-ai.github.io/InvokeAI). Drive site/base from this
+// so the same source can be deployed to either target.
+const deployTarget = process.env.DEPLOY_TARGET ?? 'custom';
+const isGhPages = deployTarget === 'ghpages';
+const enableAnalytics = process.env.ENABLE_ANALYTICS === 'true';
+const base = isGhPages ? '/InvokeAI' : '';
+const site = isGhPages ? 'https://invoke-ai.github.io' : 'https://invoke.ai';
+
+const redirects = createRedirects(base);
+const head = createHeadConfig({ base, enableAnalytics, isGhPages, site });
+
+// https://astro.build/config
+export default defineConfig({
+ site,
+ base: base || undefined,
+ markdown: {
+ rehypePlugins: [[rehypePrefixBaseToRootLinks, { base }]],
+ },
+ integrations: [
+ starlight({
+ // Content
+ title: {
+ en: 'InvokeAI Documentation',
+ },
+ logo: {
+ src: './src/assets/invoke-icon-wide.svg',
+ alt: 'InvokeAI Logo',
+ replacesTitle: true,
+ },
+ favicon: 'favicon.svg',
+ editLink: {
+ baseUrl: 'https://github.com/invoke-ai/InvokeAI/edit/main/docs',
+ },
+ head,
+ defaultLocale: 'root',
+ locales: {
+ root: {
+ label: 'English',
+ lang: 'en',
+ },
+ },
+ social: socialConfig,
+ tableOfContents: {
+ maxHeadingLevel: 4,
+ },
+ customCss: [
+ '@fontsource-variable/inter',
+ '@fontsource-variable/roboto-mono',
+ './src/styles/custom.css',
+ ],
+ sidebar: sidebarConfig,
+ components: {
+ ThemeProvider: './src/lib/components/ForceDarkTheme.astro',
+ ThemeSelect: './src/lib/components/EmptyComponent.astro',
+ Footer: './src/lib/components/Footer.astro',
+ PageFrame: './src/layouts/PageFrameExtended.astro',
+ },
+ plugins: [
+ starlightLinksValidator({
+ errorOnRelativeLinks: false,
+ errorOnLocalLinks: false,
+ }),
+ starlightLlmsText(),
+ starlightChangelogs(),
+ starlightContextualMenu({
+ actions: ['copy', 'view', 'chatgpt', 'claude'],
+ }),
+ ],
+ }),
+ ],
+ redirects,
+});
diff --git a/ldm/data/__init__.py b/docs/invoke-config.json
similarity index 100%
rename from ldm/data/__init__.py
rename to docs/invoke-config.json
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 00000000000..29d0786e8ea
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "docs",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "generate-docs-data": "uv run python ../scripts/generate_docs_json.py",
+ "check-docs-data": "pnpm run generate-docs-data && git diff --exit-code -- src/generated",
+ "check-deploy-output": "node ./scripts/verify-deploy-output.mjs",
+ "check-redirects": "node ./scripts/validate-redirect-targets.mjs",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/starlight": "^0.39.2",
+ "@fontsource-variable/inter": "^5.2.8",
+ "@fontsource-variable/roboto-mono": "^5.2.9",
+ "astro": "^6.3.7",
+ "mermaid": "^11.15.0",
+ "rehype-external-links": "^3.0.0",
+ "sharp": "^0.34.5",
+ "starlight-changelogs": "^0.5.0",
+ "starlight-contextual-menu": "^0.1.5",
+ "starlight-links-validator": "^0.24.0",
+ "starlight-llms-txt": "^0.10.0"
+ },
+ "devDependencies": {
+ "node-addon-api": "^8.8.0",
+ "node-gyp": "^12.3.0"
+ },
+ "packageManager": "pnpm@10.12.4"
+}
diff --git a/docs/plugins/rehype-prefix-base-to-root-links.mjs b/docs/plugins/rehype-prefix-base-to-root-links.mjs
new file mode 100644
index 00000000000..fa424ab5c54
--- /dev/null
+++ b/docs/plugins/rehype-prefix-base-to-root-links.mjs
@@ -0,0 +1,53 @@
+export function rehypePrefixBaseToRootLinks(options = {}) {
+ const base = normalizeBase(options.base);
+
+ return (tree) => {
+ if (!base) {
+ return;
+ }
+
+ walk(tree, (node) => {
+ if (node.tagName !== 'a') {
+ return;
+ }
+
+ const href = node.properties?.href;
+
+ if (typeof href !== 'string') {
+ return;
+ }
+
+ if (!href.startsWith('/') || href.startsWith('//') || href.startsWith(`${base}/`)) {
+ return;
+ }
+
+ node.properties.href = `${base}${href}`;
+ });
+ };
+}
+
+function walk(node, visitor) {
+ if (!node || typeof node !== 'object') {
+ return;
+ }
+
+ if (node.type === 'element') {
+ visitor(node);
+ }
+
+ if (!Array.isArray(node.children)) {
+ return;
+ }
+
+ for (const child of node.children) {
+ walk(child, visitor);
+ }
+}
+
+function normalizeBase(base) {
+ if (!base || base === '/') {
+ return '';
+ }
+
+ return base.endsWith('/') ? base.slice(0, -1) : base;
+}
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
new file mode 100644
index 00000000000..a2daac1ce72
--- /dev/null
+++ b/docs/pnpm-lock.yaml
@@ -0,0 +1,5443 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@astrojs/starlight':
+ specifier: ^0.39.2
+ version: 0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@fontsource-variable/inter':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource-variable/roboto-mono':
+ specifier: ^5.2.9
+ version: 5.2.9
+ astro:
+ specifier: ^6.3.7
+ version: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ mermaid:
+ specifier: ^11.15.0
+ version: 11.15.0
+ rehype-external-links:
+ specifier: ^3.0.0
+ version: 3.0.0
+ sharp:
+ specifier: ^0.34.5
+ version: 0.34.5
+ starlight-changelogs:
+ specifier: ^0.5.0
+ version: 0.5.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ starlight-contextual-menu:
+ specifier: ^0.1.5
+ version: 0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))(starlight-markdown@0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))
+ starlight-links-validator:
+ specifier: ^0.24.0
+ version: 0.24.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ starlight-llms-txt:
+ specifier: ^0.10.0
+ version: 0.10.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ devDependencies:
+ node-addon-api:
+ specifier: ^8.8.0
+ version: 8.8.0
+ node-gyp:
+ specifier: ^12.3.0
+ version: 12.3.0
+
+packages:
+
+ '@antfu/install-pkg@1.1.0':
+ resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
+
+ '@ascorbic/loader-utils@1.0.2':
+ resolution: {integrity: sha512-pg43g83gojVtEsAkXfjWuzJhuXneJp4wM/leBftGkCPV3yxKgB92EWA+nWu735BgbBMph3P7DrVqVc3ikt+dJA==}
+ peerDependencies:
+ astro: ^4.14.0 || ^5.0.0-beta.0
+
+ '@astrojs/compiler@4.0.0':
+ resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==}
+
+ '@astrojs/internal-helpers@0.9.0':
+ resolution: {integrity: sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==}
+
+ '@astrojs/internal-helpers@0.9.1':
+ resolution: {integrity: sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ==}
+
+ '@astrojs/markdown-remark@7.1.1':
+ resolution: {integrity: sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==}
+
+ '@astrojs/markdown-remark@7.1.2':
+ resolution: {integrity: sha512-caXZ4Dc2St2dW8luEg22GlP0gupLdztCTQE4EzZOxW1pqWXz9mbeJEuHUkgDYcKWW8tjIHkydYDhWLVoxJ327Q==}
+
+ '@astrojs/mdx@5.0.4':
+ resolution: {integrity: sha512-tSbuuYueNODiFAFaME7pjHY5lOLoxBYJi1cKd6scw9+a4ZO7C7UGdafEoVAQvOV2eO8a6RaHSAJYGVPL1w8BPA==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ astro: ^6.0.0
+
+ '@astrojs/mdx@5.0.6':
+ resolution: {integrity: sha512-4dKe0ZMmqujofPNDHahzClkwinn9f8jHPcaXcgdGvPAlboD2mjzkUCofli2cBnxYAkdfhC6d50gBJ8i/cH8gHw==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ astro: ^6.0.0
+
+ '@astrojs/prism@4.0.1':
+ resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/prism@4.0.2':
+ resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/sitemap@3.7.2':
+ resolution: {integrity: sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==}
+
+ '@astrojs/starlight@0.39.2':
+ resolution: {integrity: sha512-vlw+bwnjtf5buCTUtLU7JfV6D3knslxqnspr6LKs6hfRuFZiyr5hT44F7GyDqR9FKANUqFxnIzWM81F1k/kOUA==}
+ peerDependencies:
+ astro: ^6.0.0
+
+ '@astrojs/telemetry@3.3.2':
+ resolution: {integrity: sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==}
+ engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.28.5':
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.3':
+ resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.0':
+ resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
+ engines: {node: '>=6.9.0'}
+
+ '@braintree/sanitize-url@7.1.2':
+ resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
+
+ '@capsizecss/unpack@4.0.0':
+ resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==}
+ engines: {node: '>=18'}
+
+ '@chevrotain/types@11.1.2':
+ resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
+
+ '@clack/core@1.3.1':
+ resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==}
+ engines: {node: '>= 20.12.0'}
+
+ '@clack/prompts@1.4.0':
+ resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==}
+ engines: {node: '>= 20.12.0'}
+
+ '@ctrl/tinycolor@4.2.0':
+ resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==}
+ engines: {node: '>=14'}
+
+ '@emnapi/runtime@1.9.2':
+ resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
+
+ '@esbuild/aix-ppc64@0.27.7':
+ resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.27.7':
+ resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.27.7':
+ resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.27.7':
+ resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.27.7':
+ resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.27.7':
+ resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.27.7':
+ resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.27.7':
+ resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.27.7':
+ resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.27.7':
+ resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.27.7':
+ resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.27.7':
+ resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.27.7':
+ resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.27.7':
+ resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.27.7':
+ resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.27.7':
+ resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.27.7':
+ resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.27.7':
+ resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.27.7':
+ resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.27.7':
+ resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.27.7':
+ resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.27.7':
+ resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.27.7':
+ resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.27.7':
+ resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.27.7':
+ resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.27.7':
+ resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@expressive-code/core@0.42.0':
+ resolution: {integrity: sha512-MN11+9nfmaC7sYu2BZJXAXqwkBRt8t1xTSqP+Ti1NfTEskgl6xUnzDxoaiQkg0BMzpglA0pys4dpDKquP/cyIw==}
+
+ '@expressive-code/plugin-frames@0.42.0':
+ resolution: {integrity: sha512-XtkPm+941Uta7Y+81Acv+OA/20F1NJmJhCX6UYGKpqEIGqplNh3PTOhcURp6tcruhlzJcWcvpWy6Oigz3SrjqA==}
+
+ '@expressive-code/plugin-shiki@0.42.0':
+ resolution: {integrity: sha512-PMKey/kLmewttAHQezL+Y5Fx3vVssfDi3+FJOYQQS2mXP3tQspFELtKKAfsXfmSXdToZYgwoO69HJndqfE+09g==}
+
+ '@expressive-code/plugin-text-markers@0.42.0':
+ resolution: {integrity: sha512-l59lUx8fq1v5g6SpmbDjiU0+7IdfbiWnAyRmtTVSpfhyq+nZMN4UcmYyu2b9Mynhzt7Gr+O+cXyEPDNb2AVWVQ==}
+
+ '@fontsource-variable/inter@5.2.8':
+ resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
+
+ '@fontsource-variable/roboto-mono@5.2.9':
+ resolution: {integrity: sha512-OzFO2AXlSGcXl/NcXS3CGjImb6rczCByPJ1C+Dzp9kkYOrUPyrGTuAtqPcmA/d+nZGX5oyOWKXLk5BrwVLYqkw==}
+
+ '@iconify/types@2.0.0':
+ resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
+
+ '@iconify/utils@3.1.0':
+ resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.34.5':
+ resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linux-arm64@0.34.5':
+ resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linux-arm@0.34.5':
+ resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@img/sharp-linux-s390x@0.34.5':
+ resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@img/sharp-linux-x64@0.34.5':
+ resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@img/sharp-wasm32@0.34.5':
+ resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.34.5':
+ resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.34.5':
+ resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.34.5':
+ resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@isaacs/fs-minipass@4.0.1':
+ resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+ engines: {node: '>=18.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@mdx-js/mdx@3.1.1':
+ resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==}
+
+ '@mermaid-js/parser@1.1.1':
+ resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
+
+ '@oslojs/encoding@1.1.0':
+ resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pagefind/darwin-x64@1.5.2':
+ resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pagefind/default-ui@1.5.2':
+ resolution: {integrity: sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pagefind/linux-arm64@1.5.2':
+ resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@pagefind/linux-x64@1.5.2':
+ resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@pagefind/windows-arm64@1.5.2':
+ resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pagefind/windows-x64@1.5.2':
+ resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/pluginutils@5.3.0':
+ resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ '@rollup/rollup-android-arm-eabi@4.60.4':
+ resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.60.4':
+ resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.60.4':
+ resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.60.4':
+ resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.60.4':
+ resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.60.4':
+ resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.4':
+ resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.60.4':
+ resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.60.4':
+ resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.60.4':
+ resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.60.4':
+ resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.60.4':
+ resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.60.4':
+ resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.60.4':
+ resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.60.4':
+ resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.60.4':
+ resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.60.4':
+ resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.60.4':
+ resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.60.4':
+ resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.60.4':
+ resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.60.4':
+ resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.60.4':
+ resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.60.4':
+ resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.60.4':
+ resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.60.4':
+ resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@shikijs/core@4.0.2':
+ resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/core@4.1.0':
+ resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.0.2':
+ resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.1.0':
+ resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.1.0':
+ resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.0.2':
+ resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.1.0':
+ resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.0.2':
+ resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.1.0':
+ resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.0.2':
+ resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.1.0':
+ resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.0.2':
+ resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.1.0':
+ resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@types/braces@3.0.5':
+ resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==}
+
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-axis@3.0.6':
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
+
+ '@types/d3-brush@3.0.6':
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
+
+ '@types/d3-chord@3.0.6':
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-contour@3.0.6':
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
+
+ '@types/d3-delaunay@6.0.4':
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
+
+ '@types/d3-dispatch@3.0.7':
+ resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-dsv@3.0.7':
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-fetch@3.0.7':
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
+
+ '@types/d3-force@3.0.10':
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
+
+ '@types/d3-format@3.0.4':
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
+
+ '@types/d3-geo@3.1.0':
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
+
+ '@types/d3-hierarchy@3.1.7':
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-polygon@3.0.2':
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
+
+ '@types/d3-quadtree@3.0.6':
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
+
+ '@types/d3-random@3.0.3':
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+
+ '@types/d3-scale-chromatic@3.1.0':
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-shape@3.1.8':
+ resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
+
+ '@types/d3-time-format@4.0.3':
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
+ '@types/d3@7.4.3':
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/geojson@7946.0.16':
+ resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
+
+ '@types/hast@3.0.4':
+ resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
+
+ '@types/js-yaml@4.0.9':
+ resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/mdx@2.0.13':
+ resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==}
+
+ '@types/micromatch@4.0.10':
+ resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
+ '@types/nlcst@2.0.3':
+ resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+
+ '@types/node@24.12.3':
+ resolution: {integrity: sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==}
+
+ '@types/picomatch@4.0.3':
+ resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==}
+
+ '@types/sax@1.2.7':
+ resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
+
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@ungap/structured-clone@1.3.0':
+ resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+
+ '@ungap/structured-clone@1.3.1':
+ resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
+
+ '@upsetjs/venn.js@2.0.0':
+ resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
+
+ abbrev@4.0.0:
+ resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.16.0:
+ resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ansi-escapes@7.3.0:
+ resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
+ engines: {node: '>=18'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ array-iterate@2.0.1:
+ resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
+
+ astring@1.9.0:
+ resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==}
+ hasBin: true
+
+ astro-expressive-code@0.42.0:
+ resolution: {integrity: sha512-aiTePi2Cn0mJPYWZSzP1GcxCinX9mNtJyCCshVVPSg1yRwM7ADvFJOx0FnS440M9t65hp8JH//dc2qr22Bm4ag==}
+ peerDependencies:
+ astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta
+
+ astro@6.3.7:
+ resolution: {integrity: sha512-zIeDRrI0qNgN1lcCjNqt6/IVCVej7VwSa326cO8uP9BOk1cg4QuffhLnOn2gCgWQr32/wxpSRFfXiLKHglu1Tw==}
+ engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
+ hasBin: true
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
+ bcp-47-match@2.0.3:
+ resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
+
+ bcp-47@2.1.0:
+ resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
+ chownr@3.0.0:
+ resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+ engines: {node: '>=18'}
+
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ collapse-white-space@2.1.0:
+ resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
+ common-ancestor-path@2.0.0:
+ resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==}
+ engines: {node: '>= 18'}
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ cookie-es@1.2.3:
+ resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
+
+ cookie@1.1.1:
+ resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
+ engines: {node: '>=18'}
+
+ cose-base@1.0.3:
+ resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
+
+ cose-base@2.2.0:
+ resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
+
+ crossws@0.3.5:
+ resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+
+ css-select@5.2.2:
+ resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
+
+ css-selector-parser@3.3.0:
+ resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==}
+
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-what@6.2.2:
+ resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
+ engines: {node: '>= 6'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ cytoscape-cose-bilkent@4.1.0:
+ resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape-fcose@2.2.0:
+ resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==}
+ peerDependencies:
+ cytoscape: ^3.2.0
+
+ cytoscape@3.33.2:
+ resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==}
+ engines: {node: '>=0.10'}
+
+ d3-array@2.12.1:
+ resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==}
+
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-axis@3.0.0:
+ resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+ engines: {node: '>=12'}
+
+ d3-brush@3.0.0:
+ resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+ engines: {node: '>=12'}
+
+ d3-chord@3.0.1:
+ resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-contour@4.0.2:
+ resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+ engines: {node: '>=12'}
+
+ d3-delaunay@6.0.4:
+ resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-dsv@3.0.1:
+ resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-fetch@3.0.1:
+ resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+ engines: {node: '>=12'}
+
+ d3-force@3.0.0:
+ resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.2:
+ resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==}
+ engines: {node: '>=12'}
+
+ d3-geo@3.1.1:
+ resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+ engines: {node: '>=12'}
+
+ d3-hierarchy@3.1.2:
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@1.0.9:
+ resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-polygon@3.0.1:
+ resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+ engines: {node: '>=12'}
+
+ d3-quadtree@3.0.1:
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+ engines: {node: '>=12'}
+
+ d3-random@3.0.1:
+ resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+ engines: {node: '>=12'}
+
+ d3-sankey@0.12.3:
+ resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==}
+
+ d3-scale-chromatic@3.1.0:
+ resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@1.3.7:
+ resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
+ d3@7.9.0:
+ resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+ engines: {node: '>=12'}
+
+ dagre-d3-es@7.0.14:
+ resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
+
+ dayjs@1.11.20:
+ resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
+ delaunator@5.1.0:
+ resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ devalue@5.8.1:
+ resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ diff@8.0.4:
+ resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
+ engines: {node: '>=0.3.1'}
+
+ direction@2.0.1:
+ resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
+ hasBin: true
+
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ dompurify@3.3.3:
+ resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ dset@3.1.4:
+ resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
+ engines: {node: '>=4'}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ environment@1.1.0:
+ resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
+ engines: {node: '>=18'}
+
+ es-module-lexer@2.1.0:
+ resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
+
+ es-toolkit@1.46.1:
+ resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==}
+
+ esast-util-from-estree@2.0.0:
+ resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
+
+ esast-util-from-js@2.0.1:
+ resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
+
+ esbuild@0.27.7:
+ resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escape-string-regexp@5.0.0:
+ resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+ engines: {node: '>=12'}
+
+ estree-util-attach-comments@3.0.0:
+ resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
+
+ estree-util-build-jsx@3.0.1:
+ resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==}
+
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
+ estree-util-scope@1.0.0:
+ resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==}
+
+ estree-util-to-js@2.0.0:
+ resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==}
+
+ estree-util-visit@2.0.0:
+ resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+ exponential-backoff@3.1.3:
+ resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==}
+
+ expressive-code@0.42.0:
+ resolution: {integrity: sha512-V5DtJLEKuj4wf9O6IRtPtRObkMVy2ggR+S0MdjrTw6m58krZnDioyhW1si3Y04c5YPeooP4nd85Yq9NwEVHS4g==}
+
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
+ fast-string-truncated-width@3.0.3:
+ resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+ fast-string-width@3.0.2:
+ resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
+
+ fast-wrap-ansi@0.2.2:
+ resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ flattie@1.1.1:
+ resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
+ engines: {node: '>=8'}
+
+ fontace@0.4.1:
+ resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==}
+
+ fontkitten@1.0.3:
+ resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
+ engines: {node: '>=20'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-tsconfig@5.0.0-beta.4:
+ resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==}
+ engines: {node: '>=20.20.0'}
+
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ h3@1.15.11:
+ resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
+
+ hachure-fill@0.5.2:
+ resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
+
+ has-flag@5.0.1:
+ resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==}
+ engines: {node: '>=12'}
+
+ hast-util-embedded@3.0.0:
+ resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==}
+
+ hast-util-format@1.1.0:
+ resolution: {integrity: sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==}
+
+ hast-util-from-html@2.0.3:
+ resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==}
+
+ hast-util-from-parse5@8.0.3:
+ resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
+
+ hast-util-has-property@3.0.0:
+ resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
+
+ hast-util-is-body-ok-link@3.0.1:
+ resolution: {integrity: sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==}
+
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
+ hast-util-minify-whitespace@1.0.1:
+ resolution: {integrity: sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hast-util-phrasing@3.0.1:
+ resolution: {integrity: sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==}
+
+ hast-util-raw@9.1.0:
+ resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
+
+ hast-util-select@6.0.4:
+ resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==}
+
+ hast-util-to-estree@3.1.3:
+ resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
+ hast-util-to-mdast@10.1.2:
+ resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==}
+
+ hast-util-to-parse5@8.0.1:
+ resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
+
+ hast-util-to-string@3.0.1:
+ resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
+
+ hast-util-to-text@4.0.2:
+ resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ html-escaper@3.0.3:
+ resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ html-whitespace-sensitive-tag-names@3.0.1:
+ resolution: {integrity: sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==}
+
+ http-cache-semantics@4.2.0:
+ resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
+
+ i18next@26.1.0:
+ resolution: {integrity: sha512-dIU6td04DvQuIqVst5S9g0GviTmhZ0DYD4b9ociVGJmuCa5vZ2de/t+Enf4olvj87mF8Y2lwjNQBwC9QZsvzKQ==}
+ peerDependencies:
+ typescript: ^5 || ^6
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ inline-style-parser@0.2.7:
+ resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
+
+ internmap@1.0.1:
+ resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
+
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
+ iron-webcrypto@1.2.1:
+ resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
+
+ is-absolute-url@4.0.1:
+ resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ is-absolute-url@5.0.0:
+ resolution: {integrity: sha512-sdJyNpBnQHuVnBunfzjAecOhZr2+A30ywfFvu3EnxtKLUWfwGgyWUmqHbGZiU6vTfHpCPm5GvLe4BAvlU9n8VQ==}
+ engines: {node: '>=20'}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-docker@3.0.0:
+ resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hasBin: true
+
+ is-docker@4.0.0:
+ resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-inside-container@1.0.0:
+ resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
+ engines: {node: '>=14.16'}
+ hasBin: true
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ is-wsl@3.1.1:
+ resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
+ engines: {node: '>=16'}
+
+ isexe@4.0.0:
+ resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==}
+ engines: {node: '>=20'}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ js-yaml@4.1.1:
+ resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+ hasBin: true
+
+ jsonc-parser@3.3.1:
+ resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
+
+ katex@0.16.45:
+ resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==}
+ hasBin: true
+
+ khroma@2.1.0:
+ resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==}
+
+ klona@2.0.6:
+ resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
+ engines: {node: '>= 8'}
+
+ layout-base@1.0.2:
+ resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==}
+
+ layout-base@2.0.1:
+ resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
+
+ lodash-es@4.18.1:
+ resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
+
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
+ lru-cache@11.5.0:
+ resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==}
+ engines: {node: 20 || >=22}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ magicast@0.5.3:
+ resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
+
+ markdown-extensions@2.0.0:
+ resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
+ engines: {node: '>=16'}
+
+ markdown-table@3.0.4:
+ resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
+
+ marked@16.4.2:
+ resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==}
+ engines: {node: '>= 20'}
+ hasBin: true
+
+ mdast-util-definitions@6.0.0:
+ resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==}
+
+ mdast-util-directive@3.1.0:
+ resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==}
+
+ mdast-util-find-and-replace@3.0.2:
+ resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
+
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
+
+ mdast-util-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
+
+ mdast-util-gfm-table@2.0.0:
+ resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
+
+ mdast-util-gfm@3.1.0:
+ resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
+
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdx@3.0.0:
+ resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ mermaid@11.15.0:
+ resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==}
+
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-directive@4.0.0:
+ resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==}
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
+
+ micromark-extension-gfm-footnote@2.1.0:
+ resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
+
+ micromark-extension-gfm-table@2.1.1:
+ resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
+
+ micromark-extension-gfm@3.0.0:
+ resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
+
+ micromark-extension-mdx-expression@3.0.1:
+ resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
+
+ micromark-extension-mdx-jsx@3.0.2:
+ resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==}
+
+ micromark-extension-mdx-md@2.0.0:
+ resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==}
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==}
+
+ micromark-extension-mdxjs@3.0.0:
+ resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-mdx-expression@2.0.3:
+ resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-events-to-acorn@2.0.3:
+ resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ minipass@7.1.3:
+ resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minizlib@3.1.0:
+ resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
+ engines: {node: '>= 18'}
+
+ mlly@1.8.2:
+ resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.12:
+ resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ neotraverse@0.6.18:
+ resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
+ engines: {node: '>= 10'}
+
+ nlcst-to-string@4.0.0:
+ resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
+
+ node-addon-api@8.8.0:
+ resolution: {integrity: sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==}
+ engines: {node: ^18 || ^20 || >= 21}
+
+ node-fetch-native@1.6.7:
+ resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+
+ node-gyp@12.3.0:
+ resolution: {integrity: sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+ hasBin: true
+
+ node-mock-http@1.0.4:
+ resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==}
+
+ nopt@9.0.0:
+ resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+ hasBin: true
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
+ ofetch@1.5.1:
+ resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
+
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ oniguruma-parser@0.12.1:
+ resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
+
+ oniguruma-parser@0.12.2:
+ resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
+
+ oniguruma-to-es@4.3.5:
+ resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==}
+
+ oniguruma-to-es@4.3.6:
+ resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
+
+ p-limit@7.3.0:
+ resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==}
+ engines: {node: '>=20'}
+
+ p-queue@9.3.0:
+ resolution: {integrity: sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==}
+ engines: {node: '>=20'}
+
+ p-timeout@7.0.1:
+ resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
+ engines: {node: '>=20'}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ pagefind@1.5.2:
+ resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==}
+ hasBin: true
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ parse-latin@7.0.0:
+ resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==}
+
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
+ path-data-parser@0.1.0:
+ resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ piccolore@0.1.3:
+ resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ pkg-types@1.3.1:
+ resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
+
+ points-on-curve@0.2.0:
+ resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
+
+ points-on-path@0.2.1:
+ resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==}
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.2:
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.14:
+ resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ postcss@8.5.15:
+ resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
+ proc-log@6.1.0:
+ resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+
+ property-information@7.1.0:
+ resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
+
+ radix3@1.1.2:
+ resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
+
+ readdirp@5.0.0:
+ resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
+ engines: {node: '>= 20.19.0'}
+
+ recma-build-jsx@1.0.0:
+ resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
+
+ recma-jsx@1.0.1:
+ resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ recma-parse@1.0.0:
+ resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==}
+
+ recma-stringify@1.0.0:
+ resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
+
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
+ rehype-expressive-code@0.42.0:
+ resolution: {integrity: sha512-8rp/1YMEVVSYbtz+bFBx+uSx3vA4i4T8RwRm5Q/IWbucQnnQqQ0hDqtmKOr8tv+59Cik6cu5aH3WPo0I7csuTA==}
+
+ rehype-external-links@3.0.0:
+ resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==}
+
+ rehype-format@5.0.1:
+ resolution: {integrity: sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==}
+
+ rehype-minify-whitespace@6.0.2:
+ resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==}
+
+ rehype-parse@9.0.1:
+ resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==}
+
+ rehype-raw@7.0.0:
+ resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
+
+ rehype-recma@1.0.0:
+ resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
+
+ rehype-remark@10.0.1:
+ resolution: {integrity: sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==}
+
+ rehype-stringify@10.0.1:
+ resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
+
+ rehype@13.0.2:
+ resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==}
+
+ remark-directive@4.0.0:
+ resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
+
+ remark-gfm@4.0.1:
+ resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
+
+ remark-mdx@3.1.1:
+ resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
+
+ remark-parse@11.0.0:
+ resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==}
+
+ remark-rehype@11.1.2:
+ resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
+
+ remark-smartypants@3.0.2:
+ resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==}
+ engines: {node: '>=16.0.0'}
+
+ remark-stringify@11.0.0:
+ resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ retext-latin@4.0.0:
+ resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==}
+
+ retext-smartypants@6.2.0:
+ resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==}
+
+ retext-stringify@4.0.0:
+ resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==}
+
+ retext@9.0.0:
+ resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==}
+
+ robust-predicates@3.0.3:
+ resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
+
+ rollup@4.60.4:
+ resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ roughjs@4.6.6:
+ resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
+
+ rw@1.3.3:
+ resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sax@1.6.0:
+ resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
+ engines: {node: '>=11.0.0'}
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ semver@7.8.1:
+ resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ sharp@0.34.5:
+ resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
+ shiki@4.0.2:
+ resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==}
+ engines: {node: '>=20'}
+
+ shiki@4.1.0:
+ resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==}
+ engines: {node: '>=20'}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ sitemap@9.0.1:
+ resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==}
+ engines: {node: '>=20.19.5', npm: '>=10.8.2'}
+ hasBin: true
+
+ smol-toml@1.6.1:
+ resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
+ engines: {node: '>= 18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ starlight-changelogs@0.5.0:
+ resolution: {integrity: sha512-bqBAHle6N4jApkLz8qlN+k8nmaJUXvYunrJeYWTZats15SeEmsT+1N7KdshODCky2AzLfIBy4Qa2v1YSQNrjxQ==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ '@astrojs/starlight': '>=0.38.0'
+
+ starlight-contextual-menu@0.1.5:
+ resolution: {integrity: sha512-MYQ6eFDIBBnKrEh3XqR7RZ6YDJ641ADmrSjj93d+cVJGPvrCHrd6VYiKeehhczsrn6GqjaCCFAn4xUd69gcfcQ==}
+ peerDependencies:
+ astro: ^5.0.0
+ starlight-markdown: ^0.1.5
+
+ starlight-links-validator@0.24.0:
+ resolution: {integrity: sha512-bsZf77oRJmY92KWOcu3vYK8Y12KJNvO3jQca1BgOBs+XskNfjPXrkgVtT7ls/FnLoomfsIV0wLdJfJs7kzGojA==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ '@astrojs/starlight': '>=0.38.0'
+ astro: '>=6.0.0'
+
+ starlight-llms-txt@0.10.0:
+ resolution: {integrity: sha512-LgkSjkvdACsGHkFq1ES00F0BU4lRepjJoaUmOgxBxNWx4txwpySVPtntKdAvDvlhinyN0ZBRpnAsN/sVQ1UEfA==}
+ peerDependencies:
+ '@astrojs/starlight': '>=0.38.0'
+ astro: ^6.0.0
+
+ starlight-markdown@0.1.5:
+ resolution: {integrity: sha512-23LXRaZp7pyE+r/HP6rxHfwic8HfvUBT4EImECA6encs/eTtrF0Z+7svANofdtfbiNt31D5q26i03B6FtcSmGg==}
+ peerDependencies:
+ astro: ^5.0.0
+
+ stream-replace-string@2.0.0:
+ resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ style-to-js@1.1.21:
+ resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
+
+ style-to-object@1.0.14:
+ resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
+
+ stylis@4.3.6:
+ resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
+
+ supports-color@10.2.2:
+ resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
+ engines: {node: '>=18'}
+
+ supports-hyperlinks@4.4.0:
+ resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==}
+ engines: {node: '>=20'}
+
+ svgo@4.0.1:
+ resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ tar@7.5.15:
+ resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
+ engines: {node: '>=18'}
+
+ terminal-link@5.0.0:
+ resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==}
+ engines: {node: '>=20'}
+
+ tiny-inflate@1.0.3:
+ resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+
+ tinyclip@0.1.12:
+ resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==}
+ engines: {node: ^16.14.0 || >= 17.3.0}
+
+ tinyexec@1.0.4:
+ resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
+ engines: {node: '>=18'}
+
+ tinyexec@1.2.2:
+ resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trim-trailing-lines@2.1.0:
+ resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
+ ts-dedent@2.2.0:
+ resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
+ engines: {node: '>=6.10'}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ ufo@1.6.3:
+ resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
+
+ ufo@1.6.4:
+ resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
+
+ ultrahtml@1.6.0:
+ resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==}
+
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ undici@6.25.0:
+ resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
+ engines: {node: '>=18.17'}
+
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unifont@0.7.4:
+ resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==}
+
+ unist-util-find-after@5.0.0:
+ resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-modify-children@4.0.0:
+ resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==}
+
+ unist-util-position-from-estree@2.0.0:
+ resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-remove-position@5.0.0:
+ resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==}
+
+ unist-util-remove@4.0.0:
+ resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-children@3.0.0:
+ resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ unstorage@1.17.5:
+ resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==}
+ peerDependencies:
+ '@azure/app-configuration': ^1.8.0
+ '@azure/cosmos': ^4.2.0
+ '@azure/data-tables': ^13.3.0
+ '@azure/identity': ^4.6.0
+ '@azure/keyvault-secrets': ^4.9.0
+ '@azure/storage-blob': ^12.26.0
+ '@capacitor/preferences': ^6 || ^7 || ^8
+ '@deno/kv': '>=0.9.0'
+ '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0
+ '@planetscale/database': ^1.19.0
+ '@upstash/redis': ^1.34.3
+ '@vercel/blob': '>=0.27.1'
+ '@vercel/functions': ^2.2.12 || ^3.0.0
+ '@vercel/kv': ^1 || ^2 || ^3
+ aws4fetch: ^1.0.20
+ db0: '>=0.2.1'
+ idb-keyval: ^6.2.1
+ ioredis: ^5.4.2
+ uploadthing: ^7.4.4
+ peerDependenciesMeta:
+ '@azure/app-configuration':
+ optional: true
+ '@azure/cosmos':
+ optional: true
+ '@azure/data-tables':
+ optional: true
+ '@azure/identity':
+ optional: true
+ '@azure/keyvault-secrets':
+ optional: true
+ '@azure/storage-blob':
+ optional: true
+ '@capacitor/preferences':
+ optional: true
+ '@deno/kv':
+ optional: true
+ '@netlify/blobs':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/blob':
+ optional: true
+ '@vercel/functions':
+ optional: true
+ '@vercel/kv':
+ optional: true
+ aws4fetch:
+ optional: true
+ db0:
+ optional: true
+ idb-keyval:
+ optional: true
+ ioredis:
+ optional: true
+ uploadthing:
+ optional: true
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ uuid@11.1.0:
+ resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
+ hasBin: true
+
+ vfile-location@5.0.3:
+ resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite@7.3.3:
+ resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ web-namespaces@2.0.1:
+ resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
+
+ which-pm-runs@1.1.0:
+ resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==}
+ engines: {node: '>=4'}
+
+ which@6.0.1:
+ resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==}
+ engines: {node: ^20.17.0 || >=22.9.0}
+ hasBin: true
+
+ xxhash-wasm@1.1.0:
+ resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
+
+ yallist@5.0.0:
+ resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+ engines: {node: '>=18'}
+
+ yaml@2.9.0:
+ resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
+ engines: {node: '>= 14.6'}
+ hasBin: true
+
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yocto-queue@1.2.2:
+ resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
+ engines: {node: '>=12.20'}
+
+ zod@4.4.3:
+ resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@antfu/install-pkg@1.1.0':
+ dependencies:
+ package-manager-detector: 1.6.0
+ tinyexec: 1.0.4
+
+ '@ascorbic/loader-utils@1.0.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))':
+ dependencies:
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+
+ '@astrojs/compiler@4.0.0': {}
+
+ '@astrojs/internal-helpers@0.9.0':
+ dependencies:
+ picomatch: 4.0.4
+
+ '@astrojs/internal-helpers@0.9.1':
+ dependencies:
+ picomatch: 4.0.4
+
+ '@astrojs/markdown-remark@7.1.1':
+ dependencies:
+ '@astrojs/internal-helpers': 0.9.0
+ '@astrojs/prism': 4.0.1
+ github-slugger: 2.0.0
+ hast-util-from-html: 2.0.3
+ hast-util-to-text: 4.0.2
+ js-yaml: 4.1.1
+ mdast-util-definitions: 6.0.0
+ rehype-raw: 7.0.0
+ rehype-stringify: 10.0.1
+ remark-gfm: 4.0.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ remark-smartypants: 3.0.2
+ retext-smartypants: 6.2.0
+ shiki: 4.0.2
+ smol-toml: 1.6.1
+ unified: 11.0.5
+ unist-util-remove-position: 5.0.0
+ unist-util-visit: 5.1.0
+ unist-util-visit-parents: 6.0.2
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@astrojs/markdown-remark@7.1.2':
+ dependencies:
+ '@astrojs/internal-helpers': 0.9.1
+ '@astrojs/prism': 4.0.2
+ github-slugger: 2.0.0
+ hast-util-from-html: 2.0.3
+ hast-util-to-text: 4.0.2
+ js-yaml: 4.1.1
+ mdast-util-definitions: 6.0.0
+ rehype-raw: 7.0.0
+ rehype-stringify: 10.0.1
+ remark-gfm: 4.0.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ remark-smartypants: 3.0.2
+ retext-smartypants: 6.2.0
+ shiki: 4.1.0
+ smol-toml: 1.6.1
+ unified: 11.0.5
+ unist-util-remove-position: 5.0.0
+ unist-util-visit: 5.1.0
+ unist-util-visit-parents: 6.0.2
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@astrojs/mdx@5.0.4(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))':
+ dependencies:
+ '@astrojs/markdown-remark': 7.1.1
+ '@mdx-js/mdx': 3.1.1
+ acorn: 8.16.0
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ es-module-lexer: 2.1.0
+ estree-util-visit: 2.0.0
+ hast-util-to-html: 9.0.5
+ piccolore: 0.1.3
+ rehype-raw: 7.0.0
+ remark-gfm: 4.0.1
+ remark-smartypants: 3.0.2
+ source-map: 0.7.6
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@astrojs/mdx@5.0.6(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))':
+ dependencies:
+ '@astrojs/markdown-remark': 7.1.2
+ '@mdx-js/mdx': 3.1.1
+ acorn: 8.16.0
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ es-module-lexer: 2.1.0
+ estree-util-visit: 2.0.0
+ hast-util-to-html: 9.0.5
+ piccolore: 0.1.3
+ rehype-raw: 7.0.0
+ remark-gfm: 4.0.1
+ remark-smartypants: 3.0.2
+ source-map: 0.7.6
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@astrojs/prism@4.0.1':
+ dependencies:
+ prismjs: 1.30.0
+
+ '@astrojs/prism@4.0.2':
+ dependencies:
+ prismjs: 1.30.0
+
+ '@astrojs/sitemap@3.7.2':
+ dependencies:
+ sitemap: 9.0.1
+ stream-replace-string: 2.0.0
+ zod: 4.4.3
+
+ '@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))':
+ dependencies:
+ '@astrojs/markdown-remark': 7.1.1
+ '@astrojs/mdx': 5.0.4(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@astrojs/sitemap': 3.7.2
+ '@pagefind/default-ui': 1.5.2
+ '@types/hast': 3.0.4
+ '@types/js-yaml': 4.0.9
+ '@types/mdast': 4.0.4
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ astro-expressive-code: 0.42.0(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ bcp-47: 2.1.0
+ hast-util-from-html: 2.0.3
+ hast-util-select: 6.0.4
+ hast-util-to-string: 3.0.1
+ hastscript: 9.0.1
+ i18next: 26.1.0
+ js-yaml: 4.1.1
+ klona: 2.0.6
+ magic-string: 0.30.21
+ mdast-util-directive: 3.1.0
+ mdast-util-to-markdown: 2.1.2
+ mdast-util-to-string: 4.0.0
+ pagefind: 1.5.2
+ rehype: 13.0.2
+ rehype-format: 5.0.1
+ remark-directive: 4.0.0
+ ultrahtml: 1.6.0
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@astrojs/telemetry@3.3.2':
+ dependencies:
+ ci-info: 4.4.0
+ dset: 3.1.4
+ is-docker: 4.0.0
+ is-wsl: 3.1.1
+ which-pm-runs: 1.1.0
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.28.5': {}
+
+ '@babel/parser@7.29.3':
+ dependencies:
+ '@babel/types': 7.29.0
+
+ '@babel/types@7.29.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
+
+ '@braintree/sanitize-url@7.1.2': {}
+
+ '@capsizecss/unpack@4.0.0':
+ dependencies:
+ fontkitten: 1.0.3
+
+ '@chevrotain/types@11.1.2': {}
+
+ '@clack/core@1.3.1':
+ dependencies:
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@clack/prompts@1.4.0':
+ dependencies:
+ '@clack/core': 1.3.1
+ fast-string-width: 3.0.2
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@ctrl/tinycolor@4.2.0': {}
+
+ '@emnapi/runtime@1.9.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.27.7':
+ optional: true
+
+ '@esbuild/android-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/android-arm@0.27.7':
+ optional: true
+
+ '@esbuild/android-x64@0.27.7':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/darwin-x64@0.27.7':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-arm@0.27.7':
+ optional: true
+
+ '@esbuild/linux-ia32@0.27.7':
+ optional: true
+
+ '@esbuild/linux-loong64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.27.7':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.27.7':
+ optional: true
+
+ '@esbuild/linux-s390x@0.27.7':
+ optional: true
+
+ '@esbuild/linux-x64@0.27.7':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.27.7':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/sunos-x64@0.27.7':
+ optional: true
+
+ '@esbuild/win32-arm64@0.27.7':
+ optional: true
+
+ '@esbuild/win32-ia32@0.27.7':
+ optional: true
+
+ '@esbuild/win32-x64@0.27.7':
+ optional: true
+
+ '@expressive-code/core@0.42.0':
+ dependencies:
+ '@ctrl/tinycolor': 4.2.0
+ hast-util-select: 6.0.4
+ hast-util-to-html: 9.0.5
+ hast-util-to-text: 4.0.2
+ hastscript: 9.0.1
+ postcss: 8.5.14
+ postcss-nested: 6.2.0(postcss@8.5.14)
+ unist-util-visit: 5.1.0
+ unist-util-visit-parents: 6.0.2
+
+ '@expressive-code/plugin-frames@0.42.0':
+ dependencies:
+ '@expressive-code/core': 0.42.0
+
+ '@expressive-code/plugin-shiki@0.42.0':
+ dependencies:
+ '@expressive-code/core': 0.42.0
+ shiki: 4.0.2
+
+ '@expressive-code/plugin-text-markers@0.42.0':
+ dependencies:
+ '@expressive-code/core': 0.42.0
+
+ '@fontsource-variable/inter@5.2.8': {}
+
+ '@fontsource-variable/roboto-mono@5.2.9': {}
+
+ '@iconify/types@2.0.0': {}
+
+ '@iconify/utils@3.1.0':
+ dependencies:
+ '@antfu/install-pkg': 1.1.0
+ '@iconify/types': 2.0.0
+ mlly: 1.8.2
+
+ '@img/colour@1.1.0': {}
+
+ '@img/sharp-darwin-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.2.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.34.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ optional: true
+
+ '@img/sharp-wasm32@0.34.5':
+ dependencies:
+ '@emnapi/runtime': 1.9.2
+ optional: true
+
+ '@img/sharp-win32-arm64@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.34.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.34.5':
+ optional: true
+
+ '@isaacs/fs-minipass@4.0.1':
+ dependencies:
+ minipass: 7.1.3
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@mdx-js/mdx@3.1.1':
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdx': 2.0.13
+ acorn: 8.16.0
+ collapse-white-space: 2.1.0
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-util-scope: 1.0.0
+ estree-walker: 3.0.3
+ hast-util-to-jsx-runtime: 2.3.6
+ markdown-extensions: 2.0.0
+ recma-build-jsx: 1.0.0
+ recma-jsx: 1.0.1(acorn@8.16.0)
+ recma-stringify: 1.0.0
+ rehype-recma: 1.0.0
+ remark-mdx: 3.1.1
+ remark-parse: 11.0.0
+ remark-rehype: 11.1.2
+ source-map: 0.7.6
+ unified: 11.0.5
+ unist-util-position-from-estree: 2.0.0
+ unist-util-stringify-position: 4.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@mermaid-js/parser@1.1.1':
+ dependencies:
+ '@chevrotain/types': 11.1.2
+
+ '@oslojs/encoding@1.1.0': {}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/darwin-x64@1.5.2':
+ optional: true
+
+ '@pagefind/default-ui@1.5.2': {}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-x64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-x64@1.5.2':
+ optional: true
+
+ '@rollup/pluginutils@5.3.0(rollup@4.60.4)':
+ dependencies:
+ '@types/estree': 1.0.9
+ estree-walker: 2.0.2
+ picomatch: 4.0.4
+ optionalDependencies:
+ rollup: 4.60.4
+
+ '@rollup/rollup-android-arm-eabi@4.60.4':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.60.4':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.60.4':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.60.4':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.60.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.60.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.60.4':
+ optional: true
+
+ '@shikijs/core@4.0.2':
+ dependencies:
+ '@shikijs/primitive': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/core@4.1.0':
+ dependencies:
+ '@shikijs/primitive': 4.1.0
+ '@shikijs/types': 4.1.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.5
+
+ '@shikijs/engine-javascript@4.1.0':
+ dependencies:
+ '@shikijs/types': 4.1.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.6
+
+ '@shikijs/engine-oniguruma@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/engine-oniguruma@4.1.0':
+ dependencies:
+ '@shikijs/types': 4.1.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/langs@4.1.0':
+ dependencies:
+ '@shikijs/types': 4.1.0
+
+ '@shikijs/primitive@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/primitive@4.1.0':
+ dependencies:
+ '@shikijs/types': 4.1.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/themes@4.0.2':
+ dependencies:
+ '@shikijs/types': 4.0.2
+
+ '@shikijs/themes@4.1.0':
+ dependencies:
+ '@shikijs/types': 4.1.0
+
+ '@shikijs/types@4.0.2':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/types@4.1.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@types/braces@3.0.5': {}
+
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-axis@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-brush@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-chord@3.0.6': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-contour@3.0.6':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-delaunay@6.0.4': {}
+
+ '@types/d3-dispatch@3.0.7': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-dsv@3.0.7': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-fetch@3.0.7':
+ dependencies:
+ '@types/d3-dsv': 3.0.7
+
+ '@types/d3-force@3.0.10': {}
+
+ '@types/d3-format@3.0.4': {}
+
+ '@types/d3-geo@3.1.0':
+ dependencies:
+ '@types/geojson': 7946.0.16
+
+ '@types/d3-hierarchy@3.1.7': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-polygon@3.0.2': {}
+
+ '@types/d3-quadtree@3.0.6': {}
+
+ '@types/d3-random@3.0.3': {}
+
+ '@types/d3-scale-chromatic@3.1.0': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-shape@3.1.8':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time-format@4.0.3': {}
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3@7.4.3':
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-axis': 3.0.6
+ '@types/d3-brush': 3.0.6
+ '@types/d3-chord': 3.0.6
+ '@types/d3-color': 3.1.3
+ '@types/d3-contour': 3.0.6
+ '@types/d3-delaunay': 6.0.4
+ '@types/d3-dispatch': 3.0.7
+ '@types/d3-drag': 3.0.7
+ '@types/d3-dsv': 3.0.7
+ '@types/d3-ease': 3.0.2
+ '@types/d3-fetch': 3.0.7
+ '@types/d3-force': 3.0.10
+ '@types/d3-format': 3.0.4
+ '@types/d3-geo': 3.1.0
+ '@types/d3-hierarchy': 3.1.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-path': 3.1.1
+ '@types/d3-polygon': 3.0.2
+ '@types/d3-quadtree': 3.0.6
+ '@types/d3-random': 3.0.3
+ '@types/d3-scale': 4.0.9
+ '@types/d3-scale-chromatic': 3.1.0
+ '@types/d3-selection': 3.0.11
+ '@types/d3-shape': 3.1.8
+ '@types/d3-time': 3.0.4
+ '@types/d3-time-format': 4.0.3
+ '@types/d3-timer': 3.0.2
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.8
+
+ '@types/estree@1.0.8': {}
+
+ '@types/estree@1.0.9': {}
+
+ '@types/geojson@7946.0.16': {}
+
+ '@types/hast@3.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/js-yaml@4.0.9': {}
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/mdx@2.0.13': {}
+
+ '@types/micromatch@4.0.10':
+ dependencies:
+ '@types/braces': 3.0.5
+
+ '@types/ms@2.1.0': {}
+
+ '@types/nlcst@2.0.3':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/node@24.12.3':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/picomatch@4.0.3': {}
+
+ '@types/sax@1.2.7':
+ dependencies:
+ '@types/node': 24.12.3
+
+ '@types/trusted-types@2.0.7':
+ optional: true
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@ungap/structured-clone@1.3.0': {}
+
+ '@ungap/structured-clone@1.3.1': {}
+
+ '@upsetjs/venn.js@2.0.0':
+ optionalDependencies:
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ abbrev@4.0.0: {}
+
+ acorn-jsx@5.3.2(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
+ acorn@8.16.0: {}
+
+ ansi-escapes@7.3.0:
+ dependencies:
+ environment: 1.1.0
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.2
+
+ arg@5.0.2: {}
+
+ argparse@2.0.1: {}
+
+ aria-query@5.3.2: {}
+
+ array-iterate@2.0.1: {}
+
+ astring@1.9.0: {}
+
+ astro-expressive-code@0.42.0(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)):
+ dependencies:
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ rehype-expressive-code: 0.42.0
+
+ astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0):
+ dependencies:
+ '@astrojs/compiler': 4.0.0
+ '@astrojs/internal-helpers': 0.9.1
+ '@astrojs/markdown-remark': 7.1.2
+ '@astrojs/telemetry': 3.3.2
+ '@capsizecss/unpack': 4.0.0
+ '@clack/prompts': 1.4.0
+ '@oslojs/encoding': 1.1.0
+ '@rollup/pluginutils': 5.3.0(rollup@4.60.4)
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ ci-info: 4.4.0
+ clsx: 2.1.1
+ common-ancestor-path: 2.0.0
+ cookie: 1.1.1
+ devalue: 5.8.1
+ diff: 8.0.4
+ dset: 3.1.4
+ es-module-lexer: 2.1.0
+ esbuild: 0.27.7
+ flattie: 1.1.1
+ fontace: 0.4.1
+ get-tsconfig: 5.0.0-beta.4
+ github-slugger: 2.0.0
+ html-escaper: 3.0.3
+ http-cache-semantics: 4.2.0
+ js-yaml: 4.1.1
+ jsonc-parser: 3.3.1
+ magic-string: 0.30.21
+ magicast: 0.5.3
+ mrmime: 2.0.1
+ neotraverse: 0.6.18
+ obug: 2.1.1
+ p-limit: 7.3.0
+ p-queue: 9.3.0
+ package-manager-detector: 1.6.0
+ piccolore: 0.1.3
+ picomatch: 4.0.4
+ rehype: 13.0.2
+ semver: 7.8.1
+ shiki: 4.1.0
+ smol-toml: 1.6.1
+ svgo: 4.0.1
+ tinyclip: 0.1.12
+ tinyexec: 1.2.2
+ tinyglobby: 0.2.16
+ ultrahtml: 1.6.0
+ unifont: 0.7.4
+ unist-util-visit: 5.1.0
+ unstorage: 1.17.5
+ vfile: 6.0.3
+ vite: 7.3.3(@types/node@24.12.3)(jiti@2.6.1)(yaml@2.9.0)
+ vitefu: 1.1.3(vite@7.3.3(@types/node@24.12.3)(jiti@2.6.1)(yaml@2.9.0))
+ xxhash-wasm: 1.1.0
+ yargs-parser: 22.0.0
+ zod: 4.4.3
+ optionalDependencies:
+ sharp: 0.34.5
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@types/node'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - aws4fetch
+ - db0
+ - idb-keyval
+ - ioredis
+ - jiti
+ - less
+ - lightningcss
+ - rollup
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+ - tsx
+ - uploadthing
+ - yaml
+
+ axobject-query@4.1.0: {}
+
+ bail@2.0.2: {}
+
+ bcp-47-match@2.0.3: {}
+
+ bcp-47@2.1.0:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+
+ boolbase@1.0.0: {}
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ ccount@2.0.1: {}
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ chokidar@5.0.0:
+ dependencies:
+ readdirp: 5.0.0
+
+ chownr@3.0.0: {}
+
+ ci-info@4.4.0: {}
+
+ clsx@2.1.1: {}
+
+ collapse-white-space@2.1.0: {}
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@11.1.0: {}
+
+ commander@7.2.0: {}
+
+ commander@8.3.0: {}
+
+ common-ancestor-path@2.0.0: {}
+
+ confbox@0.1.8: {}
+
+ cookie-es@1.2.3: {}
+
+ cookie@1.1.1: {}
+
+ cose-base@1.0.3:
+ dependencies:
+ layout-base: 1.0.2
+
+ cose-base@2.2.0:
+ dependencies:
+ layout-base: 2.0.1
+
+ crossws@0.3.5:
+ dependencies:
+ uncrypto: 0.1.3
+
+ css-select@5.2.2:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.2.2
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-selector-parser@3.3.0: {}
+
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.1
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
+ css-what@6.2.2: {}
+
+ cssesc@3.0.0: {}
+
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
+ cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2):
+ dependencies:
+ cose-base: 1.0.3
+ cytoscape: 3.33.2
+
+ cytoscape-fcose@2.2.0(cytoscape@3.33.2):
+ dependencies:
+ cose-base: 2.2.0
+ cytoscape: 3.33.2
+
+ cytoscape@3.33.2: {}
+
+ d3-array@2.12.1:
+ dependencies:
+ internmap: 1.0.1
+
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-axis@3.0.0: {}
+
+ d3-brush@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3-chord@3.0.1:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-color@3.1.0: {}
+
+ d3-contour@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-delaunay@6.0.4:
+ dependencies:
+ delaunator: 5.1.0
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-dsv@3.0.1:
+ dependencies:
+ commander: 7.2.0
+ iconv-lite: 0.6.3
+ rw: 1.3.3
+
+ d3-ease@3.0.1: {}
+
+ d3-fetch@3.0.1:
+ dependencies:
+ d3-dsv: 3.0.1
+
+ d3-force@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-timer: 3.0.1
+
+ d3-format@3.1.2: {}
+
+ d3-geo@3.1.1:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-hierarchy@3.1.2: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@1.0.9: {}
+
+ d3-path@3.1.0: {}
+
+ d3-polygon@3.0.1: {}
+
+ d3-quadtree@3.0.1: {}
+
+ d3-random@3.0.1: {}
+
+ d3-sankey@0.12.3:
+ dependencies:
+ d3-array: 2.12.1
+ d3-shape: 1.3.7
+
+ d3-scale-chromatic@3.1.0:
+ dependencies:
+ d3-color: 3.1.0
+ d3-interpolate: 3.0.1
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-shape@1.3.7:
+ dependencies:
+ d3-path: 1.0.9
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3@7.9.0:
+ dependencies:
+ d3-array: 3.2.4
+ d3-axis: 3.0.0
+ d3-brush: 3.0.0
+ d3-chord: 3.0.1
+ d3-color: 3.1.0
+ d3-contour: 4.0.2
+ d3-delaunay: 6.0.4
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-dsv: 3.0.1
+ d3-ease: 3.0.1
+ d3-fetch: 3.0.1
+ d3-force: 3.0.0
+ d3-format: 3.1.2
+ d3-geo: 3.1.1
+ d3-hierarchy: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-path: 3.1.0
+ d3-polygon: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-random: 3.0.1
+ d3-scale: 4.0.2
+ d3-scale-chromatic: 3.1.0
+ d3-selection: 3.0.0
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ d3-timer: 3.0.1
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3-zoom: 3.0.0
+
+ dagre-d3-es@7.0.14:
+ dependencies:
+ d3: 7.9.0
+ lodash-es: 4.18.1
+
+ dayjs@1.11.20: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
+ defu@6.1.7: {}
+
+ delaunator@5.1.0:
+ dependencies:
+ robust-predicates: 3.0.3
+
+ dequal@2.0.3: {}
+
+ destr@2.0.5: {}
+
+ detect-libc@2.1.2: {}
+
+ devalue@5.8.1: {}
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ diff@8.0.4: {}
+
+ direction@2.0.1: {}
+
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ dompurify@3.3.3:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dset@3.1.4: {}
+
+ entities@4.5.0: {}
+
+ entities@6.0.1: {}
+
+ env-paths@2.2.1: {}
+
+ environment@1.1.0: {}
+
+ es-module-lexer@2.1.0: {}
+
+ es-toolkit@1.46.1: {}
+
+ esast-util-from-estree@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ unist-util-position-from-estree: 2.0.0
+
+ esast-util-from-js@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ acorn: 8.16.0
+ esast-util-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ esbuild@0.27.7:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.27.7
+ '@esbuild/android-arm': 0.27.7
+ '@esbuild/android-arm64': 0.27.7
+ '@esbuild/android-x64': 0.27.7
+ '@esbuild/darwin-arm64': 0.27.7
+ '@esbuild/darwin-x64': 0.27.7
+ '@esbuild/freebsd-arm64': 0.27.7
+ '@esbuild/freebsd-x64': 0.27.7
+ '@esbuild/linux-arm': 0.27.7
+ '@esbuild/linux-arm64': 0.27.7
+ '@esbuild/linux-ia32': 0.27.7
+ '@esbuild/linux-loong64': 0.27.7
+ '@esbuild/linux-mips64el': 0.27.7
+ '@esbuild/linux-ppc64': 0.27.7
+ '@esbuild/linux-riscv64': 0.27.7
+ '@esbuild/linux-s390x': 0.27.7
+ '@esbuild/linux-x64': 0.27.7
+ '@esbuild/netbsd-arm64': 0.27.7
+ '@esbuild/netbsd-x64': 0.27.7
+ '@esbuild/openbsd-arm64': 0.27.7
+ '@esbuild/openbsd-x64': 0.27.7
+ '@esbuild/openharmony-arm64': 0.27.7
+ '@esbuild/sunos-x64': 0.27.7
+ '@esbuild/win32-arm64': 0.27.7
+ '@esbuild/win32-ia32': 0.27.7
+ '@esbuild/win32-x64': 0.27.7
+
+ escape-string-regexp@5.0.0: {}
+
+ estree-util-attach-comments@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ estree-util-build-jsx@3.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ estree-walker: 3.0.3
+
+ estree-util-is-identifier-name@3.0.0: {}
+
+ estree-util-scope@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+
+ estree-util-to-js@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ astring: 1.9.0
+ source-map: 0.7.6
+
+ estree-util-visit@2.0.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/unist': 3.0.3
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ eventemitter3@5.0.4: {}
+
+ exponential-backoff@3.1.3: {}
+
+ expressive-code@0.42.0:
+ dependencies:
+ '@expressive-code/core': 0.42.0
+ '@expressive-code/plugin-frames': 0.42.0
+ '@expressive-code/plugin-shiki': 0.42.0
+ '@expressive-code/plugin-text-markers': 0.42.0
+
+ extend@3.0.2: {}
+
+ fast-string-truncated-width@3.0.3: {}
+
+ fast-string-width@3.0.2:
+ dependencies:
+ fast-string-truncated-width: 3.0.3
+
+ fast-wrap-ansi@0.2.2:
+ dependencies:
+ fast-string-width: 3.0.2
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ flattie@1.1.1: {}
+
+ fontace@0.4.1:
+ dependencies:
+ fontkitten: 1.0.3
+
+ fontkitten@1.0.3:
+ dependencies:
+ tiny-inflate: 1.0.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ get-tsconfig@5.0.0-beta.4:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ github-slugger@2.0.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ h3@1.15.11:
+ dependencies:
+ cookie-es: 1.2.3
+ crossws: 0.3.5
+ defu: 6.1.7
+ destr: 2.0.5
+ iron-webcrypto: 1.2.1
+ node-mock-http: 1.0.4
+ radix3: 1.1.2
+ ufo: 1.6.4
+ uncrypto: 0.1.3
+
+ hachure-fill@0.5.2: {}
+
+ has-flag@5.0.1: {}
+
+ hast-util-embedded@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-is-element: 3.0.0
+
+ hast-util-format@1.1.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-embedded: 3.0.0
+ hast-util-minify-whitespace: 1.0.1
+ hast-util-phrasing: 3.0.1
+ hast-util-whitespace: 3.0.0
+ html-whitespace-sensitive-tag-names: 3.0.1
+ unist-util-visit-parents: 6.0.2
+
+ hast-util-from-html@2.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ devlop: 1.1.0
+ hast-util-from-parse5: 8.0.3
+ parse5: 7.3.0
+ vfile: 6.0.3
+ vfile-message: 4.0.3
+
+ hast-util-from-parse5@8.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ hastscript: 9.0.1
+ property-information: 7.1.0
+ vfile: 6.0.3
+ vfile-location: 5.0.3
+ web-namespaces: 2.0.1
+
+ hast-util-has-property@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-is-body-ok-link@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-minify-whitespace@1.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-embedded: 3.0.0
+ hast-util-is-element: 3.0.0
+ hast-util-whitespace: 3.0.0
+ unist-util-is: 6.0.1
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-phrasing@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-embedded: 3.0.0
+ hast-util-has-property: 3.0.0
+ hast-util-is-body-ok-link: 3.0.1
+ hast-util-is-element: 3.0.0
+
+ hast-util-raw@9.1.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ '@ungap/structured-clone': 1.3.0
+ hast-util-from-parse5: 8.0.3
+ hast-util-to-parse5: 8.0.1
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ parse5: 7.3.0
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ web-namespaces: 2.0.1
+ zwitch: 2.0.4
+
+ hast-util-select@6.0.4:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ bcp-47-match: 2.0.3
+ comma-separated-tokens: 2.0.3
+ css-selector-parser: 3.3.0
+ devlop: 1.1.0
+ direction: 2.0.1
+ hast-util-has-property: 3.0.0
+ hast-util-to-string: 3.0.1
+ hast-util-whitespace: 3.0.0
+ nth-check: 2.1.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ hast-util-to-estree@3.1.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-attach-comments: 3.0.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ zwitch: 2.0.4
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.21
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ hast-util-to-mdast@10.1.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.1
+ hast-util-phrasing: 3.0.1
+ hast-util-to-html: 9.0.5
+ hast-util-to-text: 4.0.2
+ hast-util-whitespace: 3.0.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-hast: 13.2.1
+ mdast-util-to-string: 4.0.0
+ rehype-minify-whitespace: 6.0.2
+ trim-trailing-lines: 2.1.0
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+
+ hast-util-to-parse5@8.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ web-namespaces: 2.0.1
+ zwitch: 2.0.4
+
+ hast-util-to-string@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hast-util-to-text@4.0.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ hast-util-is-element: 3.0.0
+ unist-util-find-after: 5.0.0
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+
+ html-escaper@3.0.3: {}
+
+ html-void-elements@3.0.0: {}
+
+ html-whitespace-sensitive-tag-names@3.0.1: {}
+
+ http-cache-semantics@4.2.0: {}
+
+ i18next@26.1.0: {}
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ inline-style-parser@0.2.7: {}
+
+ internmap@1.0.1: {}
+
+ internmap@2.0.3: {}
+
+ iron-webcrypto@1.2.1: {}
+
+ is-absolute-url@4.0.1: {}
+
+ is-absolute-url@5.0.0: {}
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-decimal@2.0.1: {}
+
+ is-docker@3.0.0: {}
+
+ is-docker@4.0.0: {}
+
+ is-hexadecimal@2.0.1: {}
+
+ is-inside-container@1.0.0:
+ dependencies:
+ is-docker: 3.0.0
+
+ is-number@7.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-wsl@3.1.1:
+ dependencies:
+ is-inside-container: 1.0.0
+
+ isexe@4.0.0: {}
+
+ jiti@2.6.1:
+ optional: true
+
+ js-yaml@4.1.1:
+ dependencies:
+ argparse: 2.0.1
+
+ jsonc-parser@3.3.1: {}
+
+ katex@0.16.45:
+ dependencies:
+ commander: 8.3.0
+
+ khroma@2.1.0: {}
+
+ klona@2.0.6: {}
+
+ layout-base@1.0.2: {}
+
+ layout-base@2.0.1: {}
+
+ lodash-es@4.18.1: {}
+
+ longest-streak@3.1.0: {}
+
+ lru-cache@11.5.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ magicast@0.5.3:
+ dependencies:
+ '@babel/parser': 7.29.3
+ '@babel/types': 7.29.0
+ source-map-js: 1.2.1
+
+ markdown-extensions@2.0.0: {}
+
+ markdown-table@3.0.4: {}
+
+ marked@16.4.2: {}
+
+ mdast-util-definitions@6.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ unist-util-visit: 5.1.0
+
+ mdast-util-directive@3.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-visit-parents: 6.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-find-and-replace@3.0.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ escape-string-regexp: 5.0.0
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-autolink-literal@2.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-find-and-replace: 3.0.2
+ micromark-util-character: 2.1.1
+
+ mdast-util-gfm-footnote@2.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ micromark-util-normalize-identifier: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-strikethrough@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-table@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ markdown-table: 3.0.4
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm-task-list-item@2.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-gfm@3.1.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-gfm-autolink-literal: 2.0.1
+ mdast-util-gfm-footnote: 2.1.0
+ mdast-util-gfm-strikethrough: 2.0.0
+ mdast-util-gfm-table: 2.0.0
+ mdast-util-gfm-task-list-item: 2.0.0
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx@3.0.0:
+ dependencies:
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.3.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
+ mdn-data@2.0.28: {}
+
+ mdn-data@2.27.1: {}
+
+ mermaid@11.15.0:
+ dependencies:
+ '@braintree/sanitize-url': 7.1.2
+ '@iconify/utils': 3.1.0
+ '@mermaid-js/parser': 1.1.1
+ '@types/d3': 7.4.3
+ '@upsetjs/venn.js': 2.0.0
+ cytoscape: 3.33.2
+ cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2)
+ cytoscape-fcose: 2.2.0(cytoscape@3.33.2)
+ d3: 7.9.0
+ d3-sankey: 0.12.3
+ dagre-d3-es: 7.0.14
+ dayjs: 1.11.20
+ dompurify: 3.3.3
+ es-toolkit: 1.46.1
+ katex: 0.16.45
+ khroma: 2.1.0
+ marked: 16.4.2
+ roughjs: 4.6.6
+ stylis: 4.3.6
+ ts-dedent: 2.2.0
+ uuid: 11.1.0
+
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-directive@4.0.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ parse-entities: 4.0.2
+
+ micromark-extension-gfm-autolink-literal@2.1.0:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-footnote@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-strikethrough@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-table@2.1.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-tagfilter@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm-task-list-item@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-gfm@3.0.0:
+ dependencies:
+ micromark-extension-gfm-autolink-literal: 2.1.0
+ micromark-extension-gfm-footnote: 2.1.0
+ micromark-extension-gfm-strikethrough: 2.1.0
+ micromark-extension-gfm-table: 2.1.1
+ micromark-extension-gfm-tagfilter: 2.0.0
+ micromark-extension-gfm-task-list-item: 2.1.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-expression@3.0.1:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdx-jsx@3.0.2:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ micromark-factory-mdx-expression: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-extension-mdx-md@2.0.0:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-extension-mdxjs-esm@3.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-extension-mdxjs@3.0.0:
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ micromark-extension-mdx-expression: 3.0.1
+ micromark-extension-mdx-jsx: 3.0.2
+ micromark-extension-mdx-md: 2.0.0
+ micromark-extension-mdxjs-esm: 3.0.0
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-mdx-expression@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-events-to-acorn: 2.0.3
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-position-from-estree: 2.0.0
+ vfile-message: 4.0.3
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-events-to-acorn@2.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/unist': 3.0.3
+ devlop: 1.1.0
+ estree-util-visit: 2.0.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ vfile-message: 4.0.3
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.2
+
+ minipass@7.1.3: {}
+
+ minizlib@3.1.0:
+ dependencies:
+ minipass: 7.1.3
+
+ mlly@1.8.2:
+ dependencies:
+ acorn: 8.16.0
+ pathe: 2.0.3
+ pkg-types: 1.3.1
+ ufo: 1.6.3
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.12: {}
+
+ neotraverse@0.6.18: {}
+
+ nlcst-to-string@4.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+
+ node-addon-api@8.8.0: {}
+
+ node-fetch-native@1.6.7: {}
+
+ node-gyp@12.3.0:
+ dependencies:
+ env-paths: 2.2.1
+ exponential-backoff: 3.1.3
+ graceful-fs: 4.2.11
+ nopt: 9.0.0
+ proc-log: 6.1.0
+ semver: 7.8.1
+ tar: 7.5.15
+ tinyglobby: 0.2.16
+ undici: 6.25.0
+ which: 6.0.1
+
+ node-mock-http@1.0.4: {}
+
+ nopt@9.0.0:
+ dependencies:
+ abbrev: 4.0.0
+
+ normalize-path@3.0.0: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ obug@2.1.1: {}
+
+ ofetch@1.5.1:
+ dependencies:
+ destr: 2.0.5
+ node-fetch-native: 1.6.7
+ ufo: 1.6.4
+
+ ohash@2.0.11: {}
+
+ oniguruma-parser@0.12.1: {}
+
+ oniguruma-parser@0.12.2: {}
+
+ oniguruma-to-es@4.3.5:
+ dependencies:
+ oniguruma-parser: 0.12.1
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
+ oniguruma-to-es@4.3.6:
+ dependencies:
+ oniguruma-parser: 0.12.2
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
+ p-limit@7.3.0:
+ dependencies:
+ yocto-queue: 1.2.2
+
+ p-queue@9.3.0:
+ dependencies:
+ eventemitter3: 5.0.4
+ p-timeout: 7.0.1
+
+ p-timeout@7.0.1: {}
+
+ package-manager-detector@1.6.0: {}
+
+ pagefind@1.5.2:
+ optionalDependencies:
+ '@pagefind/darwin-arm64': 1.5.2
+ '@pagefind/darwin-x64': 1.5.2
+ '@pagefind/freebsd-x64': 1.5.2
+ '@pagefind/linux-arm64': 1.5.2
+ '@pagefind/linux-x64': 1.5.2
+ '@pagefind/windows-arm64': 1.5.2
+ '@pagefind/windows-x64': 1.5.2
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
+ parse-latin@7.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ '@types/unist': 3.0.3
+ nlcst-to-string: 4.0.0
+ unist-util-modify-children: 4.0.0
+ unist-util-visit-children: 3.0.0
+ vfile: 6.0.3
+
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
+ path-data-parser@0.1.0: {}
+
+ pathe@2.0.3: {}
+
+ piccolore@0.1.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ picomatch@4.0.4: {}
+
+ pkg-types@1.3.1:
+ dependencies:
+ confbox: 0.1.8
+ mlly: 1.8.2
+ pathe: 2.0.3
+
+ points-on-curve@0.2.0: {}
+
+ points-on-path@0.2.1:
+ dependencies:
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+
+ postcss-nested@6.2.0(postcss@8.5.14):
+ dependencies:
+ postcss: 8.5.14
+ postcss-selector-parser: 6.1.2
+
+ postcss-selector-parser@6.1.2:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.14:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ postcss@8.5.15:
+ dependencies:
+ nanoid: 3.3.12
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prismjs@1.30.0: {}
+
+ proc-log@6.1.0: {}
+
+ property-information@7.1.0: {}
+
+ radix3@1.1.2: {}
+
+ readdirp@5.0.0: {}
+
+ recma-build-jsx@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-util-build-jsx: 3.0.1
+ vfile: 6.0.3
+
+ recma-jsx@1.0.1(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+ acorn-jsx: 5.3.2(acorn@8.16.0)
+ estree-util-to-js: 2.0.0
+ recma-parse: 1.0.0
+ recma-stringify: 1.0.0
+ unified: 11.0.5
+
+ recma-parse@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ esast-util-from-js: 2.0.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ recma-stringify@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-util-to-js: 2.0.0
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ rehype-expressive-code@0.42.0:
+ dependencies:
+ expressive-code: 0.42.0
+
+ rehype-external-links@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@ungap/structured-clone': 1.3.0
+ hast-util-is-element: 3.0.0
+ is-absolute-url: 4.0.1
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 5.1.0
+
+ rehype-format@5.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-format: 1.1.0
+
+ rehype-minify-whitespace@6.0.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-minify-whitespace: 1.0.1
+
+ rehype-parse@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-from-html: 2.0.3
+ unified: 11.0.5
+
+ rehype-raw@7.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-raw: 9.1.0
+ vfile: 6.0.3
+
+ rehype-recma@1.0.0:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/hast': 3.0.4
+ hast-util-to-estree: 3.1.3
+ transitivePeerDependencies:
+ - supports-color
+
+ rehype-remark@10.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ hast-util-to-mdast: 10.1.2
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ rehype-stringify@10.0.1:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+ unified: 11.0.5
+
+ rehype@13.0.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ rehype-parse: 9.0.1
+ rehype-stringify: 10.0.1
+ unified: 11.0.5
+
+ remark-directive@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-directive: 3.1.0
+ micromark-extension-directive: 4.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-gfm@4.0.1:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-gfm: 3.1.0
+ micromark-extension-gfm: 3.0.0
+ remark-parse: 11.0.0
+ remark-stringify: 11.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-mdx@3.1.1:
+ dependencies:
+ mdast-util-mdx: 3.0.0
+ micromark-extension-mdxjs: 3.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-parse@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-from-markdown: 2.0.3
+ micromark-util-types: 2.0.2
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ remark-rehype@11.1.2:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ mdast-util-to-hast: 13.2.1
+ unified: 11.0.5
+ vfile: 6.0.3
+
+ remark-smartypants@3.0.2:
+ dependencies:
+ retext: 9.0.0
+ retext-smartypants: 6.2.0
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+
+ remark-stringify@11.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-to-markdown: 2.1.2
+ unified: 11.0.5
+
+ resolve-pkg-maps@1.0.0: {}
+
+ retext-latin@4.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ parse-latin: 7.0.0
+ unified: 11.0.5
+
+ retext-smartypants@6.2.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ nlcst-to-string: 4.0.0
+ unist-util-visit: 5.1.0
+
+ retext-stringify@4.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ nlcst-to-string: 4.0.0
+ unified: 11.0.5
+
+ retext@9.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ retext-latin: 4.0.0
+ retext-stringify: 4.0.0
+ unified: 11.0.5
+
+ robust-predicates@3.0.3: {}
+
+ rollup@4.60.4:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.60.4
+ '@rollup/rollup-android-arm64': 4.60.4
+ '@rollup/rollup-darwin-arm64': 4.60.4
+ '@rollup/rollup-darwin-x64': 4.60.4
+ '@rollup/rollup-freebsd-arm64': 4.60.4
+ '@rollup/rollup-freebsd-x64': 4.60.4
+ '@rollup/rollup-linux-arm-gnueabihf': 4.60.4
+ '@rollup/rollup-linux-arm-musleabihf': 4.60.4
+ '@rollup/rollup-linux-arm64-gnu': 4.60.4
+ '@rollup/rollup-linux-arm64-musl': 4.60.4
+ '@rollup/rollup-linux-loong64-gnu': 4.60.4
+ '@rollup/rollup-linux-loong64-musl': 4.60.4
+ '@rollup/rollup-linux-ppc64-gnu': 4.60.4
+ '@rollup/rollup-linux-ppc64-musl': 4.60.4
+ '@rollup/rollup-linux-riscv64-gnu': 4.60.4
+ '@rollup/rollup-linux-riscv64-musl': 4.60.4
+ '@rollup/rollup-linux-s390x-gnu': 4.60.4
+ '@rollup/rollup-linux-x64-gnu': 4.60.4
+ '@rollup/rollup-linux-x64-musl': 4.60.4
+ '@rollup/rollup-openbsd-x64': 4.60.4
+ '@rollup/rollup-openharmony-arm64': 4.60.4
+ '@rollup/rollup-win32-arm64-msvc': 4.60.4
+ '@rollup/rollup-win32-ia32-msvc': 4.60.4
+ '@rollup/rollup-win32-x64-gnu': 4.60.4
+ '@rollup/rollup-win32-x64-msvc': 4.60.4
+ fsevents: 2.3.3
+
+ roughjs@4.6.6:
+ dependencies:
+ hachure-fill: 0.5.2
+ path-data-parser: 0.1.0
+ points-on-curve: 0.2.0
+ points-on-path: 0.2.1
+
+ rw@1.3.3: {}
+
+ safer-buffer@2.1.2: {}
+
+ sax@1.6.0: {}
+
+ semver@7.7.4: {}
+
+ semver@7.8.1: {}
+
+ sharp@0.34.5:
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.7.4
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.34.5
+ '@img/sharp-darwin-x64': 0.34.5
+ '@img/sharp-libvips-darwin-arm64': 1.2.4
+ '@img/sharp-libvips-darwin-x64': 1.2.4
+ '@img/sharp-libvips-linux-arm': 1.2.4
+ '@img/sharp-libvips-linux-arm64': 1.2.4
+ '@img/sharp-libvips-linux-ppc64': 1.2.4
+ '@img/sharp-libvips-linux-riscv64': 1.2.4
+ '@img/sharp-libvips-linux-s390x': 1.2.4
+ '@img/sharp-libvips-linux-x64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.2.4
+ '@img/sharp-linux-arm': 0.34.5
+ '@img/sharp-linux-arm64': 0.34.5
+ '@img/sharp-linux-ppc64': 0.34.5
+ '@img/sharp-linux-riscv64': 0.34.5
+ '@img/sharp-linux-s390x': 0.34.5
+ '@img/sharp-linux-x64': 0.34.5
+ '@img/sharp-linuxmusl-arm64': 0.34.5
+ '@img/sharp-linuxmusl-x64': 0.34.5
+ '@img/sharp-wasm32': 0.34.5
+ '@img/sharp-win32-arm64': 0.34.5
+ '@img/sharp-win32-ia32': 0.34.5
+ '@img/sharp-win32-x64': 0.34.5
+
+ shiki@4.0.2:
+ dependencies:
+ '@shikijs/core': 4.0.2
+ '@shikijs/engine-javascript': 4.0.2
+ '@shikijs/engine-oniguruma': 4.0.2
+ '@shikijs/langs': 4.0.2
+ '@shikijs/themes': 4.0.2
+ '@shikijs/types': 4.0.2
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ shiki@4.1.0:
+ dependencies:
+ '@shikijs/core': 4.1.0
+ '@shikijs/engine-javascript': 4.1.0
+ '@shikijs/engine-oniguruma': 4.1.0
+ '@shikijs/langs': 4.1.0
+ '@shikijs/themes': 4.1.0
+ '@shikijs/types': 4.1.0
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ sisteransi@1.0.5: {}
+
+ sitemap@9.0.1:
+ dependencies:
+ '@types/node': 24.12.3
+ '@types/sax': 1.2.7
+ arg: 5.0.2
+ sax: 1.6.0
+
+ smol-toml@1.6.1: {}
+
+ source-map-js@1.2.1: {}
+
+ source-map@0.7.6: {}
+
+ space-separated-tokens@2.0.2: {}
+
+ starlight-changelogs@0.5.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)):
+ dependencies:
+ '@ascorbic/loader-utils': 1.0.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@astrojs/starlight': 0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ github-slugger: 2.0.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ mdast-util-to-string: 4.0.0
+ unist-util-visit: 5.1.0
+ transitivePeerDependencies:
+ - astro
+ - supports-color
+
+ starlight-contextual-menu@0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))(starlight-markdown@0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))):
+ dependencies:
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ starlight-markdown: 0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+
+ starlight-links-validator@0.24.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)):
+ dependencies:
+ '@astrojs/starlight': 0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@types/picomatch': 4.0.3
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ github-slugger: 2.0.0
+ hast-util-from-html: 2.0.3
+ is-absolute-url: 5.0.0
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-to-hast: 13.2.1
+ picomatch: 4.0.4
+ terminal-link: 5.0.0
+ unist-util-visit: 5.1.0
+ yaml: 2.9.0
+ transitivePeerDependencies:
+ - supports-color
+
+ starlight-llms-txt@0.10.0(@astrojs/starlight@0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)))(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)):
+ dependencies:
+ '@astrojs/mdx': 5.0.6(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@astrojs/starlight': 0.39.2(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0))
+ '@types/hast': 3.0.4
+ '@types/micromatch': 4.0.10
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+ github-slugger: 2.0.0
+ hast-util-select: 6.0.4
+ micromatch: 4.0.8
+ rehype-parse: 9.0.1
+ rehype-remark: 10.0.1
+ remark-gfm: 4.0.1
+ remark-stringify: 11.0.0
+ unified: 11.0.5
+ unist-util-remove: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ starlight-markdown@0.1.5(astro@6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)):
+ dependencies:
+ astro: 6.3.7(@types/node@24.12.3)(jiti@2.6.1)(rollup@4.60.4)(yaml@2.9.0)
+
+ stream-replace-string@2.0.0: {}
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
+ style-to-js@1.1.21:
+ dependencies:
+ style-to-object: 1.0.14
+
+ style-to-object@1.0.14:
+ dependencies:
+ inline-style-parser: 0.2.7
+
+ stylis@4.3.6: {}
+
+ supports-color@10.2.2: {}
+
+ supports-hyperlinks@4.4.0:
+ dependencies:
+ has-flag: 5.0.1
+ supports-color: 10.2.2
+
+ svgo@4.0.1:
+ dependencies:
+ commander: 11.1.0
+ css-select: 5.2.2
+ css-tree: 3.2.1
+ css-what: 6.2.2
+ csso: 5.0.5
+ picocolors: 1.1.1
+ sax: 1.6.0
+
+ tar@7.5.15:
+ dependencies:
+ '@isaacs/fs-minipass': 4.0.1
+ chownr: 3.0.0
+ minipass: 7.1.3
+ minizlib: 3.1.0
+ yallist: 5.0.0
+
+ terminal-link@5.0.0:
+ dependencies:
+ ansi-escapes: 7.3.0
+ supports-hyperlinks: 4.4.0
+
+ tiny-inflate@1.0.3: {}
+
+ tinyclip@0.1.12: {}
+
+ tinyexec@1.0.4: {}
+
+ tinyexec@1.2.2: {}
+
+ tinyglobby@0.2.16:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ trim-lines@3.0.1: {}
+
+ trim-trailing-lines@2.1.0: {}
+
+ trough@2.2.0: {}
+
+ ts-dedent@2.2.0: {}
+
+ tslib@2.8.1:
+ optional: true
+
+ ufo@1.6.3: {}
+
+ ufo@1.6.4: {}
+
+ ultrahtml@1.6.0: {}
+
+ uncrypto@0.1.3: {}
+
+ undici-types@7.16.0: {}
+
+ undici@6.25.0: {}
+
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unifont@0.7.4:
+ dependencies:
+ css-tree: 3.2.1
+ ofetch: 1.5.1
+ ohash: 2.0.11
+
+ unist-util-find-after@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-modify-children@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ array-iterate: 2.0.1
+
+ unist-util-position-from-estree@2.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-remove-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-visit: 5.1.0
+
+ unist-util-remove@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-children@3.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ unstorage@1.17.5:
+ dependencies:
+ anymatch: 3.1.3
+ chokidar: 5.0.0
+ destr: 2.0.5
+ h3: 1.15.11
+ lru-cache: 11.5.0
+ node-fetch-native: 1.6.7
+ ofetch: 1.5.1
+ ufo: 1.6.4
+
+ util-deprecate@1.0.2: {}
+
+ uuid@11.1.0: {}
+
+ vfile-location@5.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile: 6.0.3
+
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite@7.3.3(@types/node@24.12.3)(jiti@2.6.1)(yaml@2.9.0):
+ dependencies:
+ esbuild: 0.27.7
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+ postcss: 8.5.15
+ rollup: 4.60.4
+ tinyglobby: 0.2.16
+ optionalDependencies:
+ '@types/node': 24.12.3
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ yaml: 2.9.0
+
+ vitefu@1.1.3(vite@7.3.3(@types/node@24.12.3)(jiti@2.6.1)(yaml@2.9.0)):
+ optionalDependencies:
+ vite: 7.3.3(@types/node@24.12.3)(jiti@2.6.1)(yaml@2.9.0)
+
+ web-namespaces@2.0.1: {}
+
+ which-pm-runs@1.1.0: {}
+
+ which@6.0.1:
+ dependencies:
+ isexe: 4.0.0
+
+ xxhash-wasm@1.1.0: {}
+
+ yallist@5.0.0: {}
+
+ yaml@2.9.0: {}
+
+ yargs-parser@22.0.0: {}
+
+ yocto-queue@1.2.2: {}
+
+ zod@4.4.3: {}
+
+ zwitch@2.0.4: {}
diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml
new file mode 100644
index 00000000000..d0b7dbe2294
--- /dev/null
+++ b/docs/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+onlyBuiltDependencies:
+ - esbuild
+ - sharp
diff --git a/docs/public/CNAME b/docs/public/CNAME
new file mode 100644
index 00000000000..91547b2904f
--- /dev/null
+++ b/docs/public/CNAME
@@ -0,0 +1 @@
+invoke.ai
diff --git a/docs/public/coverimage.png b/docs/public/coverimage.png
new file mode 100644
index 00000000000..6586d8ba738
Binary files /dev/null and b/docs/public/coverimage.png differ
diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg
new file mode 100644
index 00000000000..28d6a6d08e5
--- /dev/null
+++ b/docs/public/favicon.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/scripts/validate-redirect-targets.mjs b/docs/scripts/validate-redirect-targets.mjs
new file mode 100644
index 00000000000..1ec6e7f0898
--- /dev/null
+++ b/docs/scripts/validate-redirect-targets.mjs
@@ -0,0 +1,65 @@
+import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
+import { dirname, join, relative } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const docsRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
+const contentRoot = join(docsRoot, 'src', 'content', 'docs');
+const redirectsFile = join(docsRoot, 'src', 'config', 'redirects.ts');
+
+const normalizeRoute = (route) => {
+ const normalized = route
+ .replace(/^\/+|\/+$/g, '')
+ .split('/')
+ .filter(Boolean)
+ .map((segment) => segment.toLowerCase().replaceAll(' ', '-'))
+ .join('/');
+
+ return normalized ? `/${normalized}` : '/';
+};
+
+const collectDocsRoutes = (dir, routes = new Set()) => {
+ for (const entry of readdirSync(dir)) {
+ const entryPath = join(dir, entry);
+ const stats = statSync(entryPath);
+
+ if (stats.isDirectory()) {
+ collectDocsRoutes(entryPath, routes);
+ continue;
+ }
+
+ if (!entry.endsWith('.md') && !entry.endsWith('.mdx')) {
+ continue;
+ }
+
+ const relativePath = relative(contentRoot, entryPath).replace(/\\/g, '/').replace(/\.mdx?$/, '');
+ const route = relativePath.endsWith('/index') ? relativePath.slice(0, -'/index'.length) : relativePath;
+ routes.add(normalizeRoute(route));
+
+ const segments = route.split('/').filter(Boolean);
+ for (let index = 1; index < segments.length; index++) {
+ routes.add(normalizeRoute(segments.slice(0, index).join('/')));
+ }
+ }
+
+ return routes;
+};
+
+if (!existsSync(contentRoot)) {
+ throw new Error(`Docs content directory not found: ${contentRoot}`);
+}
+
+const redirectsSource = readFileSync(redirectsFile, 'utf8');
+const redirectMatches = redirectsSource.matchAll(/^\s*['"]([^'"]+)['"]:\s*['"]([^'"]+)['"]/gm);
+const redirectTargets = Array.from(redirectMatches, ([, from, to]) => ({ from, to }));
+const docsRoutes = collectDocsRoutes(contentRoot);
+const missingTargets = redirectTargets.filter(({ to }) => !docsRoutes.has(normalizeRoute(to)));
+
+if (missingTargets.length > 0) {
+ console.error('Redirect targets must resolve to generated docs routes:');
+ for (const { from, to } of missingTargets) {
+ console.error(` ${from} -> ${to}`);
+ }
+ process.exit(1);
+}
+
+console.log(`Validated ${redirectTargets.length} redirect targets.`);
diff --git a/docs/scripts/verify-deploy-output.mjs b/docs/scripts/verify-deploy-output.mjs
new file mode 100644
index 00000000000..885d1f44baa
--- /dev/null
+++ b/docs/scripts/verify-deploy-output.mjs
@@ -0,0 +1,70 @@
+import { readFileSync } from 'node:fs';
+
+const deployTarget = process.env.DEPLOY_TARGET ?? 'custom';
+const base = deployTarget === 'ghpages' ? '/InvokeAI' : '';
+const withBase = (path) => `${base}${path}`;
+
+const expectations = [
+ {
+ file: 'index.html',
+ includes: [
+ `href="${withBase('/_astro/')}`,
+ `src="${withBase('/_astro/')}`,
+ `href="${withBase('/start-here/installation/')}`,
+ ],
+ excludes: deployTarget === 'custom' ? ['href="/InvokeAI/', 'src="/InvokeAI/'] : ['href="/_astro/', 'src="/_astro/'],
+ },
+ {
+ file: 'contributing/index.html',
+ includes: [`href="${withBase('/contributing/new-contributor-guide/')}`],
+ excludes: [
+ deployTarget === 'custom'
+ ? 'href="/InvokeAI/contributing/new-contributor-guide/"'
+ : 'href="/contributing/new-contributor-guide/"',
+ 'newContributorChecklist.md',
+ ],
+ },
+ {
+ file: 'contributing/contribution_guides/newContributorChecklist/index.html',
+ includes: [
+ `Redirecting to: ${withBase('/contributing/new-contributor-guide')}`,
+ `content="0;url=${withBase('/contributing/new-contributor-guide')}`,
+ `href="${withBase('/contributing/new-contributor-guide')}`,
+ ],
+ excludes: deployTarget === 'custom'
+ ? [
+ 'Redirecting to: /InvokeAI/contributing/new-contributor-guide',
+ 'content="0;url=/InvokeAI/contributing/new-contributor-guide',
+ 'href="/InvokeAI/contributing/new-contributor-guide',
+ ]
+ : [
+ 'Redirecting to: /contributing/new-contributor-guide',
+ 'content="0;url=/contributing/new-contributor-guide',
+ 'href="/contributing/new-contributor-guide',
+ ],
+ },
+];
+
+const errors = [];
+
+for (const { file, includes = [], excludes = [] } of expectations) {
+ const html = readFileSync(new URL(`../dist/${file}`, import.meta.url), 'utf8');
+
+ for (const expected of includes) {
+ if (!html.includes(expected)) {
+ errors.push(`${file} is missing ${expected}`);
+ }
+ }
+
+ for (const unexpected of excludes) {
+ if (html.includes(unexpected)) {
+ errors.push(`${file} still contains ${unexpected}`);
+ }
+ }
+}
+
+if (errors.length > 0) {
+ throw new Error(`${deployTarget} output validation failed:\n- ${errors.join('\n- ')}`);
+}
+
+console.log(`${deployTarget} output links and assets look correct.`);
diff --git a/docs/src/assets/coverimage.png b/docs/src/assets/coverimage.png
new file mode 100644
index 00000000000..6586d8ba738
Binary files /dev/null and b/docs/src/assets/coverimage.png differ
diff --git a/docs/src/assets/invoke-icon-wide.svg b/docs/src/assets/invoke-icon-wide.svg
new file mode 100644
index 00000000000..cfeff994147
--- /dev/null
+++ b/docs/src/assets/invoke-icon-wide.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/src/assets/invoke-icon.svg b/docs/src/assets/invoke-icon.svg
new file mode 100644
index 00000000000..17cfdc77da7
--- /dev/null
+++ b/docs/src/assets/invoke-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/docs/src/config/head.ts b/docs/src/config/head.ts
new file mode 100644
index 00000000000..03fe1debd5f
--- /dev/null
+++ b/docs/src/config/head.ts
@@ -0,0 +1,79 @@
+import type { StarlightUserConfig } from '@astrojs/starlight/types';
+
+type HeadConfig = NonNullable;
+
+type CreateHeadConfigParams = {
+ base: string;
+ enableAnalytics: boolean;
+ isGhPages: boolean;
+ site: string;
+};
+
+const plausibleScriptUrl =
+ 'https://plausible.tracking.events/js/pa-BHcumuOemKz4XIQeWkTn4.js';
+const plausibleInitScript =
+ 'window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()';
+
+function createHeadConfig({
+ base,
+ enableAnalytics,
+ isGhPages,
+ site,
+}: CreateHeadConfigParams): HeadConfig {
+ const coverImageUrl = new URL(`${base}/coverimage.png`, site).toString();
+
+ return [
+ {
+ tag: 'meta',
+ attrs: {
+ property: 'og:image',
+ content: coverImageUrl,
+ },
+ },
+ {
+ tag: 'meta',
+ attrs: {
+ property: 'og:image:width',
+ content: '1200',
+ },
+ },
+ {
+ tag: 'meta',
+ attrs: {
+ property: 'og:image:height',
+ content: '630',
+ },
+ },
+ {
+ tag: 'meta',
+ attrs: {
+ name: 'twitter:card',
+ content: 'summary_large_image',
+ },
+ },
+ {
+ tag: 'meta',
+ attrs: {
+ name: 'twitter:image',
+ content: coverImageUrl,
+ },
+ },
+ ...(enableAnalytics && !isGhPages
+ ? ([
+ {
+ tag: 'script',
+ attrs: {
+ async: true,
+ src: plausibleScriptUrl,
+ },
+ },
+ {
+ tag: 'script',
+ content: plausibleInitScript,
+ },
+ ] satisfies HeadConfig)
+ : []),
+ ] satisfies HeadConfig;
+}
+
+export { createHeadConfig };
diff --git a/docs/src/config/index.ts b/docs/src/config/index.ts
new file mode 100644
index 00000000000..6fdc64bfb21
--- /dev/null
+++ b/docs/src/config/index.ts
@@ -0,0 +1,4 @@
+export * from './head';
+export * from './redirects';
+export * from './sidebar';
+export * from './social';
diff --git a/docs/src/config/redirects.ts b/docs/src/config/redirects.ts
new file mode 100644
index 00000000000..8c2b3e69c7f
--- /dev/null
+++ b/docs/src/config/redirects.ts
@@ -0,0 +1,55 @@
+import type { AstroConfig } from 'astro';
+
+type RedirectsConfig = AstroConfig['redirects'];
+
+const redirects: RedirectsConfig = {
+ '/CODE_OF_CONDUCT': '/contributing/code-of-conduct',
+ '/RELEASE': '/development/process/release-process',
+ '/installation': '/start-here/installation',
+ '/installation/docker': '/configuration/docker',
+ '/installation/manual': '/start-here/manual',
+ '/installation/models': '/concepts/models',
+ '/installation/patchmatch': '/configuration/patchmatch',
+ '/installation/quick_start': '/start-here/installation',
+ '/installation/requirements': '/start-here/system-requirements',
+ '/configuration': '/configuration/invokeai-yaml',
+ '/features/low-vram/': '/configuration/low-vram-mode/',
+ '/features/lasso-tool': '/features/canvas/lasso-tool',
+ '/features/shapes-tool': '/features/canvas/shapes-tool',
+ '/faq': '/troubleshooting/faq',
+ '/help/SAMPLER_CONVERGENCE': '/concepts/parameters',
+ '/help/diffusion': '/concepts/diffusion',
+ '/help/gettingStartedWithAI': '/concepts/image-generation',
+ '/nodes/NODES': '/features/workflows/editor-interface',
+ '/nodes/NODES_MIGRATION_V3_V4': '/development/guides/api-development',
+ '/nodes/comfyToInvoke': '/features/workflows/comfyui-migration',
+ '/nodes/communityNodes': '/features/workflows/community-nodes',
+ '/nodes/contributingNodes': '/development/guides/creating-nodes',
+ '/nodes/detailedNodes/faceTools': '/features/workflows/face-tools',
+ '/nodes/invocation-api': '/development/guides/api-development',
+ '/contributing/ARCHITECTURE': '/development/architecture/overview',
+ '/contributing/DOWNLOAD_QUEUE': '/development/architecture/model-manager',
+ '/contributing/HOTKEYS': '/features/hotkeys',
+ '/contributing/INVOCATIONS': '/development/architecture/invocations',
+ '/contributing/LOCAL_DEVELOPMENT': '/development/setup/dev-environment',
+ '/contributing/MODEL_MANAGER': '/development/architecture/model-manager',
+ '/contributing/NEW_MODEL_INTEGRATION': '/development/guides/models',
+ '/contributing/PR-MERGE-POLICY': '/development/process/pr-merge-policy',
+ '/contributing/TESTS': '/development/guides/tests',
+ '/contributing/contribution_guides/development': '/development',
+ '/contributing/contribution_guides/newContributorChecklist':
+ '/contributing/new-contributor-guide',
+ '/contributing/dev-environment': '/development/setup/dev-environment',
+ '/contributing/frontend': '/development/front-end',
+ '/contributing/frontend/state-management':
+ '/development/front-end/state-management',
+ '/contributing/frontend/workflows': '/development/front-end/workflows',
+};
+
+function createRedirects(base: string): RedirectsConfig {
+ return Object.fromEntries(
+ Object.entries(redirects).map(([from, to]) => [from, base + to]),
+ );
+}
+
+export { createRedirects };
diff --git a/docs/src/config/sidebar.ts b/docs/src/config/sidebar.ts
new file mode 100644
index 00000000000..7d85c5d2e1d
--- /dev/null
+++ b/docs/src/config/sidebar.ts
@@ -0,0 +1,80 @@
+import type { StarlightUserConfig } from '@astrojs/starlight/types';
+import { makeChangelogsSidebarLinks } from 'starlight-changelogs';
+
+type SidebarConfig = StarlightUserConfig['sidebar'];
+
+const sidebar: SidebarConfig = [
+ {
+ label: 'Start Here',
+ items: [
+ {
+ autogenerate: { directory: 'start-here' },
+ },
+ ],
+ },
+ {
+ label: 'Configuration',
+ items: [
+ {
+ autogenerate: { directory: 'configuration' },
+ },
+ ],
+ },
+ {
+ label: 'Concepts',
+ items: [
+ {
+ autogenerate: { directory: 'concepts' },
+ },
+ ],
+ },
+ {
+ label: 'Features',
+ items: [
+ {
+ autogenerate: { directory: 'features' },
+ },
+ ],
+ },
+ {
+ label: 'Development',
+ items: [
+ {
+ autogenerate: { directory: 'development', collapsed: true },
+ },
+ ],
+ collapsed: true,
+ },
+ {
+ label: 'Contributing',
+ items: [
+ {
+ autogenerate: { directory: 'contributing' },
+ },
+ ],
+ collapsed: true,
+ },
+ {
+ label: 'Troubleshooting',
+ items: [
+ {
+ autogenerate: { directory: 'troubleshooting' },
+ },
+ ],
+ collapsed: true,
+ },
+ {
+ label: 'Releases',
+ collapsed: true,
+ items: [
+ ...makeChangelogsSidebarLinks([
+ {
+ type: 'recent',
+ base: 'releases',
+ },
+ ]),
+ ],
+ },
+];
+
+export { sidebar as sidebarConfig };
diff --git a/docs/src/config/social.ts b/docs/src/config/social.ts
new file mode 100644
index 00000000000..8c7d416a004
--- /dev/null
+++ b/docs/src/config/social.ts
@@ -0,0 +1,23 @@
+import type { StarlightUserConfig } from '@astrojs/starlight/types';
+
+type SocialConfig = StarlightUserConfig['social'];
+
+const social: SocialConfig = [
+ {
+ icon: 'github',
+ label: 'GitHub',
+ href: 'https://github.com/invoke-ai/InvokeAI',
+ },
+ {
+ icon: 'discord',
+ label: 'Discord',
+ href: 'https://discord.gg/ZmtBAhwWhy',
+ },
+ {
+ icon: 'youtube',
+ label: 'YouTube',
+ href: 'https://www.youtube.com/@invokeai',
+ },
+];
+
+export { social as socialConfig };
diff --git a/docs/src/content.config.ts b/docs/src/content.config.ts
new file mode 100644
index 00000000000..5dbc874545d
--- /dev/null
+++ b/docs/src/content.config.ts
@@ -0,0 +1,22 @@
+import { defineCollection } from 'astro:content';
+import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
+import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
+
+import { changelogsLoader } from 'starlight-changelogs/loader';
+
+export const collections = {
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
+ i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
+ changelogs: defineCollection({
+ loader: changelogsLoader([
+ {
+ title: "Releases",
+ provider: 'github',
+ base: 'releases',
+ owner: 'invoke-ai',
+ repo: 'InvokeAI',
+ pagefind: false,
+ }
+ ]),
+ })
+};
diff --git a/docs/src/content/docs/assets/controlnets-parallax/city-canny.png b/docs/src/content/docs/assets/controlnets-parallax/city-canny.png
new file mode 100644
index 00000000000..f46ad56e1b5
Binary files /dev/null and b/docs/src/content/docs/assets/controlnets-parallax/city-canny.png differ
diff --git a/docs/src/content/docs/assets/controlnets-parallax/city-depth.png b/docs/src/content/docs/assets/controlnets-parallax/city-depth.png
new file mode 100644
index 00000000000..5ed305e8cd9
Binary files /dev/null and b/docs/src/content/docs/assets/controlnets-parallax/city-depth.png differ
diff --git a/docs/src/content/docs/assets/controlnets-parallax/city-og.png b/docs/src/content/docs/assets/controlnets-parallax/city-og.png
new file mode 100644
index 00000000000..25fd75cdf6e
Binary files /dev/null and b/docs/src/content/docs/assets/controlnets-parallax/city-og.png differ
diff --git a/docs/src/content/docs/assets/controlnets-parallax/city-ui-layers.png b/docs/src/content/docs/assets/controlnets-parallax/city-ui-layers.png
new file mode 100644
index 00000000000..6ddc0f74058
Binary files /dev/null and b/docs/src/content/docs/assets/controlnets-parallax/city-ui-layers.png differ
diff --git a/docs/src/content/docs/assets/invoke-web-server-1.png b/docs/src/content/docs/assets/invoke-web-server-1.png
new file mode 100644
index 00000000000..e1cf27a2176
Binary files /dev/null and b/docs/src/content/docs/assets/invoke-web-server-1.png differ
diff --git a/docs/src/content/docs/assets/invoke-webui-canvas.png b/docs/src/content/docs/assets/invoke-webui-canvas.png
new file mode 100644
index 00000000000..29dda3baf42
Binary files /dev/null and b/docs/src/content/docs/assets/invoke-webui-canvas.png differ
diff --git a/docs/src/content/docs/assets/splash-banner.png b/docs/src/content/docs/assets/splash-banner.png
new file mode 100644
index 00000000000..74d23f514d9
Binary files /dev/null and b/docs/src/content/docs/assets/splash-banner.png differ
diff --git a/docs/src/content/docs/concepts/diffusion.mdx b/docs/src/content/docs/concepts/diffusion.mdx
new file mode 100644
index 00000000000..27030b2af4a
--- /dev/null
+++ b/docs/src/content/docs/concepts/diffusion.mdx
@@ -0,0 +1,77 @@
+---
+title: Diffusion
+lastUpdated: 2026-02-20
+sidebar:
+ order: 5
+---
+
+import { Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
+
+Taking the time to understand the diffusion process will help you to understand how to more effectively use InvokeAI.
+
+## Image Space vs. Latent Space
+
+There are two main ways Stable Diffusion works — with images, and latents.
+
+
+
+ Represents images in pixel form that you look at. This is the final visual output you see.
+
+
+ Represents compressed inputs. It's in latent space that Stable Diffusion processes images.
+
+
+
+:::note[What is a VAE?]
+ A **VAE (Variational Auto Encoder)** is responsible for compressing and encoding inputs into *latent space*, as well as decoding outputs back into *image space*.
+:::
+
+## Core Components
+
+To fully understand the diffusion process, we need to understand a few more terms: **U-Net**, **CLIP**, and **conditioning**.
+
+
+
+ A model trained on a large number of latent images with known amounts of random noise added. The U-Net can be given a slightly noisy image and it will predict the pattern of noise needed to subtract from the image in order to recover the original.
+
+
+ **CLIP** is a model that tokenizes and encodes text into **conditioning**. This conditioning guides the model during the denoising steps to produce a new image.
+
+
+
+The U-Net and CLIP work together during the image generation process at each denoising step. The U-Net removes noise so that the result is similar to images in its training set, while CLIP guides the U-Net towards creating images that are most similar to your prompt.
+
+## The Generation Process
+
+
+
+ When you generate an image using text-to-image, multiple steps occur in latent space:
+
+
+ 1. **Noise Generation:** Random noise is generated at the chosen height and width. The noise's characteristics are dictated by the seed. This noise tensor is passed into latent space. We'll call this *noise A*.
+ 2. **Noise Prediction:** Using a model's U-Net, a noise predictor examines *noise A* and the words tokenized by CLIP from your prompt (conditioning). It generates its own noise tensor to predict what the final image might look like in latent space. We'll call this *noise B*.
+ 3. **Subtraction:** *Noise B* is subtracted from *noise A* in an attempt to create a latent image consistent with the prompt. This step is repeated for the number of sampler steps chosen.
+ 4. **Decoding:** The VAE decodes the final latent image from latent space into image space.
+
+
+
+ Image-to-image is a similar process, with only the first step being different:
+
+
+ 1. **Encoding & Adding Noise:** The input image is encoded from image space into latent space by the VAE. Noise is then added to the input latent image.
+ * **Denoising Strength** dictates how many noise steps are added, and the amount of noise added at each step.
+ * A strength of `0` means there are 0 steps and no noise added, resulting in an unchanged image.
+ * A strength of `1` results in the image being completely replaced with noise and a full set of denoising steps are performed.
+ 2. **Noise Prediction:** Using a model's U-Net, a noise predictor examines the noisy latent image and the conditioning from your prompt. It generates its own noise tensor to predict the final image.
+ 3. **Subtraction:** The predicted noise is subtracted from the current noise in an attempt to create a latent image consistent with the prompt. This step is repeated for the remaining sampler steps.
+ 4. **Decoding:** The VAE decodes the final latent image from latent space into image space.
+
+
+
+
+## Summary
+
+
+- A **Model** provides the CLIP prompt tokenizer, the VAE, and a U-Net (where noise prediction occurs given a prompt and initial noise tensor).
+- A **Noise Scheduler** (e.g. `DPM++ 2M Karras`) schedules the subtraction of noise from the latent image across the sampler steps chosen. Less noise is usually subtracted at higher sampler steps.
+
diff --git a/docs/src/content/docs/concepts/dynamic-prompting.mdx b/docs/src/content/docs/concepts/dynamic-prompting.mdx
new file mode 100644
index 00000000000..c345fea5fe4
--- /dev/null
+++ b/docs/src/content/docs/concepts/dynamic-prompting.mdx
@@ -0,0 +1,133 @@
+---
+title: Dynamic Prompting
+lastUpdated: 2026-03-30
+sidebar:
+ order: 4
+---
+
+import { Card, CardGrid, Steps, LinkCard } from '@astrojs/starlight/components';
+
+Dynamic prompting expands a single prompt into many prompt variations. It is useful for brainstorming, prompt exploration, and batch testing without rewriting the same prompt by hand.
+
+## Basic syntax
+
+Put alternatives inside braces and separate them with `|`.
+
+```text
+a {red|green|blue} balloon
+```
+
+This can expand into:
+
+```text
+a red balloon
+a green balloon
+a blue balloon
+```
+
+You can use more than one dynamic group in the same prompt:
+
+```text
+a {red|green} {balloon|kite}
+```
+
+That creates a set of prompt combinations such as `a red balloon`, `a red kite`, `a green balloon`, and `a green kite`.
+
+## Select more than one option with `$$`
+
+Prefix a group with a number and `$$` to choose multiple distinct options from the same set.
+
+```text
+portrait, {2$$rim light|fog|rain|neon reflections}
+```
+
+Possible results include:
+
+```text
+portrait, rim light, fog
+portrait, fog, rain
+portrait, rim light, neon reflections
+```
+
+This is useful when you want controlled variety without writing every combination by hand.
+
+## Random vs combinatorial expansion
+
+
+
+ Walks the possible prompt combinations systematically until `Max Prompts` is reached.
+
+
+ Samples prompt variations instead of enumerating every combination. A seed can make random expansion repeatable.
+
+
+
+InvokeAI supports both modes, but where you can choose them depends on the workflow.
+
+- In the current linear UI, dynamic prompt preview is driven from the positive prompt and currently follows the standard combinatorial expansion path.
+- In node and backend contexts, random and combinatorial generation are exposed more explicitly.
+
+## Max Prompts
+
+`Max Prompts` limits how many expanded prompts InvokeAI will generate.
+
+This matters because combinations grow quickly. For example:
+
+```text
+a {red|green|blue} balloon in {morning mist|golden hour|rain}
+```
+
+Even this small prompt already has nine possible combinations.
+
+:::tip[Start small]
+ Preview a handful of prompt variants first. Once the combinations look useful, increase `Max Prompts` for a larger batch.
+:::
+
+## Seed Behaviour
+
+In the current UI, the `Seed Behaviour` setting controls how seeds are reused across expanded prompts.
+
+
+
+ Uses one seed per iteration, so prompt variants in the same iteration share a seed. This is useful when you want to compare prompt wording more directly.
+
+
+ Uses a different seed for every generated image. This is useful when you want the widest possible variety.
+
+
+
+## Using dynamic prompting in the linear UI
+
+
+ 1. **Put dynamic prompt syntax in the positive prompt**
+
+ In the current linear UI, dynamic prompt expansion is driven from the positive prompt.
+
+ 2. **Open the preview**
+
+ Use `Show Dynamic Prompts` or the prompts preview to inspect the expanded list before you generate.
+
+ 3. **Set `Max Prompts`**
+
+ Keep the expansion under control before launching a large batch.
+
+ 4. **Choose the right seed behavior**
+
+ Use `Seed per Iteration` for easier comparison, or `Seed per Image` for more variety.
+
+ 5. **Generate a small batch first**
+
+ Sanity-check the combinations before scaling up.
+
+
+:::note[Current linear UI behavior]
+ The linear UI currently exposes `Max Prompts`, preview, and seed behavior. It does not expose a separate random-versus-combinatorial mode switch in the main positive prompt flow.
+:::
+
+## Tips
+
+- Keep each option group internally compatible.
+- Be careful with multiple groups, because the number of combinations grows quickly.
+- Review the expanded prompt list before launching a large batch.
+- Use dynamic prompting for variation, not to avoid thinking through the base prompt.
+- When one specific term needs more emphasis, use [Prompting Syntax](../prompt-syntax) instead of adding more dynamic groups.
diff --git a/docs/src/content/docs/concepts/image-generation.mdx b/docs/src/content/docs/concepts/image-generation.mdx
new file mode 100644
index 00000000000..6aa25f4b34c
--- /dev/null
+++ b/docs/src/content/docs/concepts/image-generation.mdx
@@ -0,0 +1,153 @@
+---
+title: Image Generation
+lastUpdated: 2026-03-30
+sidebar:
+ order: 1
+---
+
+import { Card, CardGrid, Steps, LinkCard } from '@astrojs/starlight/components';
+
+:::tip[New to image generation with AI?]
+ You're in the right place! This is a high-level walkthrough of some of the concepts and terms you'll see as you start using Invoke. Please note, this is not an exhaustive guide and may be out of date due to the rapidly changing nature of the space.
+:::
+
+## Using InvokeAI
+
+### Prompt Crafting
+
+Prompts are the basis of using InvokeAI, providing the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be.
+
+
+ To get started, here's an easy template to use for structuring your prompts:
+ **Subject, Style, Quality, Aesthetic**
+
+ - **Subject:** What your image will be about. E.g. “a futuristic city with trains”, “penguins floating on icebergs”, “friends sharing beers”.
+ - **Style:** The style or medium in which your image will be in. E.g. “photograph”, “pencil sketch”, “oil paints”, or “pop art”, “cubism”, “abstract”.
+ - **Quality:** A particular aspect or trait that you would like to see emphasized in your image. E.g. "award-winning", "featured in relevant set of high quality works", "professionally acclaimed". Many people often use "masterpiece".
+ - **Aesthetics:** The visual impact and design of the artwork. This can be colors, mood, lighting, setting, etc.
+
+
+There are two prompt boxes: **Positive Prompt** & **Negative Prompt**.
+
+- A **Positive Prompt** includes words you want the model to reference when creating an image.
+- A **Negative Prompt** is for anything you want the model to eliminate when creating an image. It doesn’t always interpret things exactly the way you would, but helps control the generation process. Always try to include a few terms - you can typically use lower quality image terms like “blurry” or “distorted” with good success.
+
+**Some example prompts you can try on your own:**
+
+- *A detailed oil painting of a tranquil forest at sunset with vibrant colors and soft, golden light filtering through the trees*
+- *friends sharing beers in a busy city, realistic colored pencil sketch, twilight, masterpiece, bright, lively*
+
+### Advanced Prompting
+
+
+
+
+
+
+
+### Generation Workflows
+
+Invoke offers a number of different workflows for interacting with models to produce images. Each is extremely powerful on its own, but together provide you an unparalleled way of producing high quality creative outputs that align with your vision.
+
+
+
+ Focuses on the key workflow of using a prompt to generate a new image. It includes other features that help control the generation process as well.
+
+
+ Provide an image as a reference (called the “initial image”), which provides more guidance around color and structure to the AI as it generates a new image.
+
+
+ An advanced AI-first image editing tool. Drag an image onto the canvas to regenerate elements, edit content or colors (**inpainting**), or extend the image with consistency and clarity (**outpainting**).
+
+
+
+### Improving Image Quality
+
+
+ 1. **Fine-tuning your prompt:**
+
+ The more specific you are, the closer the image will turn out to what is in your head. Adding more details in the Positive or Negative Prompt can help add or remove parts of the image. You can also use advanced techniques like upweighting and downweighting to control the influence of specific words. Learn more in the [Prompting Guide](../prompting-guide) and [Prompting Syntax](../prompt-syntax).
+
+ :::tip
+ If you're seeing poor results, try adding the things you don't like about the image to your negative prompt. E.g. *distorted, low quality, unrealistic, etc.*
+ :::
+
+ 2. **Explore different models:**
+
+ Other models can produce different results due to the data they've been trained on. Each model has specific language and settings it works best with; a model's documentation is your friend here. Play around with some and see what works best for you!
+
+ 3. **Increasing Steps:**
+
+ The number of steps used controls how much time the model is given to produce an image, and depends on the "Scheduler" used. More steps tends to mean better results, but will take longer. We recommend at least 30 steps for most.
+
+ 4. **Tweak and Iterate:**
+
+ Remember, it's best to change one thing at a time so you know what is working and what isn't. Sometimes you just need to try a new image, and other times using a new prompt might be the ticket.
+ *For testing, consider turning off the "random" Seed. Using the same seed with the same settings will produce the same image, which makes it the perfect way to learn exactly what your changes are doing.*
+
+ 5. **Explore Advanced Settings:**
+
+ InvokeAI has a full suite of tools available to allow you complete control over your image creation process. Check out our [features docs](../../features/gallery) if you want to learn more.
+
+
+## Terms & Concepts
+
+:::note
+ If you're interested in learning more, check out [this presentation](https://docs.google.com/presentation/d/1IO78i8oEXFTZ5peuHHYkVF-Y3e2M6iM5tCnc-YBfcCM/edit?usp=sharing) from one of our maintainers (@lstein).
+:::
+
+### Stable Diffusion
+
+Stable Diffusion is a deep learning, text-to-image model that is the foundation of the capabilities found in InvokeAI. Since the release of Stable Diffusion, there have been many subsequent models created based on Stable Diffusion that are designed to generate specific types of images.
+
+### Prompts
+
+Prompts provide the models directions on what to generate. As a general rule of thumb, the more detailed your prompt is, the better your result will be.
+
+### Models
+
+Models are the magic that power InvokeAI. These files represent the output of training a machine on understanding massive amounts of images - providing them with the capability to generate new images using just a text description of what you'd like to see.
+
+Invoke offers a simple way to download several different models upon installation, but many more can be discovered online, including at [civitai.com](https://civitai.com). Each model can produce a unique style of output, based on the images it was trained on.
+
+:::note
+ Models that contain "inpainting" in the name are designed for use with the inpainting feature of the Unified Canvas.
+:::
+
+### Schedulers & Steps
+
+**Schedulers** guide the process of removing noise (de-noising) from data. They determine:
+1. The number of steps to take to remove the noise.
+2. Whether the steps are random (stochastic) or predictable (deterministic).
+3. The specific method (algorithm) used for de-noising.
+
+**Steps** represent the number of de-noising iterations each generation goes through. Schedulers can be intricate and there's often a balance to strike between how quickly they can de-noise data and how well they can do it. It's typically advised to experiment with different schedulers to see which one gives the best results.
+
+### Additional Concepts
+
+
+
+ LoRAs are like a smaller, more focused version of models, intended to focus on training a better understanding of how a specific character, style, or concept looks.
+
+
+ Like LoRAs, embeddings assist with more easily prompting for certain characters, styles, or concepts. They are trained to update the relationship between a specific word (known as the "trigger") and the intended output.
+
+
+ ControlNets are neural network models that are able to extract key features from an existing image and use these features to guide the output of the image generation model.
+
+
+ A Variational Auto-Encoder (VAE) is an encode/decode model that translates the "latents" image produced during the image generation process to the large pixel images that we see.
+
+
diff --git a/docs/src/content/docs/concepts/models.mdx b/docs/src/content/docs/concepts/models.mdx
new file mode 100644
index 00000000000..3ebdf27c788
--- /dev/null
+++ b/docs/src/content/docs/concepts/models.mdx
@@ -0,0 +1,133 @@
+---
+title: Models
+sidebar:
+ order: 8
+---
+
+## Checkpoint and Diffusers Models
+
+The model checkpoint files (`*.ckpt`) are the Stable Diffusion "secret sauce". They are the product of training the AI on millions of captioned images gathered from multiple sources.
+
+Originally there was only a single Stable Diffusion weights file, which many people named `model.ckpt`.
+
+Today, there are thousands of models, fine tuned to excel at specific styles, genres, or themes.
+
+:::tip[Model Formats]
+ We also have two more popular model formats, both created by [HuggingFace](https://huggingface.co/):
+
+ - `safetensors`: Single file, like `.ckpt` files. Prevents malware from lurking in a model.
+ - `diffusers`: Splits the model components into separate files, allowing very fast loading.
+
+ InvokeAI supports all three formats.
+:::
+
+## Starter Models
+
+When you first start InvokeAI, you'll see a popup prompting you to install some starter models from the Model Manager. Click the `Starter Models` tab to see the list.
+
+You'll find a collection of popular and high-quality models available for easy download.
+
+Some models carry license terms that limit their use in commercial applications or on public servers. It's your responsibility to adhere to the license terms.
+
+## Other Models
+
+There are a few ways to install other models:
+
+- **URL or Local Path**: Provide the path to a model on your computer, or a direct link to the model. Some sites require you to use an API token to download models, which you can [set up in the config file]. You can also paste a HuggingFace Repo ID here directly — it is detected and routed to the HuggingFace installer automatically.
+- **HuggingFace**: Paste a HF Repo ID to install it. If there are multiple models in the repo, you'll get a list to choose from. Repo IDs look like this: `XpucT/Deliberate`. There is a copy button on each repo to copy the ID.
+- **Scan Folder**: Scan a local folder for models. You can install all of the detected models in one click.
+
+### Diffusers models in HF repo subfolders
+
+HuggingFace repos can be structured in any way. Some model authors include multiple models within the same folder.
+
+In this situation, you may need to provide some additional information to identify the model you want, by adding `:subfolder_name` to the repo ID.
+
+:::note[Example]
+ Say you have a repo ID `monster-labs/control_v1p_sd15_qrcode_monster`, and the model you want is inside the `v2` subfolder.
+
+ Add `:v2` to the repo ID and use that when installing the model: `monster-labs/control_v1p_sd15_qrcode_monster:v2`
+:::
+
+[set up in the config file]: ../../configuration/invokeai-yaml
+
+## Editing model metadata
+
+Every model has an editable **Source URL** field alongside its name and description. Use it to record where a model came from — for example a Civitai or HuggingFace page — independent of how it was originally installed. The URL is editable from the model's **Edit** view and appears as a clickable link in the model header once set. Models without a URL simply hide the field.
+
+This is purely metadata: the URL has no effect on loading and is not used to refresh or reinstall the model. It is mainly useful for going back to the model's documentation, license, or example prompts later.
+
+## Bulk actions in the Model Manager
+
+The Model Manager supports multi-selection for batch operations.
+
+- **Select multiple models** by clicking with **Ctrl** (Windows / Linux) or **Cmd** (macOS) held, or by using the checkboxes on each row. A sticky header at the top shows the current selection count and is always visible while you scroll.
+- Open the **Actions** dropdown for the selection. The available actions are:
+ - **Delete Models** — removes every selected model in a single confirmation step. Partial failures (e.g. permission issues) are reported per-model in the result toast.
+ - **Reidentify Models** — re-probes every selected model, updating fields that depend on the file contents (type, base, format, variant, etc.). This is the bulk version of the per-model reidentify action.
+
+:::caution[Reidentify resets custom settings]
+Reidentifying a model re-derives its configuration from the file on disk. Any custom settings you've adjusted on those models — default settings, descriptions, trigger phrases — may be overwritten. The confirmation modal warns you about this before running.
+:::
+
+Both actions handle partial failures: if some models succeed and others fail, the toast lists succeeded and failed counts and the list view updates immediately for the ones that worked.
+
+## Finding orphaned models
+
+If a model file is deleted or moved outside the Model Manager, its database entry sticks around. To find these orphaned entries:
+
+1. Open the Model Manager.
+2. Open the **type filter** dropdown and pick **Missing Files**.
+3. The list now shows only models whose files are no longer present on disk. Each one also displays a **Missing Files** badge in its row.
+
+Orphaned models are automatically excluded from selection dropdowns (main model, LoRA, VAE, etc.), so you cannot accidentally pick one for generation. Use the [bulk delete action](#bulk-actions-in-the-model-manager) to clean them out in one step.
+
+## Synchronizing orphaned model directories
+
+The **Missing Files** filter finds database records whose files are gone. InvokeAI also has a separate sync workflow for the opposite situation: model directories that still exist on disk but are not referenced in the database.
+
+This can happen after a failed import, a manual database edit, or deleting a model record while leaving files behind. The sync workflow scans the models directory for top-level folders containing model files with common model extensions, including `.safetensors`, `.ckpt`, `.pt`, `.pth`, `.bin`, `.onnx`, and `.gguf`.
+
+To review these directories:
+
+1. In multi-user mode, sign in as an administrator. In single-user mode, the Model Manager controls are available by default.
+2. Open the Model Manager.
+3. Click **Sync Models** to scan for orphaned model directories.
+4. Review each reported relative directory path, contained model files, and total size before deleting anything.
+
+:::caution[Deletion removes directories]
+Deleting an orphaned model directory removes the entire reported directory from disk. The server deletes it directly with recursive directory deletion, so make sure the directory contains only files you intend to remove.
+:::
+
+Only administrators can use this workflow in multi-user mode. The underlying API is `/api/v2/models/sync/orphaned`; API results also include the absolute path for each reported directory.
+
+## Exporting and Importing Model Settings
+
+Each installed model has an **Export Settings** and **Import Settings** action in the Model Manager. Use these to back up a model's configuration, move it to another install, or share a curated setup with someone else.
+
+### What gets exported
+
+The exported `.json` file captures the configuration you have set on the model, not the model weights themselves:
+
+- `default_settings` — steps, CFG / guidance, scheduler, dimensions, FP8 storage toggle, VAE precision, etc.
+- `trigger_phrases` — for LoRAs and similar.
+- `cpu_only` — for encoder-type models.
+- `name`, `description`, `source_url` — the model's identifying metadata.
+- `cover_image` — the model's thumbnail, embedded as a base64 data URL.
+
+Fields you have not set are omitted from the file. The format is forward and backward compatible: older clients ignore newer fields, and a file produced by a newer version still imports cleanly into an older one (it just skips the fields it does not understand).
+
+### Importing
+
+Importing applies the JSON to the currently selected model:
+
+- `default_settings`, `trigger_phrases`, `cpu_only`, `name`, `description`, and `source_url` are applied via the normal model update path. Any field that the target model type does not support (e.g. `cpu_only` on a model that has no such setting) is listed in a "skipped" toast — everything else still applies.
+- `cover_image` is uploaded and set as the model's thumbnail.
+
+Imports are validated before they run. The file is rejected if `source_url` is not an `http(s)://` URL or if `cover_image` is not a valid image data URL — so a malformed or hand-edited file cannot quietly poison a model's configuration.
+
+### Typical workflows
+
+- **Back up a model you've spent time tuning** so you can restore its settings after a reinstall, or roll back after experimenting.
+- **Copy settings between two installs of the same model** — e.g. between a desktop and a workstation.
+- **Share a curated setup** (name, description, thumbnail, default steps / CFG / scheduler, trigger phrases) for a model you have configured well.
diff --git a/docs/src/content/docs/concepts/nodes-workflows.mdx b/docs/src/content/docs/concepts/nodes-workflows.mdx
new file mode 100644
index 00000000000..019d999bb35
--- /dev/null
+++ b/docs/src/content/docs/concepts/nodes-workflows.mdx
@@ -0,0 +1,29 @@
+---
+title: Nodes and Workflows
+sidebar:
+ order: 7
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+## What are Nodes?
+
+A **Node** is simply a single operation that takes in inputs and returns outputs. Multiple nodes can be linked together to create more complex functionality. All InvokeAI features are added through nodes.
+
+With nodes, you can easily extend the image generation capabilities of InvokeAI and build workflows that suit your specific needs.
+
+### Anatomy of a Node
+
+Individual nodes are made up of the following:
+
+
+
+ Edge points on the **left side** of the node window where you connect outputs from other nodes.
+
+
+ Edge points on the **right side** of the node window where you connect to inputs on other nodes.
+
+
+ Various options which are either manually configured, or overridden by connecting an output from another node to the input.
+
+
diff --git a/docs/src/content/docs/concepts/parameters.mdx b/docs/src/content/docs/concepts/parameters.mdx
new file mode 100644
index 00000000000..c4882de919e
--- /dev/null
+++ b/docs/src/content/docs/concepts/parameters.mdx
@@ -0,0 +1,143 @@
+---
+title: Generation Parameters
+lastUpdated: 2026-02-20
+sidebar:
+ order: 6
+---
+
+import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
+
+# Sampler Convergence
+
+As features keep increasing, making the right choices for your needs can become increasingly difficult. What sampler to use? And for how many steps? Do you change the CFG value? Do you use prompt weighting? Do you allow variations?
+
+Even once you have a result, do you blend it with other images? Pass it through `img2img`? With what strength? Do you use inpainting to correct small details? Outpainting to extend cropped sections?
+
+The purpose of this series of documents is to help you better understand these tools, so you can make the best out of them. Feel free to contribute with your own findings!
+
+In this document, we will talk about **sampler convergence**.
+
+
+ Looking for a short version? Here is the summary:
+
+ - Results converge as steps (`-s`) are increased (except for `K_DPM_2_A` and `K_EULER_A`). Often at ≥ `-s100`, but may require ≥ `-s700`.
+ - Producing a batch of candidate images at low (`-s8` to `-s30`) step counts can save you hours of computation.
+ - `K_HEUN` and `K_DPM_2` converge in fewer steps (but are slower per step).
+ - `K_DPM_2_A` and `K_EULER_A` incorporate a lot of creativity and variability.
+
+
+## Sampler Performance Overview
+
+
+
+ *(Tested on M1 Max 64GB, 512x512, 3 sample average)*
+
+ | Sampler | it/s |
+ | :--- | :--- |
+ | `DDIM` | 1.89 |
+ | `PLMS` | 1.86 |
+ | `K_EULER` | 1.86 |
+ | `K_LMS` | **1.91** (Fastest) |
+ | `K_EULER_A` | 1.86 |
+ | `K_HEUN` | 0.95 *(Slower)* |
+ | `K_DPM_2` | 0.95 *(Slower)* |
+ | `K_DPM_2_A` | 0.95 *(Slower)* |
+
+
+
+ For most use cases, `K_LMS`, `K_HEUN` and `K_DPM_2` are the best choices.
+
+ While `K_HEUN` and `K_DPM_2` run half as fast, they tend to converge twice as quickly as `K_LMS`.
+
+ At very low steps (≤ `-s8`), `K_HEUN` and `K_DPM_2` are not recommended. Use `K_LMS` instead.
+
+ For high variability between steps, use `K_EULER_A` (which runs twice as fast as `K_DPM_2_A`).
+
+
+
+---
+
+## Sampler Results by Subject
+
+Let's start by choosing a prompt and using it with each of our 8 samplers, running it for 10, 20, 30, 40, 50 and 100 steps.
+
+### Anime
+> `"an anime girl" -W512 -H512 -C7.5 -S3031912972`
+
+
+
+Immediately, you can notice results tend to converge — that is, as `-s` (step) values increase, images look more and more similar until there comes a point where the image no longer changes.
+
+You can also notice how `DDIM` and `PLMS` eventually tend to converge to K-sampler results as steps are increased. Among K-samplers, `K_HEUN` and `K_DPM_2` seem to require the fewest steps to converge, and even at low step counts they are good indicators of the final result. Finally, `K_DPM_2_A` and `K_EULER_A` seem to do a bit of their own thing and don't keep much similarity with the rest of the samplers.
+
+### Nature
+Now, these results seem interesting, but do they hold for other topics? Let's try!
+
+> `"valley landscape wallpaper, d&d art, fantasy, painted, 4k, high detail, sharp focus, washed colors, elaborate excellent painted illustration" -W512 -H512 -C7.5 -S1458228930`
+
+
+
+With nature, you can see how initial results are even more indicative of the final result — more so than with characters/people. `K_HEUN` and `K_DPM_2` are again the quickest indicators, almost right from the start. Results also converge faster (e.g. `K_HEUN` converged at `-s21`).
+
+### Food
+> `"a hamburger with a bowl of french fries" -W512 -H512 -C7.5 -S4053222918`
+
+
+
+Again, `K_HEUN` and `K_DPM_2` take the fewest number of steps to be good indicators of the final result. `K_DPM_2_A` and `K_EULER_A` seem to incorporate a lot of creativity/variability, capable of producing rotten hamburgers, but also of adding lettuce to the mix. And they're the only samplers that produced an actual 'bowl of fries'!
+
+### Animals
+> `"grown tiger, full body" -W512 -H512 -C7.5 -S3721629802`
+
+
+
+`K_HEUN` and `K_DPM_2` once again require the least number of steps to be indicative of the final result (around `-s30`), while other samplers are still struggling with several tails or malformed back legs.
+
+It also takes longer to converge (for comparison, `K_HEUN` required around 150 steps to converge). This is normal, as producing human/animal faces/bodies is one of the things the model struggles the most with. For these topics, running for more steps will often increase coherence within the composition.
+
+### People
+> `"Ultra realistic photo, (Miranda Bloom-Kerr), young, stunning model, blue eyes, blond hair, beautiful face, intricate, highly detailed, smooth, art by artgerm and greg rutkowski and alphonse mucha, stained glass" -W512 -H512 -C7.5 -S2131956332`. *(This time, we will go up to 300 steps).*
+
+
+
+Observing the results, it again takes longer for all samplers to converge (`K_HEUN` took around 150 steps), but we can observe good indicative results much earlier (see: `K_HEUN`). Conversely, `DDIM` and `PLMS` are still undergoing moderate changes (see: lace around her neck), even at `-s300`.
+
+In fact, as we can see in this other experiment, some samplers can take 700+ steps to converge when generating people.
+
+
+
+Note also the point of convergence may not be the most desirable state (e.g. you might prefer an earlier version of the face that is more rounded), but it will probably be the most coherent regarding arms/hands/face attributes. You can always merge different images with a photo editing tool and pass it through `img2img` to smoothen the composition.
+
+---
+
+## Batch Generation Speedup
+
+This realization about convergence is very useful because it means you don't need to create a batch of 100 images (`-n100`) at `-s100` just to choose your favorite 2 or 3 images.
+
+You can produce the same 100 images at `-s10` to `-s30` using a K-sampler (since they converge faster), get a rough idea of the final result, choose your 2 or 3 favorite ones, and then run `-s100` on those specific images to polish details. This technique is **3-8x as quick**.
+
+:::tip[Time Savings Example]
+ Assuming 60 seconds per 100 steps:
+
+ - **Method A:** 60s * 100 images = **6000s** (100 images at `-s100`, manually picking 3 favorites). Total time: **1 hour and 40 minutes.**
+ - **Method B:** 6s * 100 images + 60s * 3 images = **780s** (100 images at `-s10`, manually picking 3 favorites, and running those 3 at `-s100` to polish details). Total time: **13 minutes.**
+:::
+
+## Three Key Takeaways
+
+Finally, it is relevant to mention that, in general, there are 3 important moments in the process of image formation as steps increase:
+
+
+1. **The Indicator Stage:**
+ The earliest point at which an image becomes a good indicator of the final result. This is useful for batch generation at low step values to preview outputs before committing to higher steps.
+2. **The Coherence Stage:**
+ The point at which an image becomes coherent, even if different from the final converged result. This is useful for low-step batch generation where quality is improved via other techniques (like inpainting) rather than raw step count.
+3. **The Convergence Stage:**
+ The point at which an image fully converges and stops changing.
+
+
+:::note[Workflow Dictates Strategy]
+ Remember that your workflow/strategy should define your optimal number of steps, even for the same prompt and seed. For example, if you seek full convergence, you may run `K_LMS` for `-s200`. However, running `K_LMS` for `-s20` (taking one-tenth the time) may perform just as well if your workflow includes adding small missing details via `img2img`.
+:::
+
+
diff --git a/docs/src/content/docs/concepts/prompt-syntax.mdx b/docs/src/content/docs/concepts/prompt-syntax.mdx
new file mode 100644
index 00000000000..5d26267c57a
--- /dev/null
+++ b/docs/src/content/docs/concepts/prompt-syntax.mdx
@@ -0,0 +1,138 @@
+---
+title: Prompting Syntax
+lastUpdated: 2026-03-30
+sidebar:
+ order: 3
+---
+
+import { Card, LinkCard, CardGrid } from '@astrojs/starlight/components';
+
+
+
+
+
+
+
+InvokeAI supports Compel-style prompt weighting and prompt functions for `SD 1.5` and `SDXL` text conditioning workflows. Recent model families, including `FLUX`, `Z-Image`, `CogView4`, and `Qwen Image`, bypass Compel and do not use the syntax documented on this page. This page documents syntax for those Compel-based workflows only. If you want general advice on writing better prompts, start with [Prompting Guide](../prompting-guide).
+
+:::note[Compatibility note]
+ If a weighted prompt seems to be ignored, check whether you are using an `SD 1.5` or `SDXL` workflow. Compel syntax on this page does not apply to newer model families such as `FLUX`, `Z-Image`, `CogView4`, and `Qwen Image`.
+:::
+
+## Quick reference
+
+
+ - Increase a single word: `trees+`
+ - Decrease a single word: `fog-`
+ - Weight a phrase: `(golden hour light)+`
+ - Use an exact numeric weight: `(cinematic lighting)1.25`
+ - Nest weights: `(portrait with (blue eyes)1.3)1.1`
+ - Blend prompts: `("portrait photo", "oil painting").blend(0.7, 0.3)`
+ - Conjoin clauses: `("red silk dress", "studio portrait", "soft rim light").and()`
+ - Escape literal parentheses: `colored pencil \(medium\)`
+
+
+## Attention weighting with `+` and `-`
+
+Append `+` to increase influence, or `-` to reduce it.
+
+```text
+freckles+
+background crowd-
+(soft rim light)++
+```
+
+Rules of thumb:
+
+- Single words can be weighted directly.
+- Multi-word phrases should be wrapped in parentheses.
+- Each additional `+` compounds upward.
+- Each additional `-` compounds downward in roughly 10% steps.
+
+:::tip[Start small]
+ One or two steps is usually enough. Extreme weighting can overpower the rest of the prompt.
+:::
+
+## Numeric weights
+
+Use numeric weights when you want precise control instead of repeated plus or minus markers.
+
+```text
+(cinematic lighting)1.25
+(background crowd)0.8
+(sharp focus)1.1
+```
+
+Guidelines:
+
+- `1` is neutral.
+- Values greater than `1` increase emphasis.
+- Values between `0` and `1` reduce emphasis.
+- Wrap the weighted phrase in parentheses.
+
+## Grouping and nesting
+
+You can group phrases and apply weight to the whole group, then nest another weighted phrase inside it.
+
+```text
+(portrait with (blue eyes)1.3)1.1
+```
+
+In this example, the outer group strengthens the whole phrase, and the inner group gives `blue eyes` even more emphasis.
+
+## Blend prompts with `.blend()`
+
+Use `.blend()` to mix the meaning of two or more prompts.
+
+```text
+("portrait photo, 85mm lens", "oil painting, visible brushstrokes").blend(0.7, 0.3)
+```
+
+This is most useful for combining concepts or styles that you want balanced deliberately.
+
+Tips:
+
+- Provide one weight for each prompt argument.
+- Keeping the weights near a total of `1` makes the result easier to reason about.
+- Quoted arguments are the safest choice, especially when the prompts contain commas.
+
+## Combine clauses with `.and()`
+
+Use `.and()` when you want separate prompt clauses encoded individually instead of as one long comma-separated sentence.
+
+```text
+("red silk dress", "studio portrait", "soft rim light").and()
+```
+
+This can behave differently from:
+
+```text
+red silk dress, studio portrait, soft rim light
+```
+
+If a normal prompt keeps collapsing ideas together, `.and()` is worth testing.
+
+## Escape literal parentheses
+
+Unescaped parentheses are treated as prompt syntax. If you want actual parentheses in the text, escape them with backslashes.
+
+```text
+colored pencil \(medium\)
+portrait \(realistic\) (high quality)1.2
+A bear \(with razor-sharp teeth\) in a forest
+```
+
+Use unescaped parentheses only when you mean grouping or weighting.
+
+## Related pages
+
+- For practical prompt-writing advice, read [Prompting Guide](../prompting-guide).
+- For prompt expansion and permutations, read [Dynamic Prompting](../dynamic-prompting).
diff --git a/docs/src/content/docs/concepts/prompting-guide.mdx b/docs/src/content/docs/concepts/prompting-guide.mdx
new file mode 100644
index 00000000000..3dbb27a9fbc
--- /dev/null
+++ b/docs/src/content/docs/concepts/prompting-guide.mdx
@@ -0,0 +1,180 @@
+---
+title: Prompting Guide
+lastUpdated: 2026-03-30
+sidebar:
+ order: 2
+---
+
+import { Card, CardGrid, Steps, LinkCard } from '@astrojs/starlight/components';
+
+
+
+
+
+
+
+Prompting in InvokeAI works best when you describe the image clearly, then refine only the parts that matter. This page focuses on practical prompt-writing habits.
+
+
+
+ Start with the main thing you want to see: a character, object, scene, or action.
+
+
+ Add the visual language: photograph, watercolor, oil painting, 3D render, anime illustration, and so on.
+
+
+ Describe the camera angle, framing, lighting, environment, color palette, or mood that will shape the image.
+
+
+ Add a few high-value quality cues such as fabric texture, shallow depth of field, natural skin texture, or painterly brushwork.
+
+
+
+A simple pattern that works well is:
+
+`subject, style or medium, lighting or composition, a few important details`
+
+Not every prompt needs every category. Start simple, then add detail only when the model needs more direction.
+
+## Positive and negative prompts
+
+
+
+ Use the positive prompt to describe what you want the model to create. Put the most important idea early and keep the wording concrete.
+
+
+ Use the negative prompt to remove recurring problems or unwanted traits. Keep it short and targeted instead of pasting a giant list into every generation.
+
+
+
+Good negative prompts usually name specific failure modes: `blurry`, `distorted hands`, `low detail`, `extra limbs`.
+
+:::tip[Negative prompts are strong]
+ A negative term can suppress nearby concepts too. If you negate something broad like `green` or `moss`, you may also weaken grass, foliage, or other related ideas.
+:::
+
+## A practical prompting workflow
+
+
+ 1. Start with the core image
+
+ Write the clearest version of the image you want before adding stylistic extras.
+
+ 2. Add style and composition
+
+ Once the subject is right, add medium, lens, lighting, mood, background, or framing details.
+
+ 3. Test with a fixed seed
+
+ When you are learning what a prompt change does, keep the seed stable so you can compare results directly.
+
+ 4. Change one thing at a time
+
+ If you add five new terms at once, you will not know which one helped.
+
+ 5. Escalate only when needed
+
+ If the result is close but one element is too weak or too strong, move to [Prompting Syntax](../prompt-syntax) for weighting. If you want lots of variations, use [Dynamic Prompting](../dynamic-prompting).
+
+
+Here is the same idea refined in stages:
+
+```text
+portrait of a woman
+
+portrait of a woman, studio photograph, soft key light
+
+portrait of a woman, studio photograph, soft key light, 85mm lens, shallow depth of field, natural skin texture
+```
+
+## Write for the model you are using
+
+The same prompt can behave very differently across models.
+
+- Photo-oriented models respond well to camera, lens, lighting, and texture language.
+- Illustration models often respond better to medium, art direction, and shape language.
+- Specialty models may expect specific trigger words, subjects, or styles from their own model card.
+- If a prompt works beautifully on one model and poorly on another, that does not always mean the prompt is bad. The model may just speak a different visual language.
+
+## When advanced syntax helps
+
+Reach for advanced syntax when a normal comma-separated prompt is almost right, but you need more control.
+
+- Use [Prompting Syntax](../prompt-syntax) when one term needs more or less influence.
+- Use `.blend()` when you want to mix concepts or styles deliberately.
+- Use `.and()` when you want separate prompt clauses encoded individually.
+- Use [Dynamic Prompting](../dynamic-prompting) when you want many prompt variations from one template.
+
+## Common mistakes
+
+- Packing too many unrelated ideas into one prompt.
+- Using long generic quality-word lists before you know the base prompt works.
+- Treating the negative prompt as a trash can for every bad outcome.
+- Expecting identical behavior across models, schedulers, and workflows.
+- Changing prompt, model, seed, and settings all at once while troubleshooting.
+
+## Example prompts
+
+### Photographic portrait
+
+**Positive prompt**
+
+```text
+editorial portrait of a woman in a charcoal coat, studio photograph, soft key light, subtle rim light, 85mm lens, shallow depth of field, natural skin texture
+```
+
+**Negative prompt**
+
+```text
+blurry, low detail, waxy skin, extra fingers
+```
+
+### Environment concept art
+
+**Positive prompt**
+
+```text
+ancient stone temple built into a cliffside, fantasy concept art, misty sunrise, towering scale, moss-covered stairs, cinematic atmosphere
+```
+
+**Negative prompt**
+
+```text
+flat lighting, low contrast, muddy details
+```
+
+### Product-style render
+
+**Positive prompt**
+
+```text
+sleek ceramic teapot on a matte stone surface, product photography, clean studio lighting, soft shadow, high detail, minimal background
+```
+
+**Negative prompt**
+
+```text
+cluttered background, distortion, duplicate objects
+```
+
+### Stylized illustration
+
+**Positive prompt**
+
+```text
+fox courier crossing a rainy city street, storybook illustration, bold shapes, glowing shop signs, reflective pavement, warm and cool color contrast
+```
+
+**Negative prompt**
+
+```text
+photorealistic, dull colors, low detail
+```
diff --git a/docs/src/content/docs/configuration/assets/cuda-sysmem-fallback.png b/docs/src/content/docs/configuration/assets/cuda-sysmem-fallback.png
new file mode 100755
index 00000000000..f79e007f871
Binary files /dev/null and b/docs/src/content/docs/configuration/assets/cuda-sysmem-fallback.png differ
diff --git a/docs/src/content/docs/configuration/docker.mdx b/docs/src/content/docs/configuration/docker.mdx
new file mode 100644
index 00000000000..591ed38d3ae
--- /dev/null
+++ b/docs/src/content/docs/configuration/docker.mdx
@@ -0,0 +1,95 @@
+---
+title: Docker
+---
+
+import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'
+
+import SystemRequirementsLink from '@components/SystemRequirmentsLink.astro'
+
+
+
+:::note[Operating Systems and GPU Support]
+
+
+ Docker Desktop on Windows [includes GPU support](https://www.docker.com/blog/wsl-2-gpu-support-for-docker-desktop-on-nvidia-gpus/).
+
+
+ Docker can not access the GPU on macOS, so your generation speeds will be slow. Use the [launcher](../../start-here/installation) instead.
+
+
+ Configure Docker to access your machine's GPU.
+ Follow the [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) or [AMD](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) documentation.
+
+
+:::
+
+## TL;DR
+
+Ensure your Docker setup is able to use your GPU. Then:
+
+```bash
+docker run --runtime=nvidia --gpus=all --publish 9090:9090 ghcr.io/invoke-ai/invokeai
+```
+
+Once the container starts up, open [http://localhost:9090](http://localhost:9090) in your browser, install some models, and start generating.
+
+## Build-It-Yourself
+
+All the docker materials are located inside the [docker](https://github.com/invoke-ai/InvokeAI/tree/main/docker) directory in the Git repo.
+
+```bash
+cd docker
+cp .env.sample .env
+docker compose up
+```
+
+We also ship the `run.sh` convenience script. See the `docker/README.md` file for detailed instructions on how to customize the docker setup to your needs.
+
+### Prerequisites
+
+#### Install [Docker](https://github.com/santisbon/guides#docker)
+
+On the [Docker Desktop app](https://docs.docker.com/get-docker/), go to `Preferences` -> `Resources` -> `Advanced`. Increase the CPUs and Memory to avoid this [Issue](https://github.com/invoke-ai/InvokeAI/issues/342). You may need to increase Swap and Disk image size too.
+
+### Setup
+
+Set up your environment variables. In the `docker` directory, make a copy of `.env.sample` and name it `.env`. Make changes as necessary.
+
+Any environment variables supported by InvokeAI can be set here - please see the [configuration docs](/configuration/invokeai-yaml/) for further detail.
+
+At the very least, you might want to set the `INVOKEAI_ROOT` environment variable
+to point to the location where you wish to store your InvokeAI models, configuration, and outputs.
+
+| Environment Variable | Default value | 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` | | InvokeAI will work without it, but some of the integrations with HuggingFace (like downloading from models from private repositories) may not work |
+| `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. |
+
+#### Build the Image
+
+Use the standard `docker compose build` command from within the `docker` directory.
+
+If using an AMD GPU:
+a: set the `GPU_DRIVER=rocm` environment variable in `docker-compose.yml` and continue using `docker compose build` as usual, or
+b: set `GPU_DRIVER=rocm` in the `.env` file and use the `build.sh` script, provided for convenience
+
+#### Run the Container
+
+Use the standard `docker compose up` command, and generally the `docker compose` [CLI](https://docs.docker.com/compose/reference/) as usual.
+
+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)
+
+## Troubleshooting / FAQ
+
+
+ "I am running Windows under WSL2, and am seeing a 'no such file or directory' error."
+
+ Your `docker-entrypoint.sh` might have has Windows (CRLF) line endings, depending how you cloned the repository.
+ To solve this, change the line endings in the `docker-entrypoint.sh` file to `LF`. You can do this in VSCode
+ (`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.
+ For more information on this issue, see [Docker Desktop documentation](https://docs.docker.com/desktop/troubleshoot/topics/#avoid-unexpected-syntax-errors-use-unix-style-line-endings-for-files-in-containers)
+
+
diff --git a/docs/src/content/docs/configuration/fp8-storage.mdx b/docs/src/content/docs/configuration/fp8-storage.mdx
new file mode 100644
index 00000000000..799821b079e
--- /dev/null
+++ b/docs/src/content/docs/configuration/fp8-storage.mdx
@@ -0,0 +1,128 @@
+---
+title: FP8 Storage
+sidebar:
+ order: 3
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+FP8 Storage cuts a model's VRAM footprint roughly in half by keeping weights on the GPU in 8-bit floating-point format (`float8_e4m3fn`). During inference, each layer's weights are cast on-the-fly back up to the compute precision (FP16/BF16), then cast back to FP8 after the forward pass — so quality is largely preserved.
+
+It pairs well with [Low-VRAM mode](/configuration/low-vram-mode/): low-VRAM mode streams layers between RAM and VRAM, while FP8 Storage shrinks the layers themselves.
+
+:::caution[For full precision models only]
+FP8 Storage only applies to **full precision** checkpoints (FP16 / BF16 / FP32). It is **silently a no-op** for already-quantized formats — **GGUF**, **NF4**, and **int8** checkpoints carry their own storage precision and the loader returns a different module type that the FP8 layer cast does not touch. If your model is already quantized, the toggle has no effect; use the full-precision variant of the model if you want to enable FP8 Storage.
+:::
+
+## Requirements
+
+- **Nvidia GPU on Windows or Linux.** FP8 Storage uses CUDA tensor types and is silently disabled on CPU and MPS.
+- **CUDA 12.x and recent PyTorch.** The `float8_e4m3fn` dtype was added in PyTorch 2.1 — InvokeAI's bundled versions satisfy this.
+
+There is no hardware requirement for FP8 *compute* — InvokeAI casts back to FP16/BF16 for math. This means FP8 Storage works on GPUs that do not natively support FP8 matmul (e.g. RTX 30-series), at a small per-step throughput cost.
+
+## Hardware support tiers
+
+InvokeAI's FP8 path stores weights in FP8 and casts them back to BF16/FP16 on each forward pass via its own `register_forward_pre_hook` / `register_forward_hook` wrappers (the same skip list as diffusers' `apply_layerwise_casting`, but applied to every `nn.Module` — including diffusers `ModelMixin` subclasses — so it composes correctly with InvokeAI's `CustomLinear` and partial loading). The practical benefit of toggling FP8 Storage depends on what your GPU can do natively. There are three tiers:
+
+### RTX 30-series and older Ampere workstation cards — VRAM win only
+
+The toggle works as advertised: the UNet / transformer drops by roughly 50% on the GPU. Per-step latency is the same or marginally slower because every forward pass adds an FP8 → BF16 cast on entry and a BF16 → FP8 cast on exit. This is the **largest target group**: 3090 owners squeezing FLUX into 24 GB benefit the most.
+
+### RTX 40-series, RTX 50-series, and Hopper — VRAM win today, compute win possible later
+
+These GPUs have native FP8 tensor cores. The toggle still buys you the same ~50% VRAM reduction today, because the forward pass still runs in BF16 — the hook casts weights back up to compute precision before each layer. If InvokeAI later wires up a true FP8 matmul path (e.g. via `torchao`), the same toggle will *also* unlock compute speedups on this hardware. Until then, treat the benefit as "VRAM only, same as Ampere".
+
+### Older CUDA cards — still a VRAM win
+
+`float8_e4m3fn` is a pure storage dtype in PyTorch and works on any CUDA device, so pre-Ampere cards (GTX 16-series, RTX 20-series, etc.) get the same ~50% VRAM reduction as Ampere. There are no native FP8 tensor cores on these GPUs, so the throughput trade-off is the same as on the 30-series: cast in, compute in BF16/FP16, cast back out.
+
+### MPS and CPU — no-op
+
+FP8 Storage is silently disabled on anything that is not CUDA. On CPU PyTorch *technically* supports FP8 dtypes, but the cast operations are software-emulated and end up costing more than the memory savings buy back, so InvokeAI gates the entire path on `device.type == "cuda"`. If you toggle it on CPU or MPS, the loader skips the cast and returns the model unchanged with no log line.
+
+## Enabling FP8 Storage
+
+FP8 Storage is a **per-model setting**, configured from the Model Manager:
+
+
+1. Open the **Model Manager**.
+2. Select a model (Main, ControlNet, or T2I-Adapter).
+3. Under **Default Settings**, toggle **FP8 Storage (Save VRAM)**.
+4. Click **Save**.
+
+
+The setting takes effect on the next load. If the model is already in the cache, InvokeAI evicts the cached copy automatically so the new setting applies — even if a generation is currently using the model (the eviction is deferred until the generation finishes).
+
+:::tip[When to enable]
+ Enable FP8 Storage on large models that don't fit comfortably in VRAM — FLUX dev/Klein, large SDXL checkpoints, ControlNet-XL adapters. For smaller SD1 / SD2 models, the savings are negligible and not worth the small precision trade-off.
+:::
+
+## What FP8 Storage applies to
+
+FP8 Storage is **only** applied to layers where the precision trade-off is acceptable:
+
+| Model type | FP8 applied? |
+| ----------------------------- | -------------------------------------- |
+| Main models (SD1, SD2, SDXL) | Yes |
+| FLUX.1 / FLUX.2 Klein | Yes |
+| ControlNet, T2I-Adapter | Yes |
+| VAE | No — visible decode-quality regression |
+| Text encoders, tokenizers | No — small models, no benefit |
+| Z-Image (any variant) | No — dtype mismatch with skipped layers|
+| LoRA, ControlLoRA | No — patched into base, not run alone |
+
+Within a supported model, **norm layers, position/patch embeddings, and `proj_in`/`proj_out` are skipped** so precision-sensitive tiny learned scalars (e.g. FLUX `RMSNorm.scale`) aren't crushed to FP8. This mirrors the diffusers default skip list.
+
+## Quality trade-offs
+
+FP8 Storage is **near-lossless** for most workloads because:
+
+- Norms and embeddings (the precision-sensitive layers) are skipped.
+- The actual matmul still happens in FP16/BF16 — FP8 is only the on-GPU storage format.
+
+That said, some artifacts have been reported on:
+
+- **VAEs** — never cast (the toggle has no effect on VAE submodels).
+- **Heavy LoRA stacks** — patching is unaffected, but very precision-sensitive LoRAs may show slight drift. Compare a side-by-side if your workflow depends on subtle LoRA behavior.
+
+If you see unexpected quality regressions, disable FP8 Storage on the affected model and re-run.
+
+## Combining with Low-VRAM mode
+
+**FP8 + partial loading**: fully supported. FP8 Storage shrinks the layers; partial loading streams them between RAM and VRAM as needed. Use both on tight VRAM budgets.
+
+(For why FP8 Storage doesn't stack on top of GGUF / NF4 / int8 checkpoints, see the callout at the top of this page.)
+
+## Troubleshooting
+
+### "I toggled FP8 Storage but VRAM usage didn't change"
+
+The cache eviction is immediate for idle models, but **deferred until the next unlock** if the model is mid-generation. Wait for the current generation to finish, then start a new one — the next load will use the new setting.
+
+If VRAM still hasn't dropped:
+
+- Check the InvokeAI log for `FP8 layerwise casting enabled for `. If the line isn't there, the model is on the exclusion list (VAE, text encoder, Z-Image, LoRA — see table above).
+- Confirm you are on CUDA. FP8 Storage is silently disabled on CPU and MPS.
+
+### Quality regression on a specific model
+
+Disable FP8 Storage for that model in Model Manager and reload. If quality is restored, the model has FP8-sensitive layers that fall outside the default skip list. Please open an issue with the model name and a side-by-side comparison.
+
+### "RuntimeError: ... float8_e4m3fn ..."
+
+You're on a PyTorch version that predates FP8 support. Reinstall InvokeAI using the official launcher — the bundled torch version supports FP8.
+
+### Reporting an FP8 issue
+
+If FP8 Storage misbehaves — crash, quality regression, OOM that shouldn't happen — please [open a GitHub issue](https://github.com/invoke-ai/InvokeAI/issues/new/choose) and include:
+
+- **What you did**: the workflow / generation step that triggered the problem, and whether it reproduces every time.
+- **Model**: exact name and variant (e.g. "FLUX.2 Klein 9B Diffusers", "SDXL Base 1.0 single-file"), and whether the file is a full-precision checkpoint or already quantized (GGUF / NF4 / int8).
+- **LoRAs**: whether any LoRAs (or ControlLoRAs) are stacked on the model, and how many.
+- **Other toggles**: Low-VRAM mode on/off, any `cpu_only` text encoder setting, configured VRAM limit.
+- **GPU**: model and VRAM size (e.g. "RTX 3090 24 GB", "RTX 4070 Ti 12 GB").
+- **OS**: Windows or Linux, plus driver / CUDA version if you have it.
+- **Logs**: the InvokeAI log around the failure — in particular the `FP8 layerwise casting enabled for ` line (or its absence) and any traceback.
+
+A side-by-side image comparison (FP8 on vs. FP8 off, same seed) is extremely useful for quality regressions.
diff --git a/docs/src/content/docs/configuration/invokeai-yaml.mdx b/docs/src/content/docs/configuration/invokeai-yaml.mdx
new file mode 100644
index 00000000000..987c8eb98a2
--- /dev/null
+++ b/docs/src/content/docs/configuration/invokeai-yaml.mdx
@@ -0,0 +1,212 @@
+---
+title: YAML Config
+sidebar:
+ order: 1
+---
+
+import { FileTree } from '@astrojs/starlight/components'
+import SettingsDocs from '@lib/components/SettingsDocs.astro'
+
+Runtime settings, including the location of files and directories, memory usage, and performance, are managed via the `invokeai.yaml` config file or environment variables. A subset of settings may be set via commandline arguments.
+
+Settings sources are used in this order:
+
+- CLI args
+- Environment variables
+- `invokeai.yaml` settings
+- Fallback: defaults
+
+### InvokeAI Root Directory
+
+On startup, InvokeAI searches for its "root" directory. This is the directory that contains models, images, the database, and so on. It also contains a configuration file called `invokeai.yaml`.
+
+
+ - models/
+ - outputs/
+ - databases/
+ - workflow_thumbnails/
+ - style_presets/
+ - nodes/
+ - configs/
+ - invokeai.example.yaml
+ - **invokeai.yaml**
+
+
+InvokeAI searches for the root directory in this order:
+
+1. The `--root ` CLI arg.
+2. The environment variable INVOKEAI_ROOT.
+3. The directory containing the currently active virtual environment.
+4. Fallback: a directory in the current user's home directory named `invokeai`.
+
+### InvokeAI Configuration File
+
+Inside the root directory, we read settings from the `invokeai.yaml` file.
+
+It has two sections - one for internal use and one for user settings:
+
+```yaml
+# Internal metadata - do not edit:
+schema_version: 4.0.2
+
+# Put user settings here - see https://invoke.ai/configuration/invokeai-yaml/:
+host: 0.0.0.0 # serve the app on your local network
+models_dir: D:\invokeai\models # store models on an external drive
+precision: float16 # always use fp16 precision
+```
+
+The settings in this file will override the defaults. You only need
+to change this file if the default for a particular setting doesn't
+work for you.
+
+You'll find an example file next to `invokeai.yaml` that shows the default values.
+
+Some settings, like [Model Marketplace API Keys], require the YAML
+to be formatted correctly. Here is a [basic guide to YAML files].
+
+#### Custom Config File Location
+
+You can use any config file with the `--config` CLI arg. Pass in the path to the `invokeai.yaml` file you want to use.
+
+Note that environment variables will trump any settings in the config file.
+
+#### Model Marketplace API Keys
+
+Some model marketplaces require an API key to download models. You can provide a URL pattern and appropriate token in your `invokeai.yaml` file to provide that API key.
+
+The pattern can be any valid regex (you may need to surround the pattern with quotes):
+
+```yaml
+remote_api_tokens:
+ # Any URL containing `models.com` will automatically use `your_models_com_token`
+ - url_regex: models.com
+ token: your_models_com_token
+ # Any URL matching this contrived regex will use `some_other_token`
+ - url_regex: '^[a-z]{3}whatever.*\.com$'
+ token: some_other_token
+```
+
+The provided token will be added as a `Bearer` token to the network requests to download the model files. As far as we know, this works for all model marketplaces that require authorization.
+
+:::tip[Hugging face Models]
+If you get an error when installing a HF model using a URL instead of repo id, you may need to [set up a HF API token](https://huggingface.co/settings/tokens) and add an entry for it under `remote_api_tokens`. Use `huggingface.co` for `url_regex`.
+:::
+
+#### Model Hashing
+
+Models are hashed during installation, providing a stable identifier for models across all platforms. Hashing is a one-time operation.
+
+```yaml
+hashing_algorithm: blake3_single # default value
+```
+
+You might want to change this setting, depending on your system:
+
+- `blake3_single` (default): Single-threaded - best for spinning HDDs, still OK for SSDs
+- `blake3_multi`: Parallelized, memory-mapped implementation - best for SSDs, terrible for spinning disks
+- `random`: Skip hashing entirely - fastest but of course no hash
+
+During the first startup after upgrading to v4, all of your models will be hashed. This can take a few minutes.
+
+Most common algorithms are supported, like `md5`, `sha256`, and `sha512`. These are typically much, much slower than either of the BLAKE3 variants.
+
+#### Path Settings
+
+These options set the paths of various directories and files used by InvokeAI. Any user-defined paths should be absolute paths.
+
+#### Image Subfolder Strategy
+
+By default, generated images are stored in a single flat directory under `outputs/images/`. The `image_subfolder_strategy` setting lets you organize newly-created images into subfolders automatically. You can edit this setting in `invokeai.yaml` or, as an admin user, in the Settings panel.
+
+```yaml
+image_subfolder_strategy: flat # default value
+```
+
+Available strategies:
+
+| Strategy | Example Path | Description |
+| -------- | -------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| `flat` | `outputs/images/abc123.png` | Store images directly in the images directory. |
+| `date` | `outputs/images/2026/03/17/abc123.png` | Organize images by creation date. |
+| `type` | `outputs/images/general/abc123.png` | Organize images by image category. |
+| `hash` | `outputs/images/ab/abc123.png` | Use the first two characters of the image UUID for filesystem performance with large collections. |
+
+Changing this setting only affects newly-created images. Existing images remain in their current locations.
+
+#### Logging
+
+Several different log handler destinations are available, and multiple destinations are supported by providing a list:
+
+```yaml
+log_handlers:
+ - console
+ - syslog=localhost
+ - file=/var/log/invokeai.log
+```
+
+- `console` is the default. It prints log messages to the command-line window from which InvokeAI was launched.
+
+- `syslog` is only available on Linux and Macintosh systems. It uses
+ the operating system's "syslog" facility to write log file entries
+ locally or to a remote logging machine. `syslog` offers a variety
+ of configuration options:
+
+```yaml
+syslog=/dev/log` - log to the /dev/log device
+syslog=localhost` - log to the network logger running on the local machine
+syslog=localhost:512` - same as above, but using a non-standard port
+syslog=fredserver,facility=LOG_USER,socktype=SOCK_DRAM`
+- Log to LAN-connected server "fredserver" using the facility LOG_USER and datagram packets.
+```
+
+- `http` can be used to log to a remote web server. The server must be
+ properly configured to receive and act on log messages. The option
+ accepts the URL to the web server, and a `method` argument
+ indicating whether the message should be submitted using the GET or
+ POST method.
+
+```yaml
+http=http://my.server/path/to/logger,method=POST
+```
+
+The `log_format` option provides several alternative formats:
+
+- `color` - default format providing time, date and a message, using text colors to distinguish different log severities
+- `plain` - same as above, but monochrome text only
+- `syslog` - the log level and error message only, allowing the syslog system to attach the time and date
+- `legacy` - a format similar to the one used by the legacy 2.3 InvokeAI releases.
+
+### Environment Variables
+
+All settings may be set via environment variables by prefixing `INVOKEAI_`
+to the variable name. For example, `INVOKEAI_HOST` would set the `host`
+setting.
+
+For non-primitive values, pass a JSON-encoded string:
+
+```sh
+export INVOKEAI_REMOTE_API_TOKENS='[{"url_regex":"modelmarketplace", "token": "12345"}]'
+```
+
+We suggest using `invokeai.yaml`, as it is more user-friendly.
+
+### CLI Args
+
+A subset of settings may be specified using CLI args:
+
+- `--root`: specify the root directory
+- `--config`: override the default `invokeai.yaml` file location
+
+### Low-VRAM Mode
+
+See the [Low-VRAM mode docs][low-vram] for details on enabling this feature.
+
+### All Settings
+
+The full settings reference is below. Additional explanations for selected settings appear earlier on this page.
+
+
+
+[basic guide to yaml files]: https://circleci.com/blog/what-is-yaml-a-beginner-s-guide/
+[Model Marketplace API Keys]: #model-marketplace-api-keys
+[low-vram]: /configuration/low-vram-mode
diff --git a/docs/src/content/docs/configuration/low-vram-mode.mdx b/docs/src/content/docs/configuration/low-vram-mode.mdx
new file mode 100644
index 00000000000..fa15cef8735
--- /dev/null
+++ b/docs/src/content/docs/configuration/low-vram-mode.mdx
@@ -0,0 +1,182 @@
+---
+title: Low-VRAM mode
+sidebar:
+ order: 2
+---
+
+As of v5.6.0, Invoke has a low-VRAM mode. It works on systems with dedicated GPUs (Nvidia GPUs on Windows/Linux and AMD GPUs on Linux).
+
+This allows you to generate even if your GPU doesn't have enough VRAM to hold full models. Most users should be able to run even the beefiest models - like the ~24GB unquantised FLUX dev model.
+
+## Enabling Low-VRAM mode
+
+Low-VRAM mode is **enabled by default** via the `enable_partial_loading: true` setting in `invokeai.yaml`. No action is required to turn it on.
+
+**Windows users should also [disable the Nvidia sysmem fallback](#disabling-nvidia-sysmem-fallback-windows-only)**.
+
+It is possible to fine-tune the settings for best performance or if you still get out-of-memory errors (OOMs).
+
+If you want to disable partial loading (e.g. on systems with plenty of VRAM where full loading is faster), add this line to your `invokeai.yaml` and restart Invoke:
+
+```yaml
+enable_partial_loading: false
+```
+
+:::tip[How to find `invokeai.yaml`]
+ The `invokeai.yaml` configuration file lives in your install directory. To access it, run the **Invoke Community Edition** launcher and click the install location. This will open your install directory in a file explorer window.
+
+ You'll see `invokeai.yaml` there and can edit it with any text editor. After making changes, restart Invoke.
+
+ If you don't see `invokeai.yaml`, launch Invoke once. It will create the file on its first startup.
+:::
+
+## Details and fine-tuning
+
+Low-VRAM mode involves 4 features, each of which can be configured or fine-tuned:
+
+- Partial model loading (`enable_partial_loading`)
+- PyTorch CUDA allocator config (`pytorch_cuda_alloc_conf`)
+- Dynamic RAM and VRAM cache sizes (`max_cache_ram_gb`, `max_cache_vram_gb`)
+- Working memory (`device_working_mem_gb`)
+- Keeping a RAM weight copy (`keep_ram_copy_of_weights`)
+
+Read on to learn about these features and understand how to fine-tune them for your system and use-cases.
+
+### Partial model loading
+
+Invoke's partial model loading works by streaming model "layers" between RAM and VRAM as they are needed.
+
+When an operation needs layers that are not in VRAM, but there isn't enough room to load them, inactive layers are offloaded to RAM to make room.
+
+#### Enabling partial model loading
+
+Partial model loading is enabled by default. The corresponding setting in `invokeai.yaml` is:
+
+```yaml
+enable_partial_loading: true
+```
+
+Set it to `false` to disable partial loading.
+
+### PyTorch CUDA allocator config
+
+The PyTorch CUDA allocator's behavior can be configured using the `pytorch_cuda_alloc_conf` config. Tuning the allocator configuration can help to reduce the peak reserved VRAM. The optimal configuration is dependent on many factors (e.g. device type, VRAM, CUDA driver version, etc.), but switching from PyTorch's native allocator to using CUDA's built-in allocator works well on many systems. To try this, add the following line to your `invokeai.yaml` file:
+
+```yaml
+pytorch_cuda_alloc_conf: "backend:cudaMallocAsync"
+```
+
+A more complete explanation of the available configuration options is [here](https://pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf).
+
+### Dynamic RAM and VRAM cache sizes
+
+Loading models from disk is slow and can be a major bottleneck for performance. Invoke uses two model caches - RAM and VRAM - to reduce loading from disk to a minimum.
+
+By default, Invoke manages these caches' sizes dynamically for best performance.
+
+#### Fine-tuning cache sizes
+
+Prior to v5.6.0, the cache sizes were static, and for best performance, many users needed to manually fine-tune the `ram` and `vram` settings in `invokeai.yaml`.
+
+As of v5.6.0, the caches are dynamically sized. The `ram` and `vram` settings are no longer used, and new settings are added to configure the cache.
+
+**Most users will not need to fine-tune the cache sizes.**
+
+But, if your GPU has enough VRAM to hold models fully, you might get a perf boost by manually setting the cache sizes in `invokeai.yaml`:
+
+```yaml
+# The default max cache RAM size is logged on InvokeAI startup. It is determined based on your system RAM / VRAM.
+# You can override the default value by setting `max_cache_ram_gb`.
+# Increasing `max_cache_ram_gb` will increase the amount of RAM used to cache inactive models, resulting in faster model
+# reloads for the cached models.
+# As an example, if your system has 32GB of RAM and no other heavy processes, setting the `max_cache_ram_gb` to 28GB
+# might be a good value to achieve aggressive model caching.
+max_cache_ram_gb: 28
+
+# The default max cache VRAM size is adjusted dynamically based on the amount of available VRAM (taking into
+# consideration the VRAM used by other processes).
+# You can override the default value by setting `max_cache_vram_gb`.
+# CAUTION: Most users should not manually set this value. See warning below.
+max_cache_vram_gb: 16
+```
+
+:::caution[Max safe value for `max_cache_vram_gb`]
+ Most users should not manually configure the `max_cache_vram_gb`. This configuration value takes precedence over the `device_working_mem_gb` and any operations that explicitly reserve additional working memory (e.g. VAE decode). As such, manually configuring it increases the likelihood of encountering out-of-memory errors.
+
+ For users who wish to configure `max_cache_vram_gb`, the max safe value can be determined by subtracting `device_working_mem_gb` from your GPU's VRAM. As described below, the default for `device_working_mem_gb` is 3GB.
+
+ For example, if you have a 12GB GPU, the max safe value for `max_cache_vram_gb` is `12GB - 3GB = 9GB`.
+
+ If you had increased `device_working_mem_gb` to 4GB, then the max safe value for `max_cache_vram_gb` is `12GB - 4GB = 8GB`.
+
+ Most users who override `max_cache_vram_gb` are doing so because they wish to use significantly less VRAM, and should be setting `max_cache_vram_gb` to a value significantly less than the 'max safe value'.
+:::
+
+### Working memory
+
+Invoke cannot use _all_ of your VRAM for model caching and loading. It requires some VRAM to use as working memory for various operations.
+
+Invoke reserves 3GB VRAM as working memory by default, which is enough for most use-cases. However, it is possible to fine-tune this setting if you still get OOMs.
+
+#### Fine-tuning working memory
+
+You can increase the working memory size in `invokeai.yaml` to prevent OOMs:
+
+```yaml
+# The default is 3GB - bump it up to 4GB to prevent OOMs.
+device_working_mem_gb: 4
+```
+
+:::tip[Operations may request more working memory]
+ For some operations, we can determine VRAM requirements in advance and allocate additional working memory to prevent OOMs.
+
+ VAE decoding is one such operation. This operation converts the generation process's output into an image. For large image outputs, this might use more than the default working memory size of 3GB.
+
+ During this decoding step, Invoke calculates how much VRAM will be required to decode and requests that much VRAM from the model manager. If the amount exceeds the working memory size, the model manager will offload cached model layers from VRAM until there's enough VRAM to decode.
+
+ Once decoding completes, the model manager "reclaims" the extra VRAM allocated as working memory for future model loading operations.
+:::
+
+### Keeping a RAM weight copy
+
+Invoke has the option of keeping a RAM copy of all model weights, even when they are loaded onto the GPU. This optimization is _on_ by default, and enables faster model switching and LoRA patching. Disabling this feature will reduce the average RAM load while running Invoke (peak RAM likely won't change), at the cost of slower model switching and LoRA patching. If you have limited RAM, you can disable this optimization:
+
+```yaml
+# Set to false to reduce the average RAM usage at the cost of slower model switching and LoRA patching.
+keep_ram_copy_of_weights: false
+```
+
+### Disabling Nvidia sysmem fallback (Windows only)
+
+On Windows, Nvidia GPUs are able to use system RAM when their VRAM fills up via **sysmem fallback**. While it sounds like a good idea on the surface, in practice it causes massive slowdowns during generation.
+
+It is strongly suggested to disable this feature:
+
+- Open the **NVIDIA Control Panel** app.
+- Expand **3D Settings** on the left panel.
+- Click **Manage 3D Settings** in the left panel.
+- Find **CUDA - Sysmem Fallback Policy** in the right panel and set it to **Prefer No Sysmem Fallback**.
+
+
+
+:::tip[Invoke does the same thing, but better]
+ If the sysmem fallback feature sounds familiar, that's because Invoke's partial model loading strategy is conceptually very similar - use VRAM when there's room, else fall back to RAM.
+
+ Unfortunately, the Nvidia implementation is not optimized for applications like Invoke and does more harm than good.
+:::
+
+## Troubleshooting
+
+### Windows page file
+
+Invoke has high virtual memory (a.k.a. 'committed memory') requirements. This can cause issues on Windows if the page file size limits are hit. (See this issue for the technical details on why this happens: https://github.com/invoke-ai/InvokeAI/issues/7563).
+
+If you run out of page file space, InvokeAI may crash. Often, these crashes will happen with one of the following errors:
+
+- InvokeAI exits with Windows error code `3221225477`
+- InvokeAI crashes without an error, but `eventvwr.msc` reveals an error with code `0xc0000005` (the hex equivalent of `3221225477`)
+
+If you are running out of page file space, try the following solutions:
+
+- Make sure that you have sufficient disk space for the page file to grow. Watch your disk usage as Invoke runs. If it climbs near 100% leading up to the crash, then this is very likely the source of the issue. Clear out some disk space to resolve the issue.
+- Make sure that your page file is set to "System managed size" (this is the default) rather than a custom size. Under the "System managed size" policy, the page file will grow dynamically as needed.
diff --git a/docs/src/content/docs/configuration/patchmatch.mdx b/docs/src/content/docs/configuration/patchmatch.mdx
new file mode 100644
index 00000000000..a91e1a8f8b7
--- /dev/null
+++ b/docs/src/content/docs/configuration/patchmatch.mdx
@@ -0,0 +1,126 @@
+---
+title: Patchmatch
+---
+
+import { Tabs, TabItem, Steps } from '@astrojs/starlight/components'
+
+PatchMatch is an algorithm used to infill images. It can greatly improve outpainting results. PyPatchMatch is a python wrapper around a C++ implementation of the algorithm.
+
+It uses the image data around the target area as a reference to generate new image data of a similar character and quality.
+
+## Why Use PatchMatch
+
+In the context of image generation, "outpainting" refers to filling in a transparent area using AI-generated image data. But the AI can't generate without some initial data. We need to first fill in the transparent area with _something_.
+
+The first step in "outpainting" then, is to fill in the transparent area with something. Generally, you get better results when that initial infill resembles the rest of the image.
+
+Because PatchMatch generates image data so similar to the rest of the image, it works very well as the first step in outpainting, typically producing better results than other infill methods supported by Invoke (e.g. LaMA, cv2 infill, random tiles).
+
+### Performance Caveat
+
+PatchMatch is CPU-bound, and the amount of time it takes increases proportionally as the infill area increases. While the numbers certainly vary depending on system specs, you can expect a noticeable slowdown once you start infilling areas around 512x512 pixels. 1024x1024 pixels can take several seconds to infill.
+
+## Installation
+
+Unfortunately, installation can be somewhat challenging, as it requires some things that `pip` cannot install for you.
+
+
+ 1. Ensure you have the necessary dependencies installed for your system (see below).
+
+
+
+ You're in luck! On Windows platforms PyPatchMatch will install automatically on Windows systems with no extra intervention.
+
+
+ You need to have opencv installed so that pypatchmatch can be built:
+
+ ```bash
+ brew install opencv
+ ```
+
+ The next time you start `invoke`, after successfully installing opencv, pypatchmatch will be built.
+
+
+ Prior to installing PyPatchMatch, you need to take the following steps:
+
+
+
+
+ 1. Install the `build-essential` tools:
+
+ ```sh
+ sudo apt update # Update package lists
+ sudo apt install build-essential
+ ```
+
+ 2. Install `opencv`:
+
+ ```sh
+ sudo apt install python3-opencv libopencv-dev
+ ```
+
+ 3. Activate the environment you use for invokeai, either with `conda` or with a virtual environment.
+
+
+
+
+ 1. Install the `base-devel` package:
+
+ ```sh
+ sudo pacman -Syu
+ sudo pacman -S --needed base-devel
+ ```
+
+ 2. Install `opencv`, `blas`, and required dependencies:
+
+ ```sh
+ sudo pacman -S opencv blas fmt glew vtk hdf5
+ ```
+
+ or for CUDA support
+
+ ```sh
+ sudo pacman -S opencv-cuda blas fmt glew vtk hdf5
+ ```
+
+ 3. Fix the naming of the `opencv` package configuration file:
+
+ ```sh
+ cd /usr/lib/pkgconfig/
+ ln -sf opencv4.pc opencv.pc
+ ```
+
+
+
+
+
+
+ 2. Install pypatchmatch:
+
+ ```sh
+ pip install pypatchmatch
+ ```
+
+ 3. Confirm that pypatchmatch is installed. At the command-line prompt enter `python`, and then at the `>>>` line type `from patchmatch import patch_match`: It should look like the following:
+
+ ```py
+ Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] on linux
+ Type "help", "copyright", "credits" or "license" for more information.
+ >>> from patchmatch import patch_match
+ Compiling and loading c extensions from "/home/lstein/Projects/InvokeAI/.invokeai-env/src/pypatchmatch/patchmatch".
+ rm -rf build/obj libpatchmatch.so
+ mkdir: created directory 'build/obj'
+ mkdir: created directory 'build/obj/csrc/'
+ [dep] csrc/masked_image.cpp ...
+ [dep] csrc/nnf.cpp ...
+ [dep] csrc/inpaint.cpp ...
+ [dep] csrc/pyinterface.cpp ...
+ [CC] csrc/pyinterface.cpp ...
+ [CC] csrc/inpaint.cpp ...
+ [CC] csrc/nnf.cpp ...
+ [CC] csrc/masked_image.cpp ...
+ [link] libpatchmatch.so ...
+ ```
+
+ If you're not seeing any errors, you're ready to go!
+
diff --git a/docs/src/content/docs/contributing/code-of-conduct.md b/docs/src/content/docs/contributing/code-of-conduct.md
new file mode 100644
index 00000000000..8ada3a81b9b
--- /dev/null
+++ b/docs/src/content/docs/contributing/code-of-conduct.md
@@ -0,0 +1,130 @@
+---
+title: Code of Conduct
+---
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported to the community leaders responsible for enforcement
+at https://github.com/invoke-ai/InvokeAI/issues. All complaints will
+be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/docs/src/content/docs/contributing/contributors.md b/docs/src/content/docs/contributing/contributors.md
new file mode 100644
index 00000000000..eb57feb295e
--- /dev/null
+++ b/docs/src/content/docs/contributing/contributors.md
@@ -0,0 +1,54 @@
+---
+title: Contributors
+---
+
+We thank [all contributors](https://github.com/invoke-ai/InvokeAI/graphs/contributors) for their time and hard work!
+
+## Original Author
+
+- [Lincoln D. Stein](mailto:lincoln.stein@gmail.com)
+
+## Current Core Team
+
+- [@lstein](https://github.com/lstein) (Lincoln Stein) - Co-maintainer
+- [@blessedcoolant](https://github.com/blessedcoolant) - Co-maintainer
+- [@hipsterusername](https://github.com/hipsterusername) (Kent Keirsey) - Co-maintainer, CEO, Positive Vibes
+- [@psychedelicious](https://github.com/psychedelicious) (Spencer Mabrito) - Web Team Leader
+- [@joshistoast](https://github.com/joshistoast) (Josh Corbett) - Web Development
+- [@cheerio](https://github.com/cheerio) (Mary Rogers) - Lead Engineer & Web App Development
+- [@ebr](https://github.com/ebr) (Eugene Brodsky) - Cloud/DevOps/Software engineer; your friendly neighbourhood cluster-autoscaler
+- [@sunija](https://github.com/sunija) - Standalone version
+- [@brandon](https://github.com/brandon) (Brandon Rising) - Platform, Infrastructure, Backend Systems
+- [@ryanjdick](https://github.com/ryanjdick) (Ryan Dick) - Machine Learning & Training
+- [@JPPhoto](https://github.com/JPPhoto) - Core image generation nodes
+- [@dunkeroni](https://github.com/dunkeroni) - Image generation backend
+- [@SkunkWorxDark](https://github.com/SkunkWorxDark) - Image generation backend
+- [@glimmerleaf](https://github.com/glimmerleaf) (Devon Hopkins) - Community Wizard
+- [@gogurt](https://github.com/gogurt) enjoyer - Discord moderator and end user support
+- [@whosawhatsis](https://github.com/whosawhatsis) - Discord moderator and end user support
+- [@dwringer](https://github.com/dwringer) - Discord moderator and end user support
+- [@526christian](https://github.com/526christian) - Discord moderator and end user support
+- [@harvester62](https://github.com/harvester62) - Discord moderator and end user support
+
+## Honored Team Alumni
+
+- [@StAlKeR7779](https://github.com/StAlKeR7779) (Sergey Borisov) - Torch stack, ONNX, model management, optimization
+- [@damian0815](https://github.com/damian0815) - Attention Systems and Compel Maintainer
+- [@netsvetaev](https://github.com/netsvetaev) (Artur) - Localization support
+- [@Kyle0654](https://github.com/Kyle0654) (Kyle Schouviller) - Node Architect and General Backend Wizard
+- [@tildebyte](https://github.com/tildebyte) - Installation and configuration
+- [@mauwii](https://github.com/mauwii) (Matthias Wilde) - Installation, release, continuous integration
+- [@chainchompa](https://github.com/chainchompa) (Jennifer Player) - Web Development & Chain-Chomping
+- [@millu](https://github.com/millu) (Millun Atluri) - Community Wizard, Documentation, Node-wrangler,
+- [@genomancer](https://github.com/genomancer) (Gregg Helt) - Controlnet support
+- [@keturn](https://github.com/keturn) (Kevin Turner) - Diffusers
+
+## Original CompVis (Stable Diffusion) Authors
+
+- [Robin Rombach](https://github.com/rromb)
+- [Patrick von Platen](https://github.com/patrickvonplaten)
+- [ablattmann](https://github.com/ablattmann)
+- [Patrick Esser](https://github.com/pesser)
+- [owenvincent](https://github.com/owenvincent)
+- [apolinario](https://github.com/apolinario)
+- [Charles Packer](https://github.com/cpacker)
diff --git a/docs/src/content/docs/contributing/external-providers.md b/docs/src/content/docs/contributing/external-providers.md
new file mode 100644
index 00000000000..13dfd2df5b3
--- /dev/null
+++ b/docs/src/content/docs/contributing/external-providers.md
@@ -0,0 +1,131 @@
+---
+title: External Provider Integration
+---
+
+This guide covers:
+
+1. Adding a new **external model** (most common; existing provider).
+2. Adding a brand-new **external provider** (adapter + config + UI wiring).
+
+## 1) Add a New External Model (Existing Provider)
+
+For provider-backed models (for example, OpenAI or Gemini), the source of truth is
+`invokeai/backend/model_manager/starter_models.py`.
+
+### Required model fields
+
+Define a `StarterModel` with:
+
+- `base=BaseModelType.External`
+- `type=ModelType.ExternalImageGenerator`
+- `format=ModelFormat.ExternalApi`
+- `source="external:///"`
+- `name`, `description`
+- `capabilities=ExternalModelCapabilities(...)`
+- optional `default_settings=ExternalApiModelDefaultSettings(...)`
+
+Example:
+
+```python
+new_external_model = StarterModel(
+ name="Provider Model Name",
+ base=BaseModelType.External,
+ source="external://openai/my-model-id",
+ description=(
+ "Provider model (external API). "
+ "Requires a configured OpenAI API key and may incur provider usage costs."
+ ),
+ type=ModelType.ExternalImageGenerator,
+ format=ModelFormat.ExternalApi,
+ capabilities=ExternalModelCapabilities(
+ modes=["txt2img", "img2img", "inpaint"],
+ supports_negative_prompt=False,
+ supports_seed=False,
+ supports_guidance=False,
+ supports_steps=False,
+ supports_reference_images=True,
+ max_images_per_request=4,
+ ),
+ default_settings=ExternalApiModelDefaultSettings(
+ width=1024,
+ height=1024,
+ num_images=1,
+ ),
+)
+```
+
+Then append it to `STARTER_MODELS`.
+
+### Required description text
+
+External starter model descriptions must clearly state:
+
+- an API key is required
+- usage may incur provider-side costs
+
+### Capabilities must be accurate
+
+These flags directly control UI visibility and request payload fields:
+
+- `supports_negative_prompt`
+- `supports_seed`
+- `supports_guidance`
+- `supports_steps`
+- `supports_reference_images`
+
+`supports_steps` is especially important: if `False`, steps are hidden for that model and `steps` is sent as `null`.
+
+### Source string stability
+
+Starter overrides are matched by `source` (`external://provider/model-id`). Keep this stable:
+
+- runtime capability/default overrides depend on it
+- installation detection in starter-model APIs depends on it
+
+`STARTER_MODELS` enforces unique `source` values with an assertion.
+
+### Install behavior notes
+
+- External starter models are managed in **External Providers** setup (not the regular Starter Models tab).
+- External starter models auto-install when a provider is configured.
+- Removing a provider API key removes installed external models for that provider.
+
+## 2) Credentials and Config
+
+External provider API keys are stored separately from `invokeai.yaml`:
+
+- default file: `~/invokeai/api_keys.yaml`
+- resolved path: `/api_keys.yaml`
+
+Non-secret provider settings (for example base URL overrides) stay in `invokeai.yaml`.
+
+Environment variables are still supported, e.g.:
+
+- `INVOKEAI_EXTERNAL_GEMINI_API_KEY`
+- `INVOKEAI_EXTERNAL_OPENAI_API_KEY`
+
+## 3) Add a New Provider (Only If Needed)
+
+If your model uses a provider that is not already integrated:
+
+1. Add config fields in `invokeai/app/services/config/config_default.py`
+ `external__api_key` and optional `external__base_url`.
+2. Add provider field mapping in `invokeai/app/api/routers/app_info.py`
+ (`EXTERNAL_PROVIDER_FIELDS`).
+3. Implement provider adapter in `invokeai/app/services/external_generation/providers/`
+ by subclassing `ExternalProvider`.
+4. Register the provider in `invokeai/app/api/dependencies.py` when building
+ `ExternalGenerationService`.
+5. Add starter model entries using `source="external:///"`.
+6. Optional UI ordering tweak:
+ `invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ExternalProviders/ExternalProvidersForm.tsx`
+ (`PROVIDER_SORT_ORDER`).
+
+## 4) Optional Manual Installation
+
+You can also install external models directly via:
+
+`POST /api/v2/models/install?source=external:///`
+
+If omitted, `path`, `source`, and `hash` are auto-populated for external model configs.
+Set capabilities conservatively; the external generation service enforces capability checks at runtime.
diff --git a/docs/src/content/docs/contributing/index.md b/docs/src/content/docs/contributing/index.md
new file mode 100644
index 00000000000..e4da6da1746
--- /dev/null
+++ b/docs/src/content/docs/contributing/index.md
@@ -0,0 +1,56 @@
+---
+title: Contributing to InvokeAI
+sidebar:
+ order: 1
+---
+
+Invoke originated as a project built by the community, and that vision carries forward today as we aim to build the best pro-grade tools available. We work together to incorporate the latest in AI/ML research, making these tools available in over 20 languages to artists and creatives around the world as part of our fully permissive OSS project designed for individual users to self-host and use.
+
+We welcome contributions, whether features, bug fixes, code cleanup, testing, code reviews, documentation or translation. Please check in with us before diving in to code to ensure your work aligns with our vision.
+
+## Development
+
+If you’d like to help with development, please see our [development guide](/development/).
+
+**New Contributors:** If you’re unfamiliar with contributing to open source projects, take a look at our [new contributor guide](/contributing/new-contributor-guide/).
+
+## Nodes
+
+If you’d like to add a Node, please see our [nodes contribution guide](/development/guides/creating-nodes/).
+
+## Support and Triaging
+
+Helping support other users in [Discord](https://discord.gg/ZmtBAhwWhy) and on Github are valuable forms of contribution that we greatly appreciate.
+
+We receive many issues and requests for help from users. We're limited in bandwidth relative to our the user base, so providing answers to questions or helping identify causes of issues is very helpful. By doing this, you enable us to spend time on the highest priority work.
+
+## Documentation
+
+If you’d like to help with documentation, please see our [contributing guide](/contributing/).
+
+## Translation
+
+If you'd like to help with translation, please see our [translation guide](/contributing/translations/).
+
+## Tutorials
+
+Please reach out to @hipsterusername on [Discord](https://discord.gg/ZmtBAhwWhy) to help create tutorials for InvokeAI.
+
+## Contributors
+
+This project is a combined effort of dedicated people from across the world. [Check out the list of all these amazing people](/contributing/contributors/). We thank them for their time, hard work and effort.
+
+## Code of Conduct
+
+The InvokeAI community is a welcoming place, and we want your help in maintaining that. Please review our [Code of Conduct](/contributing/code-of-conduct/) to learn more - it's essential to maintaining a respectful and inclusive environment.
+
+By making a contribution to this project, you certify that:
+
+1. The contribution was created in whole or in part by you and you have the right to submit it under the open-source license indicated in this project’s GitHub repository; or
+2. The contribution is based upon previous work that, to the best of your knowledge, is covered under an appropriate open-source license and you have the right under that license to submit that work with modifications, whether created in whole or in part by you, under the same open-source license (unless you are permitted to submit under a different license); or
+3. The contribution was provided directly to you by some other person who certified (1) or (2) and you have not modified it; or
+4. You understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information you submit with it, including your sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open-source license(s) involved.
+
+This disclaimer is not a license and does not grant any rights or permissions. You must obtain necessary permissions and licenses, including from third parties, before contributing to this project.
+
+This disclaimer is provided "as is" without warranty of any kind, whether expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, or non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the contribution or the use or other dealings in the contribution.
diff --git a/docs/src/content/docs/contributing/new-contributor-guide.mdx b/docs/src/content/docs/contributing/new-contributor-guide.mdx
new file mode 100644
index 00000000000..d5d29a36d0a
--- /dev/null
+++ b/docs/src/content/docs/contributing/new-contributor-guide.mdx
@@ -0,0 +1,105 @@
+---
+title: New Contributor Guide
+lastUpdated: 2026-02-19
+---
+
+import { Steps, LinkCard } from '@astrojs/starlight/components';
+
+If you're a new contributor to InvokeAI or Open Source Projects, this is the guide for you.
+
+## New Contributor Checklist
+
+
+ 1. Set up your local development environment & fork of InvokAI by following [the steps outlined here](../../development/setup/dev-environment/#initial-setup)
+
+ 2. Set up your local tooling with [this guide](/development/). Feel free to skip this step if you already have tooling you're comfortable with.
+
+ 3. Familiarize yourself with [Git](https://www.atlassian.com/git) & our project structure by reading through the [development documentation](/development/)
+
+ 4. Join the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord
+
+ 5. Choose an issue to work on! This can be achieved by asking in the #dev-chat channel, tackling a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) or finding an item on the [roadmap](https://github.com/orgs/invoke-ai/projects/7). If nothing in any of those places catches your eye, feel free to work on something of interest to you!
+
+ 6. Make your first Pull Request with the guide below
+
+ 7. Happy development! Don't be afraid to ask for help - we're happy to help you contribute!
+
+
+## How do I make a contribution?
+
+Never made an open source contribution before? Wondering how contributions work in our project? Here's a quick rundown!
+
+Before starting these steps, ensure you have your local environment [configured for development](/development/setup/dev-environment/).
+
+
+ 1. Find a [good first issue](https://github.com/invoke-ai/InvokeAI/contribute) that you are interested in addressing or a feature that you would like to add. Then, reach out to our team in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord to ensure you are setup for success.
+
+ 2. Fork the [InvokeAI](https://github.com/invoke-ai/InvokeAI) repository to your GitHub profile. This means that you will have a copy of the repository under **your-GitHub-username/InvokeAI**.
+
+ 3. Clone the repository to your local machine using:
+
+ ```bash
+ git clone https://github.com/your-GitHub-username/InvokeAI.git
+ ```
+
+ If you're unfamiliar with using Git through the commandline, [GitHub Desktop](https://desktop.github.com) is a easy-to-use alternative with a UI. You can do all the same steps listed here, but through the interface. 4. Create a new branch for your fix using:
+
+ ```bash
+ git checkout -b branch-name-here
+ ```
+
+ 5. Make the appropriate changes for the issue you are trying to address or the feature that you want to add.
+
+ 6. Add the file contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index:
+
+ ```bash
+ git add -A
+ ```
+
+ 7. Store the contents of the index with a descriptive message.
+
+ ```bash
+ git commit -m "Insert a short message of the changes made here"
+ ```
+
+ 8. Push the changes to the remote repository using
+
+ ```bash
+ git push origin branch-name-here
+ ```
+
+ 9. Submit a pull request to the **main** branch of the InvokeAI repository. If you're not sure how to, [follow this guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
+
+ 10. Title the pull request with a short description of the changes made and the issue or bug number associated with your change. For example, you can title an issue like so "Added more log outputting to resolve #1234".
+
+ 11. In the description of the pull request, explain the changes that you made, any issues you think exist with the pull request you made, and any questions you have for the maintainer. It's OK if your pull request is not perfect (no pull request is), the reviewer will be able to help you fix any problems and improve it!
+
+ 12. Wait for the pull request to be reviewed by other collaborators.
+
+ 13. Make changes to the pull request if the reviewer(s) recommend them.
+
+ 14. Celebrate your success after your pull request is merged!
+
+
+
+
+:::tip[Best Practices]
+
+- Keep your pull requests small. Smaller pull requests are more likely to be accepted and merged.
+- Comments! Commenting your code helps reviewers easily understand your contribution.
+- Use Python and Typescript’s typing systems, and consider using an editor with [LSP](https://microsoft.github.io/language-server-protocol/) support to streamline development.
+- Make all communications public. This ensure knowledge is shared with the whole community.
+
+:::
+
+## **Where can I go for help?**
+
+If you need help, you can ask questions in the [#dev-chat](https://discord.com/channels/1020123559063990373/1049495067846524939) channel of the Discord.
+
+For frontend related work, **@pyschedelicious** is the best person to reach out to.
+
+For backend related work, please reach out to **@blessedcoolant**, **@lstein**, **@StAlKeR7779** or **@pyschedelicious**.
diff --git a/docs/src/content/docs/contributing/translations.md b/docs/src/content/docs/contributing/translations.md
new file mode 100644
index 00000000000..53532312cb7
--- /dev/null
+++ b/docs/src/content/docs/contributing/translations.md
@@ -0,0 +1,21 @@
+---
+title: Translations
+---
+
+InvokeAI uses [Weblate](https://weblate.org/) for translation. Weblate is a FOSS project providing a scalable translation service. Weblate automates the tedious parts of managing translation of a growing project, and the service is generously provided at no cost to FOSS projects like InvokeAI.
+
+## Contributing
+
+If you'd like to contribute by adding or updating a translation, please visit our [Weblate project](https://hosted.weblate.org/engage/invokeai/). You'll need to sign in with your GitHub account (a number of other accounts are supported, including Google).
+
+Once signed in, select a language and then the Web UI component. From here you can Browse and Translate strings from English to your chosen language. Zen mode offers a simpler translation experience.
+
+Your changes will be attributed to you in the automated PR process; you don't need to do anything else.
+
+## Help & Questions
+
+Please check Weblate's [documentation](https://docs.weblate.org/en/latest/index.html) or ping @Harvestor on [Discord](https://discord.com/channels/1020123559063990373/1049495067846524939) if you have any questions.
+
+## Thanks
+
+Thanks to the InvokeAI community for their efforts to translate the project!
diff --git a/docs/src/content/docs/development/Architecture/assets/resize_invocation.png b/docs/src/content/docs/development/Architecture/assets/resize_invocation.png
new file mode 100644
index 00000000000..a78f8eb86a3
Binary files /dev/null and b/docs/src/content/docs/development/Architecture/assets/resize_invocation.png differ
diff --git a/docs/src/content/docs/development/Architecture/assets/resize_node_editor.png b/docs/src/content/docs/development/Architecture/assets/resize_node_editor.png
new file mode 100644
index 00000000000..d121ba1aa6d
Binary files /dev/null and b/docs/src/content/docs/development/Architecture/assets/resize_node_editor.png differ
diff --git a/docs/src/content/docs/development/Architecture/invocations.mdx b/docs/src/content/docs/development/Architecture/invocations.mdx
new file mode 100644
index 00000000000..08ebdda6a93
--- /dev/null
+++ b/docs/src/content/docs/development/Architecture/invocations.mdx
@@ -0,0 +1,425 @@
+---
+title: Invocations
+lastUpdated: 2026-02-18
+---
+
+import { FileTree, Code, Steps } from '@astrojs/starlight/components'
+
+# Nodes
+
+Features in InvokeAI are added in the form of modular nodes systems called
+**Invocations**.
+
+An Invocation is simply a single operation that takes in some inputs and gives
+out some outputs. We can then chain multiple Invocations together to create more
+complex functionality.
+
+## Invocations Directory
+
+InvokeAI Nodes can be found in the `invokeai/app/invocations` directory. These
+can be used as examples to create your own nodes.
+
+New nodes should be added to a subfolder in `nodes` direction found at the root
+level of the InvokeAI installation location. Nodes added to this folder will be
+able to be used upon application startup.
+
+Example `nodes` subfolder structure:
+
+
+ - nodes
+ - `__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
+
+
+Each node folder must have an `__init__.py` file that imports its nodes. Only
+nodes imported in the `__init__.py` file are loaded. See the README in the nodes
+folder for more examples:
+
+```py title="__init__.py"
+from .cool_node import ResizeInvocation
+````
+
+## Creating A New Invocation
+
+In order to understand the process of creating a new Invocation, let us actually
+create one.
+
+In our example, let us create an Invocation that will take in an image, resize
+it and output the resized image.
+
+The first set of things we need to do when creating a new Invocation are -
+
+
+ 1. Create a new class that derives from a predefined parent class called `BaseInvocation`.
+ 2. Every Invocation must have a `docstring` that describes what this Invocation does.
+ 3. While not strictly required, we suggest every invocation class name ends in "Invocation", eg "CropImageInvocation".
+ 4. Every Invocation must use the `@invocation` decorator to provide its unique invocation type. You may provide its title, tags and category using the decorator.
+ 5. Invocations are strictly typed. We make use of the native [typing](https://docs.python.org/3/library/typing.html) library and the installed [pydantic](https://pydantic-docs.helpmanual.io/) library for validation.
+
+
+So let us do that.
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ invocation,
+)
+
+@invocation('resize')
+class ResizeInvocation(BaseInvocation):
+ '''Resizes an image'''
+```
+
+That's great.
+
+Now we have setup the base of our new Invocation. Let us think about what inputs
+our Invocation takes.
+
+- We need an `image` that we are going to resize.
+- We will need new `width` and `height` values to which we need to resize the
+ image to.
+
+### Inputs
+
+Every Invocation input must be defined using the `InputField` function. This is
+a wrapper around the pydantic `Field` function, which handles a few extra things
+and provides type hints. Like everything else, this should be strictly typed and
+defined.
+
+So let us create these inputs for our Invocation. First up, the `image` input we
+need. Generally, we can use standard variable types in Python but InvokeAI
+already has a custom `ImageField` type that handles all the stuff that is needed
+for image inputs.
+
+But what is this `ImageField` ..? It is a special class type specifically
+written to handle how images are dealt with in InvokeAI. We will cover how to
+create your own custom field types later in this guide. For now, let's go ahead
+and use it.
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ invocation,
+)
+
+@invocation('resize')
+class ResizeInvocation(BaseInvocation):
+
+ # Inputs
+ image: ImageField = InputField(description="The input image")
+```
+
+Let us break down our input code.
+
+```python
+image: ImageField = InputField(description="The input image")
+```
+
+| Part | Value | Description |
+| ---- | ----- | ----------- |
+| Name | `image` | The variable that will hold our image. |
+| Type Hint | `ImageField` | The type for our field. Indicates that `image` must be an `ImageField`. |
+| Field | `InputField(description="The input image")` | Declares `image` as an input field and provides its description. |
+
+Great. Now let us create our other inputs for `width` and `height`
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ invocation,
+)
+
+@invocation('resize')
+class ResizeInvocation(BaseInvocation):
+
+ # Inputs
+ image: ImageField = InputField(description="The input image")
+ width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
+ height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
+```
+
+As you might have noticed, we added two new arguments to the `InputField`
+definition for `width` and `height`, called `gt` and `le`. They stand for
+_greater than or equal to_ and _less than or equal to_.
+
+These impose constraints on those fields, and will raise an exception if the
+values do not meet the constraints. Field constraints are provided by
+**pydantic**, so anything you see in the **pydantic docs** will work.
+
+**Note:** _Any time it is possible to define constraints for our field, we
+should do it so the frontend has more information on how to parse this field._
+
+Perfect. We now have our inputs. Let us do something with these.
+
+### Invoke Function
+
+The `invoke` function is where all the magic happens. This function provides you
+the `context` parameter that is of the type `InvocationContext` which will give
+you access to the current context of the generation and all the other services
+that are provided by it by InvokeAI.
+
+Let us create this function first.
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ InvocationContext,
+ invocation,
+)
+
+@invocation('resize')
+class ResizeInvocation(BaseInvocation):
+ '''Resizes an image'''
+
+ image: ImageField = InputField(description="The input image")
+ width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
+ height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
+
+ def invoke(self, context: InvocationContext):
+ pass
+```
+
+### Outputs
+
+The output of our Invocation will be whatever is returned by this `invoke`
+function. Like with our inputs, we need to strongly type and define our outputs
+too.
+
+What is our output going to be? Another image. Normally you'd have to create a
+type for this but InvokeAI already offers you an `ImageOutput` type that handles
+all the necessary info related to image outputs. So let us use that.
+
+We will cover how to create your own output types later in this guide.
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ InvocationContext,
+ invocation,
+)
+
+from invokeai.app.invocations.image import ImageOutput
+
+@invocation('resize')
+class ResizeInvocation(BaseInvocation):
+ '''Resizes an image'''
+
+ image: ImageField = InputField(description="The input image")
+ width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
+ height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ pass
+```
+
+Perfect. Now that we have our Invocation setup, let us do what we want to do.
+
+- We will first load the image using one of the services provided by InvokeAI to
+ load the image.
+- We will resize the image using `PIL` to our input data.
+- We will output this image in the format we set above.
+
+So let's do that.
+
+```py title="resize.py"
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ InvocationContext,
+ invocation,
+)
+
+from invokeai.app.invocations.image import ImageOutput
+
+@invocation("resize")
+class ResizeInvocation(BaseInvocation):
+ """Resizes an image"""
+
+ image: ImageField = InputField(description="The input image")
+ width: int = InputField(default=512, ge=64, le=2048, description="Width of the new image")
+ height: int = InputField(default=512, ge=64, le=2048, description="Height of the new image")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Load the input image as a PIL image
+ image = context.images.get_pil(self.image.image_name)
+
+ # Resize the image
+ resized_image = image.resize((self.width, self.height))
+
+ # Save the image
+ image_dto = context.images.save(image=resized_image)
+
+ # Return an ImageOutput
+ return ImageOutput.build(image_dto)
+```
+
+**Note:** Do not be overwhelmed by the `ImageOutput` process. InvokeAI has a
+certain way that the images need to be dispatched in order to be stored and read
+correctly. In 99% of the cases when dealing with an image output, you can simply
+copy-paste the template above.
+
+### Customization
+
+We can use the `@invocation` decorator to provide some additional info to the
+UI, like a custom title, tags and category.
+
+We also encourage providing a version. This must be a
+[semver](https://semver.org/) version string ("`$MAJOR`.`$MINOR`.`$PATCH`"). The UI
+will let users know if their workflow is using a mismatched version of the node.
+
+```py title="resize.py"
+@invocation("resize", title="My Resizer", tags=["resize", "image"], category="My Invocations", version="1.0.0")
+class ResizeInvocation(BaseInvocation):
+ """Resizes an image"""
+
+ image: ImageField = InputField(description="The input image")
+
+ # Rest of the code
+```
+
+That's it. You made your own **Resize Invocation**.
+
+## Result
+
+Once you make your Invocation correctly, the rest of the process is fully
+automated for you.
+
+When you launch InvokeAI, you can go to `http://localhost:9090/docs` and see
+your new Invocation show up there with all the relevant info.
+
+
+
+When you launch the frontend UI, you can go to the Node Editor tab and find your
+new Invocation ready to be used.
+
+
+
+## Contributing Nodes
+
+Once you've created a Node, the next step is to share it with the community! The
+best way to do this is to submit a Pull Request to add the Node to the
+[Community Nodes](/features/workflows/community-nodes) list. If you're not sure how to do that,
+take a look a at our [contributing nodes overview](/development/guides/creating-nodes/).
+
+## Advanced
+
+### Custom Output Types
+
+Like with custom inputs, sometimes you might find yourself needing custom
+outputs that InvokeAI does not provide. We can easily set one up.
+
+Now that you are familiar with Invocations and Inputs, let us use that knowledge
+to create an output that has an `image` field, a `color` field and a `string`
+field.
+
+- An invocation output is a class that derives from the parent class of
+ `BaseInvocationOutput`.
+- All invocation outputs must use the `@invocation_output` decorator to provide
+ their unique output type.
+- Output fields must use the provided `OutputField` function. This is very
+ similar to the `InputField` function described earlier - it's a wrapper around
+ `pydantic`'s `Field()`.
+- It is not mandatory but we recommend using names ending with `Output` for
+ output types.
+- It is not mandatory but we highly recommend adding a `docstring` to describe
+ what your output type is for.
+
+Now that we know the basic rules for creating a new output type, let us go ahead
+and make it.
+
+```py title="custom_output.py"
+from .baseinvocation import BaseInvocationOutput, OutputField, invocation_output
+from .primitives import ImageField, ColorField
+
+@invocation_output('image_color_string_output')
+class ImageColorStringOutput(BaseInvocationOutput):
+ '''Base class for nodes that output a single image'''
+
+ image: ImageField = OutputField(description="The image")
+ color: ColorField = OutputField(description="The color")
+ text: str = OutputField(description="The string")
+```
+
+That's all there is to it.
+
+### Custom Input Fields
+
+Now that you know how to create your own Invocations, let us dive into slightly
+more advanced topics.
+
+While creating your own Invocations, you might run into a scenario where the
+existing fields in InvokeAI do not meet your requirements. In such cases, you
+can create your own fields.
+
+Let us create one as an example. Let us say we want to create a color input
+field that represents a color code. But before we start on that here are some
+general good practices to keep in mind.
+
+### Best Practices
+
+- There is no naming convention for input fields, but we highly recommend that
+ you name it something appropriate like `ColorField`.
+- It is not mandatory but it is heavily recommended to add a relevant
+ `docstring` to describe your field.
+- Keep your field in the same file as the Invocation that it is made for, or in
+ another file where it is relevant.
+
+All input types are a class that derive from the `BaseModel` type from `pydantic`.
+So let's create one.
+
+```py title="color_field.py"
+ from pydantic import BaseModel
+
+ class ColorField(BaseModel):
+ '''A field that holds the rgba values of a color'''
+ pass
+```
+
+Perfect. Now let us create the properties for our field. This is similar to how
+you created input fields for your Invocation. All the same rules apply. Let us
+create four fields representing the _red(r)_, _blue(b)_, _green(g)_ and
+_alpha(a)_ channel of the color.
+
+:::note
+ Technically, the properties are _also_ called fields - but in this case, it refers to a `pydantic` field.
+:::
+
+```py title="color_field.py"
+class ColorField(BaseModel):
+ '''A field that holds the rgba values of a color'''
+ r: int = Field(ge=0, le=255, description="The red channel")
+ g: int = Field(ge=0, le=255, description="The green channel")
+ b: int = Field(ge=0, le=255, description="The blue channel")
+ a: int = Field(ge=0, le=255, description="The alpha channel")
+```
+
+That's it. We now have a new input field type that we can use in our Invocations
+like this.
+
+```python
+color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=0), description='Background color of an image')
+```
+
+### Using the custom field
+
+When you start the UI, your custom field will be automatically recognized.
+
+Custom fields only support connection inputs in the Workflow Editor.
diff --git a/docs/src/content/docs/development/Architecture/model-manager.mdx b/docs/src/content/docs/development/Architecture/model-manager.mdx
new file mode 100644
index 00000000000..ca7884482f6
--- /dev/null
+++ b/docs/src/content/docs/development/Architecture/model-manager.mdx
@@ -0,0 +1,1198 @@
+---
+title: Introduction to the Model Manager
+sidebar:
+ label: Model Manager
+lastUpdated: 2026-02-18
+---
+
+import { FileTree, Code, Steps } from '@astrojs/starlight/components';
+
+The Model Manager is responsible for organizing the various machine
+learning models used by InvokeAI. It consists of a series of
+interdependent services that together handle the full lifecycle of a
+model. These are the:
+
+- **ModelRecordServiceBase:** Responsible for managing model metadata and configuration information. Among other things, the record service tracks the type of the model, its provenance, and where it can be found on disk.
+- **ModelInstallServiceBase:** A service for installing models to disk. It uses `DownloadQueueServiceBase` to download models and their metadata, and `ModelRecordServiceBase` to store that information. It is also responsible for managing the InvokeAI `models` directory and its contents.
+- **DownloadQueueServiceBase:** A multithreaded downloader responsible for downloading models from a remote source to disk. The download queue has special methods for downloading repo_id folders from Hugging Face, as well as discriminating among model versions in Civitai, but can be used for arbitrary content.
+ - **ModelLoadServiceBase** Responsible for loading a model from disk into RAM and VRAM and getting it ready for inference.
+
+
+ ## Location of the Code
+
+ The four main services can be found in `invokeai/app/services` in the following directories:
+
+
+- invokeai
+ - app
+ - services Model manager services
+ - model_records/
+ - model_install/
+ - downloads/
+ - model_load/
+ - api
+ - routers
+ - model_manager_v2.py FastAPI web API for model management
+
+
+---
+
+## What's in a Model? The ModelRecordService
+
+The `ModelRecordService` manages the model's metadata. It supports a hierarchy of pydantic metadata "config" objects, which become increasingly specialized to support particular model types.
+
+### ModelConfigBase
+
+All model metadata classes inherit from this pydantic class. it provides the following fields:
+
+| **Field Name** | **Type** | **Description** |
+|----------------|-----------------|------------------|
+| `key` | str | Unique identifier for the model |
+| `name` | str | Name of the model (not unique) |
+| `model_type` | ModelType | The type of the model |
+| `model_format` | ModelFormat | The format of the model (e.g. "diffusers"); also used as a Union discriminator |
+| `base_model` | BaseModelType | The base model that the model is compatible with |
+| `path` | str | Location of model on disk |
+| `hash` | str | Hash of the model |
+| `description` | str | Human-readable description of the model (optional) |
+| `source` | str | Model's source URL or repo id (optional) |
+
+The `key` is a unique 32-character random ID which was generated at install time. The `hash` field stores a hash of the model's contents at install time obtained by sampling several parts of the model's files using the `imohash` library. Over the course of the model's lifetime it may be transformed in various ways, such as changing its precision or converting it from a .safetensors to a diffusers model.
+
+The `path` field can be absolute or relative. If relative, it is taken to be relative to the `models_dir` setting in the user's `invokeai.yaml` file.
+
+`ModelType`, `ModelFormat` and `BaseModelType` are string enums that are defined in `invokeai.backend.model_manager.config`. They are also imported by, and can be reexported from, `invokeai.app.services.model_manager.model_records`:
+
+```py title="invokeai.backend.model_manager.config"
+from invokeai.app.services.model_records import ModelType, ModelFormat, BaseModelType
+````
+
+### CheckpointConfig
+
+This adds support for checkpoint configurations, and adds the
+following field:
+
+| **Field Name** | **Type** | **Description** |
+|----------------|-----------------|------------------|
+| `config` | str | Path to the checkpoint's config file |
+
+`config` is the path to the checkpoint's config file. If relative, it is taken to be relative to the InvokeAI root directory (e.g. `configs/stable-diffusion/v1-inference.yaml`)
+
+### MainConfig
+
+This adds support for "main" Stable Diffusion models, and adds these fields:
+
+| **Field Name** | **Type** | **Description** |
+|----------------|-----------------|------------------|
+| `vae` | str | Path to a VAE to use instead of the burnt-in one |
+| `variant` | ModelVariantType| Model variant type, such as "inpainting" |
+
+`vae` can be an absolute or relative path. If relative, its base is taken to be the `models_dir` directory.
+
+`variant` is an enumerated string class with values `normal`, `inpaint` and `depth`. If needed, it can be imported if needed from either `invokeai.app.services.model_records` or `invokeai.backend.model_manager.config`.
+
+### ONNXSD2Config
+
+| **Field Name** | **Type** | **Description** |
+|----------------|-----------------|------------------|
+| `prediction_type` | SchedulerPredictionType | Scheduler prediction type to use, e.g. "epsilon" |
+| `upcast_attention` | bool | Model requires its attention module to be upcast |
+
+The `SchedulerPredictionType` enum can be imported from either `invokeai.app.services.model_records` or `invokeai.backend.model_manager.config`.
+
+### Other config classes
+
+There are a series of such classes each discriminated by their `ModelFormat`, including `LoRAConfig`, `IPAdapterConfig`, and so forth. These are rarely needed outside the model manager's internal code, but available in `invokeai.backend.model_manager.config` if needed. There is also a Union of all ModelConfig classes, called `AnyModelConfig` that can be imported from the same file.
+
+### Limitations of the Data Model
+
+The config hierarchy has a major limitation in its handling of the base model type. Each model can only be compatible with one base model, which breaks down in the event of models that are compatible with two or more base models. For example, SD-1 VAEs also work with SD-2 models. A partial workaround is to use `BaseModelType.Any`, which indicates that the model is compatible with any of the base models. This works OK for some models, such as the IP Adapter image encoders, but is an all-or-nothing proposition.
+
+## Reading and Writing Model Configuration Records
+
+The `ModelRecordService` provides the ability to retrieve model configuration records from SQL or YAML databases, update them, and write them back.
+
+A application-wide `ModelRecordService` is created during API initialization and can be retrieved within an invocation from the `InvocationContext` object:
+
+```py
+store = context.services.model_manager.store
+```
+
+or from elsewhere in the code by accessing `ApiDependencies.invoker.services.model_manager.store`.
+
+### Creating a `ModelRecordService`
+
+To create a new `ModelRecordService` database or open an existing one, you can directly create either a `ModelRecordServiceSQL` or a `ModelRecordServiceFile` object:
+
+```py
+from invokeai.app.services.model_records import ModelRecordServiceSQL, ModelRecordServiceFile
+
+store = ModelRecordServiceSQL.from_connection(connection, lock)
+store = ModelRecordServiceSQL.from_db_file('/path/to/sqlite_database.db')
+store = ModelRecordServiceFile.from_db_file('/path/to/database.yaml')
+```
+
+The `from_connection()` form is only available from the `ModelRecordServiceSQL` class, and is used to manage records in a previously-opened SQLITE3 database using a `sqlite3.connection` object and a `threading.lock` object. It is intended for the specific use case of storing the record information in the main InvokeAI database, usually `databases/invokeai.db`.
+
+The `from_db_file()` methods can be used to open new connections to the named database files. If the file doesn't exist, it will be created and initialized.
+
+As a convenience, `ModelRecordServiceBase` offers two methods, `from_db_file` and `open`, which will return either a SQL or File implementation depending on the context. The former looks at the file extension to determine whether to open the file as a SQL database (".db") or as a file database (".yaml"). If the file exists, but is either the wrong type or does not contain the expected schema metainformation, then an appropriate `AssertionError` will be raised:
+
+```py
+store = ModelRecordServiceBase.from_db_file('/path/to/a/file.{yaml,db}')
+```
+
+The `ModelRecordServiceBase.open()` method is specifically designed for use in the InvokeAI web server. Its signature is:
+
+```py
+def open(
+ cls,
+ config: InvokeAIAppConfig,
+ conn: Optional[sqlite3.Connection] = None,
+ lock: Optional[threading.Lock] = None
+ ) -> Union[ModelRecordServiceSQL, ModelRecordServiceFile]:
+```
+
+The way it works is as follows:
+
+1. Retrieve the value of the `model_config_db` option from the user's `invokeai.yaml` config file.
+2. If `model_config_db` is `auto` (the default), then:
+ - Use the values of `conn` and `lock` to return a `ModelRecordServiceSQL` object opened on the passed connection and lock.
+ - Open up a new connection to `databases/invokeai.db` if `conn` and/or `lock` are missing (see note below).
+3. If `model_config_db` is a Path, then use `from_db_file` to return the appropriate type of ModelRecordService.
+4. If `model_config_db` is None, then retrieve the legacy `conf_path` option from `invokeai.yaml` and use the Path indicated there. This will default to `configs/models.yaml`.
+
+So a typical startup pattern would be:
+
+```py
+import sqlite3
+from invokeai.app.services.thread import lock
+from invokeai.app.services.model_records import ModelRecordServiceBase
+from invokeai.app.services.config import InvokeAIAppConfig
+
+config = InvokeAIAppConfig.get_config()
+db_conn = sqlite3.connect(config.db_path.as_posix(), check_same_thread=False)
+store = ModelRecordServiceBase.open(config, db_conn, lock)
+```
+
+### Fetching a Model's Configuration from `ModelRecordServiceBase`
+
+Configurations can be retrieved in several ways.
+
+#### get_model(key) -> AnyModelConfig
+
+The basic functionality is to call the record store object's `get_model()` method with the desired model's unique key. It returns the appropriate subclass of ModelConfigBase:
+
+```py
+model_conf = store.get_model('f13dd932c0c35c22dcb8d6cda4203764')
+print(model_conf.path)
+
+>> '/tmp/models/ckpts/v1-5-pruned-emaonly.safetensors'
+
+```
+
+If the key is unrecognized, this call raises an `UnknownModelException`.
+
+#### exists(key) -> AnyModelConfig
+
+Returns True if a model with the given key exists in the database.
+
+#### search_by_path(path) -> AnyModelConfig
+
+Returns the configuration of the model whose path is `path`. The path is matched using a simple string comparison and won't correctly match models referred to by different paths (e.g. using symbolic links).
+
+#### search_by_name(name, base, type) -> List[AnyModelConfig]
+
+This method searches for models that match some combination of `name`, `BaseType` and `ModelType`. Calling without any arguments will return all the models in the database.
+
+#### all_models() -> List[AnyModelConfig]
+
+Return all the model configs in the database. Exactly equivalent to calling `search_by_name()` with no arguments.
+
+#### search_by_tag(tags) -> List[AnyModelConfig]
+
+`tags` is a list of strings. This method returns a list of model configs that contain all of the given tags. Examples:
+
+```py
+# find all models that are marked as both SFW and as generating
+# background scenery
+configs = store.search_by_tag(['sfw', 'scenery'])
+```
+
+Note that only tags are not searchable in this way. Other fields can be searched using a filter:
+
+```py
+commercializable_models = [x for x in store.all_models() \
+ if x.license.contains('allowCommercialUse=Sell')]
+```
+
+#### version() -> str
+
+Returns the version of the database, currently at `3.2`
+
+#### model_info_by_name(name, base_model, model_type) -> ModelConfigBase
+
+This method exists to ease the transition from the previous version of the model manager, in which `get_model()` took the three arguments shown above. This looks for a unique model identified by name, base model and model type and returns it.
+
+The method will generate a `DuplicateModelException` if there are more than one models that share the same type, base and name. While unlikely, it is certainly possible to have a situation in which the user had added two models with the same name, base and type, one located at path `/foo/my_model` and the other at `/bar/my_model`. It is strongly recommended to search for models using `search_by_name()`, which can return multiple results, and then to select the desired model and pass its key to `get_model()`.
+
+### Writing model configs to the database
+
+Several methods allow you to create and update stored model config records.
+
+#### add_model(key, config) -> AnyModelConfig
+
+Given a key and a configuration, this will add the model's configuration record to the database. `config` can either be a subclass of `ModelConfigBase` (i.e. any class listed in `AnyModelConfig`), or a `dict` of key/value pairs. In the latter case, the correct configuration class will be picked by Pydantic's discriminated union mechanism.
+
+If successful, the method will return the appropriate subclass of `ModelConfigBase`. It will raise a `DuplicateModelException` if a model with the same key is already in the database, or an `InvalidModelConfigException` if a dict was passed and Pydantic experienced a parse or validation error.
+
+### update_model(key, config) -> AnyModelConfig
+
+Given a key and a configuration, this will update the model configuration record in the database. `config` can be either a instance of `ModelConfigBase`, or a sparse `dict` containing the fields to be updated. This will return an `AnyModelConfig` on success, or raise `InvalidModelConfigException` or `UnknownModelException` exceptions on failure.
+
+---
+
+## Model installation
+
+The `ModelInstallService` class implements the
+`ModelInstallServiceBase` abstract base class, and provides a one-stop
+shop for all your model install needs. It provides the following
+functionality:
+
+- Registering a model config record for a model already located on the local filesystem, without moving it or changing its path.
+
+- Installing a model alreadiy located on the local filesystem, by moving it into the InvokeAI root directory under the `models` folder (or wherever config parameter `models_dir` specifies).
+
+- Probing of models to determine their type, base type and other key information.
+
+- Interface with the InvokeAI event bus to provide status updates on the download, installation and registration process.
+
+- Downloading a model from an arbitrary URL and installing it in `models_dir`.
+
+- Special handling for HuggingFace repo_ids to recursively download the contents of the repository, paying attention to alternative variants such as fp16.
+
+- Saving tags and other metadata about the model into the invokeai database when fetching from a repo that provides that type of information, (currently only HuggingFace).
+
+### Initializing the installer
+
+A default installer is created at InvokeAI api startup time and stored in `ApiDependencies.invoker.services.model_install` and can also be retrieved from an invocation's `context` argument with `context.services.model_install`.
+
+In the event you wish to create a new installer, you may use the following initialization pattern:
+
+```py
+from invokeai.app.services.config import get_config
+from invokeai.app.services.model_records import ModelRecordServiceSQL
+from invokeai.app.services.model_install import ModelInstallService
+from invokeai.app.services.download import DownloadQueueService
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.backend.util.logging import InvokeAILogger
+
+config = get_config()
+
+logger = InvokeAILogger.get_logger(config=config)
+db = SqliteDatabase(config.db_path, logger)
+record_store = ModelRecordServiceSQL(db, logger)
+queue = DownloadQueueService()
+queue.start()
+
+installer = ModelInstallService(app_config=config,
+ record_store=record_store,
+ download_queue=queue
+ )
+installer.start()
+```
+
+The full form of `ModelInstallService()` takes the following required parameters:
+
+| **Argument** | **Type** | **Description** |
+|------------------|------------------------------|------------------------------|
+| `app_config` | InvokeAIAppConfig | InvokeAI app configuration object |
+| `record_store` | ModelRecordServiceBase | Config record storage database |
+| `download_queue` | DownloadQueueServiceBase | Download queue object |
+|`session` | Optional[requests.Session] | Swap in a different Session object (usually for debugging) |
+
+Once initialized, the installer will provide the following methods:
+
+#### install_job = installer.heuristic_import(source, [config], [access_token])
+
+This is a simplified interface to the installer which takes a source string, an optional model configuration dictionary and an optional access token.
+
+The `source` is a string that can be any of these forms
+
+1. A path on the local filesystem (`C:\\users\\fred\\model.safetensors`)
+2. A Url pointing to a single downloadable model file (`https://civitai.com/models/58390/detail-tweaker-lora-lora`)
+3. A HuggingFace repo_id with any of the following formats:
+ * `model/name` -- entire model
+ * `model/name:fp32` -- entire model, using the fp32 variant
+ * `model/name:fp16:vae` -- vae submodel, using the fp16 variant
+ * `model/name::vae` -- vae submodel, using default precision
+ * `model/name:fp16:path/to/model.safetensors` -- an individual model file, fp16 variant
+ * `model/name::path/to/model.safetensors` -- an individual model file, default variant
+
+Note that by specifying a relative path to the top of the HuggingFace repo, you can download and install arbitrary models files.
+
+The variant, if not provided, will be automatically filled in with `fp32` if the user has requested full precision, and `fp16` otherwise. If a variant that does not exist is requested, then the method will install whatever HuggingFace returns as its default revision.
+
+`config` is an optional dict of values that will override the autoprobed values for model type, base, scheduler prediction type, and so forth. See [Model configuration and probing](#model-configuration-and-probing) for details.
+
+`access_token` is an optional access token for accessing resources that need authentication.
+
+The method will return a `ModelInstallJob`. This object is discussed at length in the following section.
+
+#### install_job = installer.import_model()
+
+The `import_model()` method is the core of the installer. The following illustrates basic usage:
+
+```py
+from invokeai.app.services.model_install import (
+ LocalModelSource,
+ HFModelSource,
+ URLModelSource,
+)
+
+source1 = LocalModelSource(path='/opt/models/sushi.safetensors') # a local safetensors file
+source2 = LocalModelSource(path='/opt/models/sushi_diffusers') # a local diffusers folder
+
+source3 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5') # a repo_id
+source4 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', subfolder='vae') # a subfolder within a repo_id
+source5 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', variant='fp16') # a named variant of a HF model
+source6 = HFModelSource(repo_id='runwayml/stable-diffusion-v1-5', subfolder='OrangeMix/OrangeMix1.ckpt') # path to an individual model file
+
+source7 = URLModelSource(url='https://civitai.com/api/download/models/63006') # model located at a URL
+source8 = URLModelSource(url='https://civitai.com/api/download/models/63006', access_token='letmein') # with an access token
+
+for source in [source1, source2, source3, source4, source5, source6, source7]:
+ install_job = installer.install_model(source)
+
+source2job = installer.wait_for_installs(timeout=120)
+for source in sources:
+ job = source2job[source]
+ if job.complete:
+ model_config = job.config_out
+ model_key = model_config.key
+ print(f"{source} installed as {model_key}")
+ elif job.errored:
+ print(f"{source}: {job.error_type}.\nStack trace:\n{job.error}")
+
+```
+
+As shown here, the `import_model()` method accepts a variety of sources, including local safetensors files, local diffusers folders, HuggingFace repo_ids with and without a subfolder designation, Civitai model URLs and arbitrary URLs that point to checkpoint files (but not to folders).
+
+Each call to `import_model()` return a `ModelInstallJob` job, an object which tracks the progress of the install.
+
+If a remote model is requested, the model's files are downloaded in parallel across a multiple set of threads using the download queue. During the download process, the `ModelInstallJob` is updated to provide status and progress information. After the files (if any) are downloaded, the remainder of the installation runs in a single serialized background thread. These are the model probing, file copying, and config record database update steps.
+
+Multiple install jobs can be queued up. You may block until all install jobs are completed (or errored) by calling the `wait_for_installs()` method as shown in the code example. `wait_for_installs()` will return a `dict` that maps the requested source to its job. This object can be interrogated to determine its status. If the job errored out, then the error type and details can be recovered from `job.error_type` and `job.error`.
+
+The full list of arguments to `import_model()` is as follows:
+
+| Argument | Type | Default | Description |
+|----------|-------|---------|-------------|
+| `source` | ModelSource | None | The source of the model, Path, URL or repo_id |
+| `config` | Dict[str, Any] | None | Override all or a portion of model's probed attributes |
+
+The next few sections describe the various types of ModelSource that can be passed to `import_model()`.
+
+`config` can be used to override all or a portion of the configuration attributes returned by the model prober. See the section below for details.
+
+#### LocalModelSource
+
+This is used for a model that is located on a locally-accessible Posix filesystem, such as a local disk or networked fileshare.
+
+| Argument | Type | Default | Description |
+|----------|------|---------|-------------|
+| `path` | str | Path | None | Path to the model file or directory |
+| `inplace` | bool | False | If set, the model file(s) will be left in their location; otherwise they will be copied into the InvokeAI root's `models` directory |
+
+#### URLModelSource
+
+This is used for a single-file model that is accessible via a URL. The
+fields are:
+
+| Argument | Type | Default | Description |
+|----------|------|---------|-------------|
+| `url` | AnyHttpUrl | None | The URL for the model file. |
+| `access_token` | str | None | An access token needed to gain access to this file. |
+
+The `AnyHttpUrl` class can be imported from `pydantic.networks`.
+
+Ordinarily, no metadata is retrieved from these sources. However, there is special-case code in the installer that looks for HuggingFace and fetches the corresponding model metadata from the corresponding repo.
+
+#### HFModelSource
+
+HuggingFace has the most complicated `ModelSource` structure:
+
+| Argument | Type | Default | Description |
+|----------|------|---------|-------------|
+| `repo_id` | str | None | The ID of the desired model. |
+| `variant` | ModelRepoVariant | ModelRepoVariant('fp16') | The desired variant. |
+| `subfolder` | Path | None | Look for the model in a subfolder of the repo. |
+| `access_token` | str | None | An access token needed to gain access to a subscriber's-only model. |
+
+The `repo_id` is the repository ID, such as `stabilityai/sdxl-turbo`.
+
+The `variant` is one of the various diffusers formats that HuggingFace supports and is used to pick out from the hodgepodge of files that in a typical HuggingFace repository the particular components needed for a complete diffusers model. `ModelRepoVariant` is an enum that can be imported from `invokeai.backend.model_manager` and has the following values:
+
+| Name | String Value |
+|------|--------------|
+| ModelRepoVariant.DEFAULT | "default" |
+| ModelRepoVariant.FP16 | "fp16" |
+| ModelRepoVariant.FP32 | "fp32" |
+| ModelRepoVariant.ONNX | "onnx" |
+| ModelRepoVariant.OPENVINO | "openvino" |
+| ModelRepoVariant.FLAX | "flax" |
+
+You can also pass the string forms to `variant` directly. Note that InvokeAI may not be able to load and run all variants. At the current time, specifying `ModelRepoVariant.DEFAULT` will retrieve model files that are unqualified, e.g. `pytorch_model.safetensors` rather than `pytorch_model.fp16.safetensors`. These are usually the 32-bit safetensors forms of the model.
+
+If `subfolder` is specified, then the requested model resides in a subfolder of the main model repository. This is typically used to fetch and install VAEs.
+
+Some models require you to be registered with HuggingFace and logged in. To download these files, you must provide an `access_token`. Internally, if no access token is provided, then `HfFolder.get_token()` will be called to fill it in with the cached one.
+
+#### Monitoring the install job process
+
+When you create an install job with `import_model()`, it launches the download and installation process in the background and returns a `ModelInstallJob` object for monitoring the process.
+
+The `ModelInstallJob` class has the following structure:
+
+| Attribute | Type | Description |
+|-----------|------|-------------|
+| `id` | `int` | Integer ID for this job |
+| `status` | `InstallStatus` | An enum of [`waiting`, `downloading`, `running`, `completed`, `error` and `cancelled`] |
+| `config_in` | `dict` | Overriding configuration values provided by the caller |
+| `config_out` | `AnyModelConfig` | After successful completion, contains the configuration record written to the database |
+| `inplace` | `boolean` | True if the caller asked to install the model in place using its local path |
+| `source` | `ModelSource` | The local path, remote URL or repo_id of the model to be installed |
+| `local_path` | `Path` | If a remote model, holds the path of the model after it is downloaded; if a local model, same as `source` |
+| `error_type` | `str` | Name of the exception that led to an error status |
+| `error` | `str` | Traceback of the error |
+
+If the `event_bus` argument was provided, events will also be broadcast to the InvokeAI event bus. The events will appear on the bus as an event of type `EventServiceBase.model_event`, a timestamp and the following event names:
+
+##### `model_install_downloading`
+
+For remote models only, `model_install_downloading` events will be issued at regular intervals as the download progresses. The event's payload contains the following keys:
+
+| Key | Type | Description |
+|-----|------|-------------|
+|`source`|str|String representation of the requested source|
+|`local_path`|str|String representation of the path to the downloading model (usually a temporary directory)|
+|`bytes`|int|How many bytes downloaded so far|
+|`total_bytes`|int|Total size of all the files that make up the model|
+|`parts`|List[Dict]|Information on the progress of the individual files that make up the model|
+
+The parts is a list of dictionaries that give information on each of the components pieces of the download. The dictionary's keys are `source`, `local_path`, `bytes` and `total_bytes`, and correspond to the like-named keys in the main event.
+
+Note that downloading events will not be issued for local models, and that downloading events occur _before_ the running event.
+
+##### `model_install_running`
+
+`model_install_running` is issued when all the required downloads have completed (if applicable) and the model probing, copying and registration process has now started.
+
+The payload will contain the key `source`.
+
+##### `model_install_completed`
+
+`model_install_completed` is issued once at the end of a successful installation. The payload will contain the keys `source`, `total_bytes` and `key`, where `key` is the ID under which the model has been registered.
+
+##### `model_install_error`
+
+`model_install_error` is emitted if the installation process fails for some reason. The payload will contain the keys `source`, `error_type` and `error`. `error_type` is a short message indicating the nature of the error, and `error` is the long traceback to help debug the problem.
+
+##### `model_install_cancelled`
+
+`model_install_cancelled` is issued if the model installation is cancelled, or if one or more of its files' downloads are cancelled. The payload will contain `source`.
+
+##### Following the model status
+
+You may poll the `ModelInstallJob` object returned by `import_model()` to ascertain the state of the install. The job status can be read from the job's `status` attribute, an `InstallStatus` enum which has the enumerated values `WAITING`, `DOWNLOADING`, `RUNNING`, `COMPLETED`, `ERROR` and `CANCELLED`.
+
+For convenience, install jobs also provided the following boolean properties: `waiting`, `downloading`, `running`, `complete`, `errored` and `cancelled`, as well as `in_terminal_state`. The last will return True if the job is in the complete, errored or cancelled states.
+
+#### Model configuration and probing
+
+The install service uses the `invokeai.backend.model_manager.probe` module during import to determine the model's type, base type, and other configuration parameters. Among other things, it assigns a default name and description for the model based on probed fields.
+
+When downloading remote models is implemented, additional configuration information, such as list of trigger terms, will be retrieved from the HuggingFace and Civitai model repositories.
+
+The probed values can be overridden by providing a dictionary in the optional `config` argument passed to `import_model()`. You may provide overriding values for any of the model's configuration attributes. Here is an example of setting the `SchedulerPredictionType` and `name` for an sd-2 model:
+
+```py
+install_job = installer.import_model(
+ source=HFModelSource(repo_id='stabilityai/stable-diffusion-2-1',variant='fp32'),
+ config=dict(
+ prediction_type=SchedulerPredictionType('v_prediction')
+ name='stable diffusion 2 base model',
+ )
+ )
+```
+
+### Other installer methods
+
+This section describes additional methods provided by the installer class.
+
+#### jobs = installer.wait_for_installs([timeout])
+
+Block until all pending installs are completed or errored and then returns a list of completed jobs. The optional `timeout` argument will return from the call if jobs aren't completed in the specified time. An argument of 0 (the default) will block indefinitely.
+
+#### jobs = installer.wait_for_job(job, [timeout])
+
+Like `wait_for_installs()`, but block until a specific job has completed or errored, and then return the job. The optional `timeout` argument will return from the call if the job doesn't complete in the specified time. An argument of 0 (the default) will block indefinitely.
+
+#### jobs = installer.list_jobs()
+
+Return a list of all active and complete `ModelInstallJobs`.
+
+#### jobs = installer.get_job_by_source(source)
+
+Return a list of `ModelInstallJob` corresponding to the indicated model source.
+
+#### jobs = installer.get_job_by_id(id)
+
+Return a list of `ModelInstallJob` corresponding to the indicated model id.
+
+#### jobs = installer.cancel_job(job)
+
+Cancel the indicated job.
+
+#### installer.prune_jobs
+
+Remove jobs that are in a terminal state (i.e. complete, errored or cancelled) from the job list returned by `list_jobs()` and `get_job()`.
+
+#### installer.app_config, installer.record_store, installer.event_bus
+
+Properties that provide access to the installer's `InvokeAIAppConfig`, `ModelRecordServiceBase` and `EventServiceBase` objects.
+
+#### key = installer.register_path(model_path, config), key = installer.install_path(model_path, config)
+
+These methods bypass the download queue and directly register or install the model at the indicated path, returning the unique ID for the installed model.
+
+Both methods accept a Path object corresponding to a checkpoint or diffusers folder, and an optional dict of config attributes to use to override the values derived from model probing.
+
+The difference between `register_path()` and `install_path()` is that the former creates a model configuration record without changing the location of the model in the filesystem. The latter makes a copy of the model inside the InvokeAI models directory before registering it.
+
+#### installer.unregister(key)
+
+This will remove the model config record for the model at key, and is equivalent to `installer.record_store.del_model(key)`
+
+#### installer.delete(key)
+
+This is similar to `unregister()` but has the additional effect of conditionally deleting the underlying model file(s) if they reside within the InvokeAI models directory
+
+#### installer.unconditionally_delete(key)
+
+This method is similar to `unregister()`, but also unconditionally deletes the corresponding model weights file(s), regardless of whether they are inside or outside the InvokeAI models hierarchy.
+
+#### path = installer.download_and_cache(remote_source, [access_token], [timeout])
+
+This utility routine will download the model file located at source, cache it, and return the path to the cached file. It does not attempt to determine the model type, probe its configuration values, or register it with the models database.
+
+You may provide an access token if the remote source requires authorization. The call will block indefinitely until the file is completely downloaded, cancelled or raises an error of some sort. If you provide a timeout (in seconds), the call will raise a `TimeoutError` exception if the download hasn't completed in the specified period.
+
+You may use this mechanism to request any type of file, not just a model. The file will be stored in a subdirectory of `INVOKEAI_ROOT/models/.cache`. If the requested file is found in the cache, its path will be returned without redownloading it.
+
+Be aware that the models cache is cleared of infrequently-used files and directories at regular intervals when the size of the cache exceeds the value specified in Invoke's `convert_cache` configuration variable.
+
+#### installer.start(invoker)
+
+The `start` method is called by the API initialization routines when the API starts up. Its effect is to call `sync_to_config()` to synchronize the model record store database with what's currently on disk.
+
+---
+
+## Get on line: The Download Queue
+
+InvokeAI can download arbitrary files using a multithreaded background download queue. Internally, the download queue is used for installing models located at remote locations. The queue is implemented by the `DownloadQueueService` defined in `invokeai.app.services.download_manager`. However, most of the implementation is spread out among several files in `invokeai/backend/model_manager/download/*`
+
+A default download queue is located in `ApiDependencies.invoker.services.download_queue`. However, you can create additional instances if you need to isolate your queue from the main one.
+
+### A job for every task
+
+The queue operates on a series of download job objects. These objects specify the source and destination of the download, and keep track of the progress of the download. Jobs come in a variety of shapes and colors as they are progressively specialized for particular download task.
+
+The basic job is the `DownloadJobBase`, a pydantic object with the following fields:
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `id` | int | | Job ID, an integer >= 0 |
+| `priority` | int | 10 | Job priority. Lower priorities run before higher priorities |
+| `source` | str | | Where to download from (specialized types used in subclasses) |
+| `destination` | Path | | Where to download to |
+| `status` | DownloadJobStatus | Idle | Job's status (see below) |
+| `event_handlers` | List[DownloadEventHandler] | | Event handlers (see below) |
+| `job_started` | float | | Timestamp for when the job started running |
+| `job_ended` | float | | Timestamp for when the job completed or errored out |
+| `job_sequence` | int | | A counter that is incremented each time a model is dequeued |
+| `error` | Exception | | A copy of the Exception that caused an error during download |
+
+When you create a job, you can assign it a `priority`. If multiple jobs are queued, the job with the lowest priority runs first. (Don't blame us! The Unix developers came up with this convention.)
+
+Every job has a `source` and a `destination`. `source` is a string in the base class, but subclassses redefine it more specifically.
+
+The `destination` must be the Path to a file or directory on the local filesystem. If the Path points to a new or existing file, then the source will be stored under that filename. If the Path ponts to an existing directory, then the downloaded file will be stored inside the directory, usually using the name assigned to it at the remote site in the `content-disposition` http field.
+
+When the job is submitted, it is assigned a numeric `id`. The id can then be used to control the job, such as starting, stopping and cancelling its download.
+
+The `status` field is updated by the queue to indicate where the job is in its lifecycle. Values are defined in the string enum `DownloadJobStatus`, a symbol available from `invokeai.app.services.download_manager`. Possible values are:
+
+| Value | String Value | Description |
+|-------|--------------|-------------|
+| `IDLE` | idle | Job created, but not submitted to the queue |
+| `ENQUEUED` | enqueued | Job is patiently waiting on the queue |
+| `RUNNING` | running | Job is running! |
+| `PAUSED` | paused | Job was paused and can be restarted |
+| `COMPLETED` | completed | Job has finished its work without an error |
+| `ERROR` | error | Job encountered an error and will not run again |
+| `CANCELLED` | cancelled | Job was cancelled and will not run (again) |
+
+`job_started`, `job_ended` and `job_sequence` indicate when the job was started (using a python timestamp), when it completed, and the order in which it was taken off the queue. These are mostly used for debugging and performance testing.
+
+In case of an error, the Exception that caused the error will be placed in the `error` field, and the job's status will be set to `DownloadJobStatus.ERROR`.
+
+After an error occurs, any partially downloaded files will be deleted from disk, unless `preserve_partial_downloads` was set to True at job creation time (or set to True any time before the error occurred). Note that since all InvokeAI model install operations involve downloading files to a temporary directory that has a limited lifetime, this flag is not used by the model installer.
+
+There are a series of subclasses of `DownloadJobBase` that provide support for specific types of downloads. These are:
+
+#### DownloadJobPath
+
+This subclass redefines `source` to be a filesystem Path. It is used to move a file or directory from the `source` to the `destination` paths in the background using a uniform event-based infrastructure.
+
+#### DownloadJobRemoteSource
+
+This subclass adds the following fields to the job:
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `bytes` | int | 0 | bytes downloaded so far |
+| `total_bytes` | int | 0 | total size to download |
+| `access_token` | Any | None | an authorization token to present to the remote source |
+
+The job will start out with 0/0 in its bytes/total_bytes fields. Once it starts running, `total_bytes` will be populated from information provided in the HTTP download header (if available), and the number of bytes downloaded so far will be progressively incremented.
+
+#### DownloadJobURL
+
+This is a subclass of `DownloadJobBase`. It redefines `source` to be a
+Pydantic `AnyHttpUrl` object, which enforces URL validation checking
+on the field.
+
+Note that the installer service defines an additional subclass of
+`DownloadJobRemoteSource` that accepts HuggingFace repo_ids in
+addition to URLs. This is discussed later in this document.
+
+### Event handlers
+
+While a job is being downloaded, the queue will emit events at
+periodic intervals. A typical series of events during a successful
+download session will look like this:
+
+
+ 1. `enqueued`
+ 2. `running`
+ 3. `running`
+ 4. `running`
+ 5. `completed`
+
+
+There will be a single enqueued event, followed by one or more running events, and finally one `completed`, `error` or `cancelled` events.
+
+It is possible for a caller to pause download temporarily, in which case the events may look something like this:
+
+
+ 1. `enqueued`
+ 2. `running`
+ 3. `running`
+ 4. `paused` user paused the download
+ 5. `running`
+ 6. `completed`
+
+
+The download queue logs when downloads start and end (unless `quiet` is set to True at initialization time) but doesn't log any progress events. You will probably want to be alerted to events during the download job and provide more user feedback. In order to intercept and respond to events you may install a series of one or more event handlers in the job. Whenever the job's status changes, the chain of event handlers is traversed and executed in the same thread that the download job is running in.
+
+Event handlers have the signature `Callable[["DownloadJobBase"], None]`, i.e.
+
+```py
+def handler(job: DownloadJobBase):
+ pass
+```
+
+A typical handler will examine `job.status` and decide if there's something to be done. This can include cancelling or erroring the job, but more typically is used to report on the job status to the user interface or to perform certain actions on successful completion of the job.
+
+Event handlers can be attached to a job at creation time. In addition, you can create a series of default handlers that are attached to the queue object itself. These handlers will be executed for each job after the job's own handlers (if any) have run.
+
+During a download, running events are issued every time roughly 1% of the file is transferred. This is to provide just enough granularity to update a tqdm progress bar smoothly.
+
+Handlers can be added to a job after the fact using the job's `add_event_handler` method:
+
+```py
+job.add_event_handler(my_handler)
+```
+
+All handlers can be cleared using the job's `clear_event_handlers()` method. Note that it might be a good idea to pause the job before altering its handlers.
+
+### Creating a download queue object
+
+The `DownloadQueueService` constructor takes the following arguments:
+
+| Argument | Type | Default | Description |
+|----------|------|---------|-------------|
+| `event_handlers` | List[DownloadEventHandler] | [] | Event handlers |
+| `max_parallel_dl` | int | 5 | Maximum number of simultaneous downloads allowed |
+| `requests_session` | requests.sessions.Session | None | An alternative requests Session object to use for the download |
+| `quiet` | bool | False | Do work quietly without issuing log messages |
+
+A typical initialization sequence will look like:
+
+```py
+from invokeai.app.services.download_manager import DownloadQueueService
+
+def log_download_event(job: DownloadJobBase):
+ logger.info(f'job={job.id}: status={job.status}')
+
+queue = DownloadQueueService(
+ event_handlers=[log_download_event]
+ )
+```
+
+Event handlers can be provided to the queue at initialization time as shown in the example. These will be automatically appended to the handler list for any job that is submitted to this queue.
+
+`max_parallel_dl` sets the number of simultaneous active downloads that are allowed. The default of five has not been benchmarked in any way, but seems to give acceptable performance.
+
+`requests_session` can be used to provide a `requests` module Session object that will be used to stream remote URLs to disk. This facility was added for use in the module's unit tests to simulate a remote web server, but may be useful in other contexts.
+
+`quiet` will prevent the queue from issuing any log messages at the INFO or higher levels.
+
+### Submitting a download job
+
+You can submit a download job to the queue either by creating the job manually and passing it to the queue's `submit_download_job()` method, or using the `create_download_job()` method, which will do the same thing on your behalf.
+
+To use the former method, follow this example:
+
+```py
+job = DownloadJobRemoteSource(
+ source='http://www.civitai.com/models/13456',
+ destination='/tmp/models/',
+ event_handlers=[my_handler1, my_handler2], # if desired
+)
+queue.submit_download_job(job, start=True)
+```
+
+`submit_download_job()` takes just two arguments: the job to submit, and a flag indicating whether to immediately start the job (defaulting to True). If you choose not to start the job immediately, you can start it later by calling the queue's `start_job()` or `start_all_jobs()` methods, which are described later.
+
+To have the queue create the job for you, follow this example instead:
+
+```py
+job = queue.create_download_job(
+ source='http://www.civitai.com/models/13456',
+ destdir='/tmp/models/',
+ filename='my_model.safetensors',
+ event_handlers=[my_handler1, my_handler2], # if desired
+ start=True,
+ )
+```
+
+The `filename` argument forces the downloader to use the specified name for the file rather than the name provided by the remote source, and is equivalent to manually specifying a destination of `/tmp/models/my_model.safetensors' in the submitted job.
+
+Here is the full list of arguments that can be provided to `create_download_job()`:
+
+| Argument | Type | Default | Description |
+|----------|------|---------|-------------|
+| `source` | Union[str, Path, AnyHttpUrl] | | Download remote or local source |
+| `destdir` | Path | | Destination directory for downloaded file |
+| `filename` | Path | None | Filename for downloaded file |
+| `start` | bool | True | Enqueue the job immediately |
+| `priority` | int | 10 | Starting priority for this job |
+| `access_token` | str | None | Authorization token for this resource |
+| `event_handlers` | List[DownloadEventHandler] | [] | Event handlers for this job |
+
+Internally, `create_download_job()` has a little bit of internal logic that looks at the type of the source and selects the right subclass of `DownloadJobBase` to create and enqueue.
+
+**TODO**: move this logic into its own method for overriding in subclasses.
+
+### Job control
+
+Prior to completion, jobs can be controlled with a series of queue method calls. Do not attempt to modify jobs by directly writing to their fields, as this is likely to lead to unexpected results.
+
+Any method that accepts a job argument may raise an `UnknownJobIDException` if the job has not yet been submitted to the queue or was not created by this queue.
+
+#### queue.join()
+
+This method will block until all the active jobs in the queue have reached a terminal state (completed, errored or cancelled).
+
+#### queue.wait_for_job(job, [timeout])
+
+This method will block until the indicated job has reached a terminal state (completed, errored or cancelled). If the optional timeout is provided, the call will block for at most timeout seconds, and raise a TimeoutError otherwise.
+
+#### jobs = queue.list_jobs()
+
+This will return a list of all jobs, including ones that have not yet been enqueued and those that have completed or errored out.
+
+#### job = queue.id_to_job(int)
+
+This method allows you to recover a submitted job using its ID.
+
+#### queue.prune_jobs()
+
+Remove completed and errored jobs from the job list.
+
+#### queue.start_job(job)
+
+If the job was submitted with `start=False`, then it can be started using this method.
+
+#### queue.pause_job(job)
+
+This will temporarily pause the job, if possible. It can later be restarted and pick up where it left off using `queue.start_job()`.
+
+#### queue.cancel_job(job)
+
+This will cancel the job if possible and clean up temporary files and other resources that it might have been using.
+
+#### queue.start_all_jobs(), queue.pause_all_jobs(), queue.cancel_all_jobs()
+
+This will start/pause/cancel all jobs that have been submitted to the queue and have not yet reached a terminal state.
+
+---
+
+## This Meta be Good: Model Metadata Storage
+
+The modules found under `invokeai.backend.model_manager.metadata` provide a straightforward API for fetching model metadatda from online repositories. Currently only HuggingFace is supported. However, the modules are easily extended for additional repos, provided that they have defined APIs for metadata access.
+
+Metadata comprises any descriptive information that is not essential for getting the model to run. For example "author" is metadata, while "type", "base" and "format" are not. The latter fields are part of the model's config, as defined in `invokeai.backend.model_manager.config`.
+
+### Example Usage
+
+```py
+from invokeai.backend.model_manager.metadata import (
+ AnyModelRepoMetadata,
+)
+# to access the initialized sql database
+from invokeai.app.api.dependencies import ApiDependencies
+
+hf = HuggingFaceMetadataFetch()
+
+# fetch the metadata
+model_metadata = hf.from_id("")
+
+assert isinstance(model_metadata, HuggingFaceMetadata)
+```
+
+### Structure of the Metadata objects
+
+There is a short class hierarchy of Metadata objects, all of which descend from the Pydantic `BaseModel`.
+
+#### `ModelMetadataBase`
+
+This is the common base class for metadata:
+
+| Field Name | Type | Description |
+|------------|------|-------------|
+| `name` | str | Repository's name for the model |
+| `author` | str | Model's author |
+| `tags` | Set[str] | Model tags |
+
+Note that the model config record also has a `name` field. It is intended that the config record version be locally customizable, while the metadata version is read-only. However, enforcing this is expected to be part of the business logic.
+
+Descendents of the base add additional fields.
+
+#### `HuggingFaceMetadata`
+
+This descends from `ModelMetadataBase` and adds the following fields:
+
+| Field Name | Type | Description |
+|------------|------|-------------|
+| `type` | Literal["huggingface"] | Used for the discriminated union of metadata classes |
+| `id` | str | HuggingFace repo_id |
+| `tag_dict` | Dict[str, Any] | A dictionary of tag/value pairs provided in addition to `tags` |
+| `last_modified` | datetime | Date of last commit of this model to the repo |
+| `files` | List[Path] | List of the files in the model repo |
+
+#### `AnyModelRepoMetadata`
+
+This is a discriminated Union of `HuggingFaceMetadata`.
+
+### Fetching Metadata from Online Repos
+
+The `HuggingFaceMetadataFetch` class will retrieve metadata from its corresponding repository and return `AnyModelRepoMetadata` objects. Their base class `ModelMetadataFetchBase` is an abstract class that defines two methods: `from_url()` and `from_id()`. The former accepts the type of model URLs that the user will try to cut and paste into the model import form. The latter accepts a string ID in the format recognized by the repository of choice. Both methods return an `AnyModelRepoMetadata`.
+
+The base class also has a class method `from_json()` which will take the JSON representation of a `ModelMetadata` object, validate it, and return the corresponding `AnyModelRepoMetadata` object.
+
+When initializing one of the metadata fetching classes, you may provide a `requests.Session` argument. This allows you to customize the low-level HTTP fetch requests and is used, for instance, in the testing suite to avoid hitting the internet.
+
+The HuggingFace fetcher subclass add additional repo-specific fetching methods:
+
+#### HuggingFaceMetadataFetch
+
+This overrides its base class `from_json()` method to return a `HuggingFaceMetadata` object directly.
+
+### Metadata Storage
+
+The `ModelConfigBase` stores this response in the `source_api_response` field as a JSON blob.
+
+---
+
+## The Lowdown on the ModelLoadService
+
+The `ModelLoadService` is responsible for loading a named model into memory so that it can be used for inference. Despite the fact that it does a lot under the covers, it is very straightforward to use.
+
+An application-wide model loader is created at API initialization time and stored in `ApiDependencies.invoker.services.model_loader`. However, you can create alternative instances if you wish.
+
+### Creating a ModelLoadService object
+
+The class is defined in `invokeai.app.services.model_load`. It is initialized with an InvokeAIAppConfig object, from which it gets configuration information such as the user's desired GPU and precision, and with a previously-created `ModelRecordServiceBase` object, from which it loads the requested model's configuration information.
+
+Here is a typical initialization pattern:
+
+```py
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.model_load import ModelLoadService, ModelLoaderRegistry
+
+config = InvokeAIAppConfig.get_config()
+
+ram_cache = ModelCache(
+ max_cache_size=config.ram_cache_size, max_vram_cache_size=config.vram_cache_size, logger=logger
+)
+
+convert_cache = ModelConvertCache(
+ cache_path=config.models_convert_cache_path, max_size=config.convert_cache_size
+)
+
+loader = ModelLoadService(
+ app_config=config,
+ ram_cache=ram_cache,
+ convert_cache=convert_cache,
+ registry=ModelLoaderRegistry
+)
+```
+
+### load_model(model_config, [submodel_type], [context]) -> LoadedModel
+
+The `load_model()` method takes an `AnyModelConfig` returned by `ModelRecordService.get_model()` and returns the corresponding loaded model. It loads the model into memory, gets the model ready for use, and returns a `LoadedModel` object.
+
+The optional second argument, `subtype` is a `SubModelType` string enum, such as "vae". It is mandatory when used with a main model, and is used to select which part of the main model to load.
+
+The optional third argument, `context` can be provided by an invocation to trigger model load event reporting. See below for details.
+
+The returned `LoadedModel` object contains a copy of the configuration record returned by the model record`get_model()` method, as well as the in-memory loaded model:
+
+| Attribute Name | Type | Description |
+|----------------|------|-------------|
+| `config` | AnyModelConfig | A copy of the model's configuration record for retrieving base type, etc. |
+| `model` | AnyModel | The instantiated model (details below) |
+
+### get_model_by_key(key, [submodel]) -> LoadedModel
+
+The `get_model_by_key()` method will retrieve the model using its unique database key. For example:
+
+```py
+loaded_model = loader.get_model_by_key('f13dd932c0c35c22dcb8d6cda4203764', SubModelType('vae'))
+```
+
+`get_model_by_key()` may raise any of the following exceptions:
+
+* `UnknownModelException` -- key not in database
+* `ModelNotFoundException` -- key in database but model not found at path
+* `NotImplementedException` -- the loader doesn't know how to load this type of model
+
+### Using the Loaded Model in Inference
+
+`LoadedModel` acts as a context manager. The context loads the model into the execution device (e.g. VRAM on CUDA systems), locks the model in the execution device for the duration of the context, and returns the model. Use it like this:
+
+```py
+loaded_model_= loader.get_model_by_key('f13dd932c0c35c22dcb8d6cda4203764', SubModelType('vae'))
+with loaded_model as vae:
+ image = vae.decode(latents)[0]
+```
+
+The object returned by the LoadedModel context manager is an `AnyModel`, which is a Union of `ModelMixin`, `torch.nn.Module`, `IAIOnnxRuntimeModel`, `IPAdapter`, `IPAdapterPlus`, and `EmbeddingModelRaw`. `ModelMixin` is the base class of all diffusers models, `EmbeddingModelRaw` is used for LoRA and TextualInversion models. The others are obvious.
+
+In addition, you may call `LoadedModel.model_on_device()`, a context manager that returns a tuple of the model's state dict in CPU and the model itself in VRAM. It is used to optimize the LoRA patching and unpatching process:
+
+```py
+loaded_model_= loader.get_model_by_key('f13dd932c0c35c22dcb8d6cda4203764', SubModelType('vae'))
+with loaded_model.model_on_device() as (state_dict, vae):
+ image = vae.decode(latents)[0]
+```
+
+Since not all models have state dicts, the `state_dict` return value can be None.
+
+### Emitting model loading events
+
+When the `context` argument is passed to `load_model_*()`, it will retrieve the invocation event bus from the passed `InvocationContext` object to emit events on the invocation bus. The two events are "model_load_started" and "model_load_completed". Both carry the following payload:
+
+```py
+payload=dict(
+ queue_id=queue_id,
+ queue_item_id=queue_item_id,
+ queue_batch_id=queue_batch_id,
+ graph_execution_state_id=graph_execution_state_id,
+ model_key=model_key,
+ submodel_type=submodel,
+ hash=model_info.hash,
+ location=str(model_info.location),
+ precision=str(model_info.precision),
+)
+```
+
+### Adding Model Loaders
+
+Model loaders are small classes that inherit from the `ModelLoader` base class. They typically implement one method `_load_model()` whose signature is:
+
+```py
+def _load_model(
+ self,
+ model_path: Path,
+ model_variant: Optional[ModelRepoVariant] = None,
+ submodel_type: Optional[SubModelType] = None,
+) -> AnyModel:
+```
+
+`_load_model()` will be passed the path to the model on disk, an optional repository variant (used by the diffusers loaders to select, e.g. the `fp16` variant, and an optional submodel_type for main and onnx models.
+
+To install a new loader, place it in `invokeai/backend/model_manager/load/model_loaders`. Inherit from `ModelLoader` and use the `@ModelLoaderRegistry.register()` decorator to indicate what type of models the loader can handle.
+
+Here is a complete example from `generic_diffusers.py`, which is able to load several different diffusers types:
+
+```py
+from pathlib import Path
+from typing import Optional
+
+from invokeai.backend.model_manager import (
+ AnyModel,
+ BaseModelType,
+ ModelFormat,
+ ModelRepoVariant,
+ ModelType,
+ SubModelType,
+)
+from .. import ModelLoader, ModelLoaderRegistry
+
+
+@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.CLIPVision, format=ModelFormat.Diffusers)
+@ModelLoaderRegistry.register(base=BaseModelType.Any, type=ModelType.T2IAdapter, format=ModelFormat.Diffusers)
+class GenericDiffusersLoader(ModelLoader):
+ """Class to load simple diffusers models."""
+
+ def _load_model(
+ self,
+ model_path: Path,
+ model_variant: Optional[ModelRepoVariant] = None,
+ submodel_type: Optional[SubModelType] = None,
+ ) -> AnyModel:
+ model_class = self._get_hf_load_class(model_path)
+ if submodel_type is not None:
+ raise Exception(f"There are no submodels in models of type {model_class}")
+ variant = model_variant.value if model_variant else None
+ result: AnyModel = model_class.from_pretrained(model_path, torch_dtype=self._torch_dtype, variant=variant) # type: ignore
+ return result
+```
+
+:::note
+ A loader can register itself to handle several different
+ model types. An exception will be raised if more than one loader tries
+ to register the same model type.
+:::
+
+#### Conversion
+
+Some models require conversion to diffusers format before they can be loaded. These loaders should override two additional methods:
+
+```py
+_needs_conversion(self, config: AnyModelConfig, model_path: Path, dest_path: Path) -> bool
+_convert_model(self, config: AnyModelConfig, model_path: Path, output_path: Path) -> Path:
+```
+
+The first method accepts the model configuration, the path to where the unmodified model is currently installed, and a proposed destination for the converted model. This method returns True if the model needs to be converted. It typically does this by comparing the last modification time of the original model file to the modification time of the converted model. In some cases you will also want to check the modification date of the configuration record, in the event that the user has changed something like the scheduler prediction type that will require the model to be re-converted. See `controlnet.py` for an example of this logic.
+
+The second method accepts the model configuration, the path to the original model on disk, and the desired output path for the converted model. It does whatever it needs to do to get the model into diffusers format, and returns the Path of the resulting model. (The path should ordinarily be the same as `output_path`.)
+
+## The ModelManagerService object
+
+For convenience, the API provides a `ModelManagerService` object which gives a single point of access to the major model manager services. This object is created at initialization time and can be found in the global `ApiDependencies.invoker.services.model_manager` object, or in `context.services.model_manager` from within an invocation.
+
+In the examples below, we have retrieved the manager using:
+
+```py
+mm = ApiDependencies.invoker.services.model_manager
+```
+
+The following properties and methods will be available:
+
+### mm.store
+
+This retrieves the `ModelRecordService` associated with the manager. Example:
+
+```py
+configs = mm.store.get_model_by_attr(name='stable-diffusion-v1-5')
+```
+
+### mm.install
+
+This retrieves the `ModelInstallService` associated with the manager. Example:
+
+```py
+job = mm.install.heuristic_import(`https://civitai.com/models/58390/detail-tweaker-lora-lora`)
+```
+
+### mm.load
+
+This retrieves the `ModelLoaderService` associated with the manager. Example:
+
+```py
+configs = mm.store.get_model_by_attr(name='stable-diffusion-v1-5')
+assert len(configs) > 0
+
+loaded_model = mm.load.load_model(configs[0])
+```
+
+The model manager also offers a few convenience shortcuts for loading models:
+
+### mm.load_model_by_config(model_config, [submodel], [context]) -> LoadedModel
+
+Same as `mm.load.load_model()`.
+
+### mm.load_model_by_attr(model_name, base_model, model_type, [submodel], [context]) -> LoadedModel
+
+This accepts the combination of the model's name, type and base, which it passes to the model record config store for retrieval. If a unique model config is found, this method returns a `LoadedModel`. It can raise the following exceptions:
+
+- `UnknownModelException` -- model with these attributes not known
+- `NotImplementedException` -- the loader doesn't know how to load this type of model
+- `ValueError` -- more than one model matches this combination of base/type/name
+
+### mm.load_model_by_key(key, [submodel], [context]) -> LoadedModel
+
+This method takes a model key, looks it up using the `ModelRecordServiceBase` object in `mm.store`, and passes the returned model configuration to `load_model_by_config()`. It may raise a `NotImplementedException`.
+
+## Invocation Context Model Manager API
+
+Within invocations, the following methods are available from the `InvocationContext` object:
+
+### context.download_and_cache_model(source) -> Path
+
+This method accepts a `source` of a remote model, downloads and caches it locally, and then returns a Path to the local model. The source can be a direct download URL or a HuggingFace repo_id.
+
+In the case of HuggingFace repo_id, the following variants are recognized:
+
+* stabilityai/stable-diffusion-v4 -- default model
+* stabilityai/stable-diffusion-v4:fp16 -- fp16 variant
+* stabilityai/stable-diffusion-v4:fp16:vae -- the fp16 vae subfolder
+* stabilityai/stable-diffusion-v4:onnx:vae -- the onnx variant vae subfolder
+
+You can also point at an arbitrary individual file within a repo_id directory using this syntax:
+
+* stabilityai/stable-diffusion-v4::/checkpoints/sd4.safetensors
+
+### context.load_local_model(model_path, [loader]) -> LoadedModel
+
+This method loads a local model from the indicated path, returning a `LoadedModel`. The optional loader is a Callable that accepts a Path to the object, and returns a `AnyModel` object. If no loader is provided, then the method will use `torch.load()` for a .ckpt or .bin checkpoint file, `safetensors.torch.load_file()` for a safetensors checkpoint file, or `cls.from_pretrained()` for a directory that looks like a diffusers directory.
+
+### context.load_remote_model(source, [loader]) -> LoadedModel
+
+This method accepts a `source` of a remote model, downloads and caches it locally, loads it, and returns a `LoadedModel`. The source can be a direct download URL or a HuggingFace repo_id.
+
+In the case of HuggingFace repo_id, the following variants are recognized:
+
+* stabilityai/stable-diffusion-v4 -- default model
+* stabilityai/stable-diffusion-v4:fp16 -- fp16 variant
+* stabilityai/stable-diffusion-v4:fp16:vae -- the fp16 vae subfolder
+* stabilityai/stable-diffusion-v4:onnx:vae -- the onnx variant vae subfolder
+
+You can also point at an arbitrary individual file within a repo_id directory using this syntax:
+
+* stabilityai/stable-diffusion-v4::/checkpoints/sd4.safetensors
diff --git a/docs/src/content/docs/development/Architecture/overview.mdx b/docs/src/content/docs/development/Architecture/overview.mdx
new file mode 100644
index 00000000000..8cbe18efad5
--- /dev/null
+++ b/docs/src/content/docs/development/Architecture/overview.mdx
@@ -0,0 +1,104 @@
+---
+title: Architecture Overview
+sidebar:
+ order: 1
+ label: Overview
+
+lastUpdated: 2026-02-18
+---
+
+import Mermaid from '@components/Mermaid.astro'
+
+
+```mermaid
+flowchart TB
+
+ subgraph apps[Applications]
+ webui[WebUI]
+ cli[CLI]
+
+ subgraph webapi[Web API]
+ api[HTTP API]
+ sio[Socket.IO]
+ end
+
+ end
+
+ subgraph invoke[Invoke]
+ direction LR
+ invoker
+ services
+ sessions
+ invocations
+ end
+
+ subgraph core[AI Core]
+ Generate
+ end
+
+ webui --> webapi
+ webapi --> invoke
+ cli --> invoke
+
+ invoker --> services & sessions
+ invocations --> services
+ sessions --> invocations
+
+ services --> core
+
+ %% Styles
+ classDef sg fill:#5028C8,font-weight:bold,stroke-width:2,color:#fff,stroke:#14141A
+ classDef default stroke-width:2px,stroke:#F6B314,color:#fff,fill:#14141A
+
+ class apps,webapi,invoke,core sg
+
+```
+
+
+## Applications
+
+Applications are built on top of the invoke framework. They should construct `invoker` and then interact through it. They should avoid interacting directly with core code in order to support a variety of configurations.
+
+### Web UI
+
+The Web UI is built on top of an HTTP API built with [FastAPI](https://fastapi.tiangolo.com/) and [Socket.IO](https://socket.io/). The frontend code is found in `/invokeai/frontend` and the backend code is found in `/invokeai/app/api_app.py` and `/invokeai/app/api/`. The code is further organized as such:
+
+| Component | Description |
+| --- | --- |
+| api_app.py | Sets up the API app, annotates the OpenAPI spec with additional data, and runs the API |
+| dependencies | Creates all invoker services and the invoker, and provides them to the API |
+| events | An eventing system that could in the future be adapted to support horizontal scale-out |
+| sockets | The Socket.IO interface - handles listening to and emitting session events (events are defined in the events service module) |
+| routers | API definitions for different areas of API functionality |
+
+### CLI
+
+The CLI is built automatically from invocation metadata, and also supports invocation piping and auto-linking. Code is available in `/invokeai/frontend/cli`.
+
+## Invoke
+
+The Invoke framework provides the interface to the underlying AI systems and is built with flexibility and extensibility in mind. There are four major concepts: invoker, sessions, invocations, and services.
+
+### Invoker
+
+The invoker (`/invokeai/app/services/invoker.py`) is the primary interface through which applications interact with the framework. Its primary purpose is to create, manage, and invoke sessions. It also maintains two sets of services:
+- **invocation services**, which are used by invocations to interact with core functionality.
+- **invoker services**, which are used by the invoker to manage sessions and manage the invocation queue.
+
+### Sessions
+
+Invocations and links between them form a graph, which is maintained in a session. Sessions can be queued for invocation, which will execute their graph (either the next ready invocation, or all invocations). Sessions also maintain execution history for the graph (including storage of any outputs). An invocation may be added to a session at any time, and there is capability to add and entire graph at once, as well as to automatically link new invocations to previous invocations. Invocations can not be deleted or modified once added.
+
+The session graph does not support looping. This is left as an application problem to prevent additional complexity in the graph.
+
+### Invocations
+
+Invocations represent individual units of execution, with inputs and outputs. All invocations are located in `/invokeai/app/invocations`, and are all automatically discovered and made available in the applications. These are the primary way to expose new functionality in Invoke.AI, and the [implementation guide](/development/architecture/invocations/) explains how to add new invocations.
+
+### Services
+
+Services provide invocations access AI Core functionality and other necessary functionality (e.g. image storage). These are available in `/invokeai/app/services`. As a general rule, new services should provide an interface as an abstract base class, and may provide a lightweight local implementation by default in their module. The goal for all services should be to enable the usage of different implementations (e.g. using cloud storage for image storage), but should not load any module dependencies unless that implementation has been used (i.e. don't import anything that won't be used, especially if it's expensive to import).
+
+## AI Core
+
+The AI Core is represented by the rest of the code base (i.e. the code outside of `/invokeai/app/`).
diff --git a/docs/src/content/docs/development/Documentation/index.mdx b/docs/src/content/docs/development/Documentation/index.mdx
new file mode 100644
index 00000000000..ae8b5248c1d
--- /dev/null
+++ b/docs/src/content/docs/development/Documentation/index.mdx
@@ -0,0 +1,314 @@
+---
+title: Documentation
+lastUpdated: 2026-05-14
+---
+
+import { Steps, Tabs, TabItem, FileTree } from '@astrojs/starlight/components'
+
+The Invoke AI website, including its documentation are all contained within the `docs` directory.
+
+## Prerequisites
+
+The documentation is built using [Astro Starlight](https://starlight.astro.build/). It's suggested you familiarize yourself with the following technologies before getting started:
+
+
+ 1. [Markdown](https://www.markdownguide.org/) - a lightweight markup language for creating formatted text.
+ 2. [MDX](https://mdxjs.com/) - a superset of Markdown that allows you to use React components in your content.
+ 3. [Astro](https://astro.build/) - a modern static site builder that supports MDX and other front-end technologies.
+ 4. [Starlight](https://starlight.astro.build/) - a theme for Astro that provides a clean and modern documentation experience.
+ 5. [Vite](https://vitejs.dev/) - a fast development server and build tool for modern web projects.
+
+
+Markdown powers the content of every page on the website (including the homepage), with additional help from [MDX](https://mdxjs.com/) to make the pages more interactive with imported React components.
+
+## Navigating the Documentation
+
+The documentation is organized into a file tree structure. It should be very familiar to anyone who has built modern web applications.
+
+
+ - docs/
+ - dist/ production build output
+ - public/ non-optimized, public assets
+ - src/ main source code
+ - assets/ optimized assets
+ - config/ astro/starlight configs
+ - content/ markdown pages and content
+ - docs/ documentation content
+ - i18n/ internationalized content
+ - generated/ generated json files for dynamic content
+ - layouts/ components used to wrap pages
+ - lib/ utility functions and shared code
+ - components/ reusable, custom components
+ - pages/ non-documentation pages
+ - styles/ global styles and themes
+
+
+## Development
+
+If you've ever worked within a react, astro or similar node-based library or framework, you should feel familiar with most of the setup here.
+
+If you're adding a feature, new behavior or etc. that changes how users expect Invoke to work, we expect you to deliver your PR with associated docs to support it. To get started, follow the steps below.
+
+### Dev Environment
+
+There are 2 main ways to get your development environment set up for documentation:
+
+
+
+ Invoke's makefile makes it easy to set up your development environment for documentation in only a couple of commands. You can run these from the root of the repository.
+
+
+ 1. First, install the required dependencies.
+
+ ```sh
+ make docs-install
+ ```
+
+ 2. Next, run the development server.
+
+ ```sh
+ make docs-dev
+ ```
+
+ 3. Open your browser and navigate to `http://localhost:4321` to view the documentation.
+
+
+
+
+ If you prefer good ol' fashioned `cd` and `pnpm` commands, you can set up your development environment manually.
+
+
+ 1. First, cd into the docs directory.
+
+ ```sh
+ cd docs
+ ```
+
+ 2. Next, install the required dependencies.
+
+ ```sh
+ pnpm install
+ ```
+
+ 3. Run the development server.
+
+ ```sh
+ pnpm dev
+ ```
+
+ 4. Open your browser and navigate to `http://localhost:4321` to view the documentation.
+
+
+
+
+If there's another local server running on port `4321` prior to running this, then use the port specified in the output.
+
+### Adding Pages
+
+Located within the `src/content/docs/` directory, this is where the documentation pages are stored and organized by category. These categories are file-based and are mirrored to the sidebar navigation.
+
+:::caution
+Do not place your docs content contributions outside of the `content` directory, it will not be seen.
+:::
+
+If you wish to add a new sub category to document a feature or a behavior, simply create a new directory within the relevant top-level category directory.
+
+For example, if we wanted to document a new feature called "Instant Bananas", we would create a new directory within `src/content/docs/features/` like so:
+
+`src/content/docs/`
+
+ - concepts/
+ - configuration/
+ - contributing/
+ - development/
+ - features/
+ - **instant-bananas/**
+ - **index.md** Write your documentation here
+ - **requirements.mdx** You can add more pages in this directory
+ - start-here/
+ - troubleshooting/
+ - workflows/
+
+
+The way you organize your added pages dictates how the URL structure is generated for your documentation pages. In this example, the url for the `index.md` page would be `https://invoke.ai/features/instant-bananas/`, and the url for the `requirements.mdx` page would be `https://invoke.ai/features/instant-bananas/requirements/`.
+
+If you wish to add a top-level category, then one additional step is required for the category to appear in the sidebar.
+
+Within the `src/config/sidebar.ts` file, you'll need to add a new sidebar category object to the array, since the fine-grained control over top-level categories needs to be a bit more explicit.
+
+```diff lang="js"
+const sidebar = [
+ // ...
+ {
+ label: 'Concepts',
+ items: [
+ {
+ autogenerate: { directory: 'concepts' },
+ },
+ ],
+ },
++ {
++ label: 'A New Category',
++ items: [
++ {
++ autogenerate: { directory: 'new-category' },
++ },
++ ],
++ },
+ {
+ label: 'Features',
+ items: [
+ {
+ autogenerate: { directory: 'features' },
+ },
+ ],
+ },
+ // ...
+]
+```
+
+### Page Metadata
+
+Before your page becomes available, you will need to add frontmatter to define the page's metadata such as its title, description, last update date, sidebar position, and etc.
+
+Learn more about what frontmatter is and how to use it in your pages in the [Starlight Documentation](https://starlight.astro.build/reference/frontmatter/).
+
+Once you have some basic frontmatter defined, you should be able to see it reflected in the sidebar and the page title.
+
+### Adding Images
+
+We encourage adding imagery to your docs for creating a more engaging and visual experience for viewers. To add images, we prefer you to utilize an `assets` directory within the concerning category.
+
+
+ - features/
+ - instant-bananas/
+ - **assets/**
+ - **demonstration.webp**
+ - **foobar.avif**
+ - index.mdx
+
+
+The Astro image optimizer/renderer is quite flexible with image formats and sizes, but we'd prefer stored images to be at reasonable sizes (not 4k), and using optimized formats (webp, avif, jpeg).
+
+To render the image, you'd just use a relative path in your markdown.
+
+```md title="index.mdx"
+
+```
+
+### Adding Translations
+
+Currently, the documentation is only available in English. If you wish to add translations for other languages, we've already laid the ground work for you to do so.
+
+Firstly, add a new folder within the `src/content/i18n` directory, and create your translated version of the markdown file into the same path as the original.
+
+For example:
+
+
+ - src/
+ - content/
+ - docs/
+ - start-here/
+ - installation.mdx
+ - i18n/
+ - zh-CN Country code here
+ - start-here/
+ - installation.mdx
+
+
+We recommend simply copy/pasting the file and rewriting the text from there.
+
+Learn more about the intricacies of translating Astro Starlight docs [here](https://starlight.astro.build/guides/i18n).
+
+## Running a Build
+
+Modifications to the docs may run fine on your machine, but as we've learned the hard way, GitHub pages flips that expectation completely. So, we've added some ways to ensure things work as expected before deploying.
+
+Just like with the dev environment, you can build the docs one of two ways:
+
+
+
+ Invoke's makefile makes it easy to build the documentation in only a single command. You can run it from the root of the repository.
+
+
+ 1. First, run the build command.
+
+ ```sh
+ make docs-build
+ ```
+
+ 2. Finally, preview the output.
+
+ ```sh
+ make docs-preview
+ ```
+
+
+ And that's it.
+
+ :::tip[Deploy Target]
+ The make command here sets the `DEPLOY_TARGET` environment variable to `custom`, so that the final output matches what you'd expect from the final deployment to https://invoke.ai.
+
+ If you'd rather set a different deploy target, use the manual method.
+ :::
+
+
+
+ If you prefer good ol' fashioned `cd` and `pnpm` commands, or to have granular control over environment variables, you can run the following:
+
+
+ 1. First, cd into the `docs` directory.
+
+ ```sh
+ cd docs
+ ```
+
+ 2. Next run the build command.
+
+ ```sh
+ pnpm run build
+ ```
+
+ 3. Finally, preview the build.
+
+ ```sh
+ pnpm run preview
+ ```
+
+ The preview url will be available on the same port as the dev server.
+
+
+
+
+
+## Generated Files
+
+The Invoke API is always evolving, and quite large. Documenting all this by hand would be wildly impractical, so there's a script we've set up to pull all that data and generate relevant json files into `generated` directory.
+
+These files are used for the [YAML Config](/configuration/invokeai-yaml) and [API Development](/development/guides/api-development) pages. If you're adding a feature that changes the yaml config, or the api then make sure to run `pnpm run generate-docs-data` to ensure tests pass, and that the docs are accurate in accordance to your updates.
+
+## Testing
+
+The docs contain tests for the following:
+
+| Test | Description | Runs on... |
+| -- | -- | -- |
+| Link Checker | Checks for invalid, malformed or misdirected internal link URLs | Dev Server, Build, Deploy |
+| Verify Deployment Output | Check to ensure the asset and page paths have the expected base paths dependent on deploy targets | Build, Deploy |
+| Check Docs Data | Checks to ensure the generated files are accurate | Deploy |
+
+## GitHub Actions
+
+Once you've submitted your updated docs, either via pull request or a main push to your own fork, the `deploy-docs` action will run.
+
+The `deploy-docs` action will install the necessary dependencies, run a build, test and serve the docs on github pages. Any failing deployments will require fixing before deploying.
+
+## Troubleshooting
+
+#### All the styles are missing and the links are wrong, what happened?
+
+This commonly happens when the base path and the deploy target are mismatched, check those first and then run your build again.
+
+#### Redirects aren't working on the production deployment, but they work locally, why?
+
+Because GitHub Pages' SSR environment is lackluster, and thus doesn't handle backend redirects. We included a redirects configuration just in case GitHub ever grows a conscience, or if the docs ever get deployed someplace else.
diff --git a/docs/src/content/docs/development/Front End/canvas-projects.mdx b/docs/src/content/docs/development/Front End/canvas-projects.mdx
new file mode 100644
index 00000000000..b36c2ce9720
--- /dev/null
+++ b/docs/src/content/docs/development/Front End/canvas-projects.mdx
@@ -0,0 +1,54 @@
+---
+title: Canvas Projects
+---
+
+Canvas projects serialize the current canvas into a portable `.invk` archive. The feature lives in `invokeai/frontend/web/src/features/controlLayers/` and is exposed in the canvas toolbar archive menu and the canvas context menu under **Project**.
+
+## File format
+
+`.invk` files are ZIP archives. The current manifest version is `1`.
+
+Each archive contains:
+
+| Target | Description |
+| - | - |
+| `manifest.json` | project metadata, including the archive version, app version, creation timestamp, and project name. |
+| `canvas_state.json` | raster layers, control layers, inpaint masks, regional guidance, bounding box state, and selected/bookmarked entity identifiers. |
+| `params.json` | generation parameter state. |
+| `ref_images.json` | global reference image state. |
+| `loras.json` | active LoRA state. |
+| `images/` | image blobs referenced by the canvas or reference image state. |
+
+The save path builds this archive in `useCanvasProjectSave.ts`. It collects all referenced `image_name` values, fetches each image from the server, writes successfully fetched files under `images/`, and downloads the ZIP with the `.invk` extension. Failed image fetches are logged and skipped rather than aborting the save.
+
+## Image collection
+
+Image references are collected by `collectImageNames()` in `canvasProjectFile.ts`.
+
+The collector checks:
+
+- Image objects in raster layers.
+- Image objects in control layers.
+- Image objects in inpaint masks.
+- Image objects and IP Adapter / Flux Redux reference images in regional guidance.
+- Global reference images, including cropped source images.
+
+Image fetches are concurrency-limited with `processWithConcurrencyLimit()` so large projects do not flood the browser or backend with simultaneous requests.
+
+## Loading and remapping
+
+The load path is implemented in `useCanvasProjectLoad.ts`.
+
+Loading validates `manifest.json`, requires `canvas_state.json`, and reads optional `params.json`, `ref_images.json`, and `loras.json` files. Before restoring state, it checks whether each referenced image already exists on the server with `checkExistingImages()`.
+
+Only missing images are uploaded from the archive. If a referenced missing image is not present in `images/`, the loader logs a warning and leaves that reference unchanged. If an upload returns a different `image_name`, the loader records an old-to-new mapping and remaps image references before dispatching restored canvas and reference image state.
+
+LoRAs are cleared before project LoRAs are recalled. This prevents LoRAs from the previous canvas session from leaking into the loaded project.
+
+Image existence checks and uploads are also concurrency-limited.
+
+## Compatibility notes
+
+The archive stores references to models, LoRAs, and other generation resources, not the model files themselves. Loading a project on another install can restore the canvas images and state, but missing model resources still need to be installed or replaced by the user.
+
+Future format changes should increment `CANVAS_PROJECT_VERSION` and keep validation in `parseManifest()` explicit so unsupported project files fail early.
diff --git a/docs/src/content/docs/development/Front End/index.md b/docs/src/content/docs/development/Front End/index.md
new file mode 100644
index 00000000000..4e12e59efe2
--- /dev/null
+++ b/docs/src/content/docs/development/Front End/index.md
@@ -0,0 +1,131 @@
+---
+title: Frontend Development
+lastUpdated: 2026-02-18
+---
+
+Invoke's UI is made possible by many contributors and open-source libraries. Thank you!
+
+## Dev environment
+
+Follow the [dev environment](/development/setup/dev-environment/) guide to get set up. Run the UI using `pnpm dev`.
+
+## Package scripts
+
+- `dev`: run the frontend in dev mode, enabling hot reloading
+- `build`: run all checks (dpdm, eslint, prettier, tsc, knip) and then build the frontend
+- `lint:dpdm`: check circular dependencies
+- `lint:eslint`: check code quality
+- `lint:prettier`: check code formatting
+- `lint:tsc`: check type issues
+- `lint:knip`: check for unused exports or objects
+- `lint`: run all checks concurrently
+- `fix`: run `eslint` and `prettier`, fixing fixable issues
+- `test:ui`: run `vitest` with the fancy web UI
+
+## Type generation
+
+We use [openapi-typescript] to generate types from the app's OpenAPI schema. The generated types are committed to the repo in [schema.ts].
+
+If you make backend changes, it's important to regenerate the frontend types:
+
+```sh
+cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
+```
+
+On macOS and Linux, you can run `make frontend-typegen` as a shortcut for the above snippet.
+
+## Localization
+
+We use [i18next] for localization, but translation to languages other than English happens on our [Weblate] project.
+
+Only the English source strings (i.e. `en.json`) should be changed on this repo.
+
+## VSCode
+
+### Example debugger config
+
+```jsonc
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "chrome",
+ "request": "launch",
+ "name": "Invoke UI",
+ "url": "http://localhost:5173",
+ "webRoot": "${workspaceFolder}/invokeai/frontend/web"
+ }
+ ]
+}
+```
+
+### Remote dev
+
+We've noticed an intermittent timeout issue with the VSCode remote dev port forwarding.
+
+We suggest disabling the editor's port forwarding feature and doing it manually via SSH:
+
+```sh
+ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@host
+```
+
+## Contributing Guidelines
+
+Thanks for your interest in contributing to the Invoke Web UI!
+
+Please follow these guidelines when contributing.
+
+## Check in before investing your time
+
+Please check in before you invest your time on anything besides a trivial fix, in case it conflicts with ongoing work or isn't aligned with the vision for the app.
+
+If a feature request or issue doesn't already exist for the thing you want to work on, please create one.
+
+Ping `@psychedelicious` on [discord] in the `#frontend-dev` channel or in the feature request / issue you want to work on - we're happy to chat.
+
+## Code conventions
+
+- This is a fairly complex app with a deep component tree. Please use memoization (`useCallback`, `useMemo`, `memo`) with enthusiasm.
+- If you need to add some global, ephemeral state, please use [nanostores] if possible.
+- Be careful with your redux selectors. If they need to be parameterized, consider creating them inside a `useMemo`.
+- Feel free to use `lodash` (via `lodash-es`) to make the intent of your code clear.
+- Please add comments describing the "why", not the "how" (unless it is really arcane).
+
+## Commit format
+
+Please use the [conventional commits] spec for the web UI, with a scope of "ui":
+
+- `chore(ui): bump deps`
+- `chore(ui): lint`
+- `feat(ui): add some cool new feature`
+- `fix(ui): fix some bug`
+
+## Tests
+
+We don't do any UI testing at this time, but consider adding tests for sensitive logic.
+
+We use `vitest`, and tests should be next to the file they are testing. If the logic is in `something.ts`, the tests should be in `something.test.ts`.
+
+In some situations, we may want to test types. For example, if you use `zod` to create a schema that should match a generated type, it's best to add a test to confirm that the types match. Use `tsafe`'s assert for this.
+
+## Submitting a PR
+
+- Ensure your branch is tidy. Use an interactive rebase to clean up the commit history and reword the commit messages if they are not descriptive.
+- Run `pnpm lint`. Some issues are auto-fixable with `pnpm fix`.
+- Fill out the PR form when creating the PR.
+ - It doesn't need to be super detailed, but a screenshot or video is nice if you changed something visually.
+ - If a section isn't relevant, delete it.
+
+## Other docs
+
+- [Workflows - Design and Implementation]
+- [State Management]
+
+[discord]: https://discord.gg/ZmtBAhwWhy
+[i18next]: https://github.com/i18next/react-i18next
+[Weblate]: https://hosted.weblate.org/engage/invokeai/
+[openapi-typescript]: https://github.com/openapi-ts/openapi-typescript
+[schema.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/services/api/schema.ts
+[conventional commits]: https://www.conventionalcommits.org/en/v1.0.0/
+[Workflows - Design and Implementation]: ./workflows/
+[State Management]: ./state-management/
diff --git a/docs/src/content/docs/development/Front End/state-management.mdx b/docs/src/content/docs/development/Front End/state-management.mdx
new file mode 100644
index 00000000000..96fede7ba7f
--- /dev/null
+++ b/docs/src/content/docs/development/Front End/state-management.mdx
@@ -0,0 +1,41 @@
+---
+title: State Management
+lastUpdated: 2026-02-18
+---
+
+The app makes heavy use of Redux Toolkit, its Query library, and `nanostores`.
+
+## Redux
+
+We use RTK extensively - slices, entity adapters, queries, reselect, the whole 9 yards. Their [docs](https://redux-toolkit.js.org/) are excellent.
+
+## `nanostores`
+
+[nanostores] is a tiny state management library. It provides both imperative and declarative APIs.
+
+### Example
+
+```ts
+export const $myStringOption = atom(null);
+
+// Outside a component, or within a callback for performance-critical logic
+$myStringOption.get();
+$myStringOption.set('new value');
+
+// Inside a component
+const myStringOption = useStore($myStringOption);
+```
+
+### Where to put nanostores
+
+- For global application state, export your stores from `invokeai/frontend/web/src/app/store/nanostores/`.
+- For feature state, create a file for the stores next to the redux slice definition (e.g. `invokeai/frontend/web/src/features/myFeature/myFeatureNanostores.ts`).
+- For hooks with global state, export the store from the same file the hook is in, or put it next to the hook.
+
+### When to use nanostores
+
+- For non-serializable data that needs to be available throughout the app, use `nanostores` instead of a global.
+- For ephemeral global state (i.e. state that does not need to be persisted), use `nanostores` instead of redux.
+- For performance-critical code and in callbacks, redux selectors can be problematic due to the declarative reactivity system. Consider refactoring to use `nanostores` if there's a **measurable** performance issue.
+
+[nanostores]: https://github.com/nanostores/nanostores/
diff --git a/docs/src/content/docs/development/Front End/text-tool.mdx b/docs/src/content/docs/development/Front End/text-tool.mdx
new file mode 100644
index 00000000000..5b19bbeef9f
--- /dev/null
+++ b/docs/src/content/docs/development/Front End/text-tool.mdx
@@ -0,0 +1,37 @@
+---
+title: "Canvas Text Tool"
+---
+
+## Overview
+
+The canvas text workflow is split between a Konva module that owns tool state and a React overlay that handles text entry.
+
+- `invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts`
+ - Owns the tool, cursor preview, and text session state (including the cursor "T" marker).
+ - Manages dynamic cursor contrast, starts sessions on pointer down, and commits sessions by rasterizing the active text block into a new raster layer.
+- `invokeai/frontend/web/src/features/controlLayers/components/Text/CanvasTextOverlay.tsx`
+ - Renders the on-canvas editor as a `contentEditable` overlay positioned in canvas space.
+ - Syncs keyboard input, suppresses app hotkeys, and forwards commits/cancels to the Konva module.
+- `invokeai/frontend/web/src/features/controlLayers/components/Text/TextToolOptions.tsx`
+ - Provides the font dropdown, size slider/input, formatting toggles, and alignment buttons that appear when the Text tool is active.
+
+## Rasterization pipeline
+
+`renderTextToCanvas()` (`invokeai/frontend/web/src/features/controlLayers/text/textRenderer.ts`) converts the editor contents into a transparent canvas. The Text tool module configures the renderer with the active font stack, weight, styling flags, alignment, and the active canvas color. The resulting canvas is encoded to a PNG data URL and stored in a new raster layer (`image` object) with a transparent background.
+
+Layer placement preserves the original click location:
+
+- The session stores the anchor coordinate (where the user clicked) and current alignment.
+- `calculateLayerPosition()` calculates the top-left position for the raster layer after applying the configured padding and alignment offsets.
+- New layers are inserted directly above the currently-selected raster layer (when present) and selected automatically.
+
+## Font stacks
+
+Font definitions live in `invokeai/frontend/web/src/features/controlLayers/text/textConstants.ts` as ten deterministic stacks (sans, serif, mono, rounded, script, humanist, slab serif, display, narrow, UI serif). Each stack lists system-safe fallbacks so the editor can choose the first available font per platform.
+
+To add or adjust fonts:
+
+1. Update `TEXT_FONT_STACKS` with the new `id`, `label`, and CSS `font-family` stack.
+2. If you add a new stack, extend the `TEXT_FONT_IDS` tuple and update the `canvasTextSlice` schema default (`TEXT_DEFAULT_FONT_ID`).
+3. Provide translation strings for any new labels in `public/locales/*`.
+4. The editor and renderer will automatically pick up the new stack via `getFontStackById()`.
diff --git a/docs/src/content/docs/development/Front End/workflows.mdx b/docs/src/content/docs/development/Front End/workflows.mdx
new file mode 100644
index 00000000000..d083bb8df1e
--- /dev/null
+++ b/docs/src/content/docs/development/Front End/workflows.mdx
@@ -0,0 +1,315 @@
+---
+title: Workflows
+lastUpdated: 2026-02-18
+---
+
+This document describes, at a high level, the design and implementation of workflows in the InvokeAI frontend. There are a substantial number of implementation details not included, but which are hopefully clear from the code.
+
+InvokeAI's backend uses graphs, composed of **nodes** and **edges**, to process data and generate images.
+
+Nodes have any number of **input fields** and **output fields**. Edges connect nodes together via their inputs and outputs. Fields have data types which dictate how they may be connected.
+
+During execution, a nodes' outputs may be passed along to any number of other nodes' inputs.
+
+Workflows are an enriched abstraction over a graph.
+
+## Design
+
+InvokeAI provide two ways to build graphs in the frontend: the [Linear UI](#linear-ui) and [Workflow Editor](#workflow-editor).
+
+To better understand the use case and challenges related to workflows, we will review both of these modes.
+
+### Linear UI
+
+This includes the **Text to Image**, **Image to Image** and **Unified Canvas** tabs.
+
+The user-managed parameters on these tabs are stored as simple objects in the application state. When the user invokes, adding a generation to the queue, we internally build a graph from these parameters.
+
+This logic can be fairly complex due to the range of features available and their interactions. Depending on the parameters selected, the graph may be very different. Building graphs in code can be challenging - you are trying to construct a non-linear structure in a linear context.
+
+The simplest graph building logic is for **Text to Image** with a SD1.5 model: [buildLinearTextToImageGraph.ts]
+
+There are many other graph builders in the same directory for different tabs or base models (e.g. SDXL). Some are pretty hairy.
+
+In the Linear UI, we go straight from **simple application state** to **graph** via these builders.
+
+### Workflow Editor
+
+The Workflow Editor is a visual graph editor, allowing users to draw edges from node to node to construct a graph. This _far_ more approachable way to create complex graphs.
+
+InvokeAI uses the [reactflow] library to power the Workflow Editor. It provides both a graph editor UI and manages its own internal graph state.
+
+#### Workflows
+
+A workflow is a representation of a graph plus additional metadata:
+
+- Name
+- Description
+- Version
+- Notes
+- [Exposed fields](#workflow-linear-view)
+- Author, tags, category, etc.
+
+Workflows should have other qualities:
+
+- Portable: you should be able to load a workflow created by another person.
+- Resilient: you should be able to "upgrade" a workflow as the application changes.
+- Abstract: as much as is possible, workflows should not be married to the specific implementation details of the application.
+
+To support these qualities, workflows are serializable, have a versioned schemas, and represent graphs as minimally as possible. Fortunately, the reactflow state for nodes and edges works perfectly for this.
+
+##### Workflow -> reactflow state -> InvokeAI graph
+
+Given a workflow, we need to be able to derive reactflow state and/or an InvokeAI graph from it.
+
+The first step - workflow to reactflow state - is very simple. The logic is in [nodesSlice.ts], in the `workflowLoaded` reducer.
+
+The reactflow state is, however, structurally incompatible with our backend's graph structure. When a user invokes on a Workflow, we need to convert the reactflow state into an InvokeAI graph. This is far simpler than the graph building logic from the Linear UI:
+[buildNodesGraph.ts]
+
+##### Nodes vs Invocations
+
+We often use the terms "node" and "invocation" interchangeably, but they may refer to different things in the frontend.
+
+reactflow [has its own definitions][reactflow-concepts] of "node", "edge" and "handle" which are closely related to InvokeAI graph concepts.
+
+- A reactflow node is related to an InvokeAI invocation. It has a "data" property, which holds the InvokeAI-specific invocation data.
+- A reactflow edge is roughly equivalent to an InvokeAI edge.
+- A reactflow handle is roughly equivalent to an InvokeAI input or output field.
+
+##### Workflow Linear View
+
+Graphs are very capable data structures, but not everyone wants to work with them all the time.
+
+To allow less technical users - or anyone who wants a less visually noisy workspace - to benefit from the power of nodes, InvokeAI has a workflow feature called the Linear View.
+
+A workflow input field can be added to this Linear View, and its input component can be presented similarly to the Linear UI tabs. Internally, we add the field to the workflow's list of exposed fields.
+
+#### OpenAPI Schema
+
+OpenAPI is a schema specification that can represent complex data structures and relationships. The backend is capable of generating an OpenAPI schema for all invocations.
+
+When the UI connects, it requests this schema and parses each invocation into an **invocation template**. Invocation templates have a number of properties, like title, description and type, but the most important ones are their input and output **field templates**.
+
+Invocation and field templates are the "source of truth" for graphs, because they indicate what the backend is able to process.
+
+When a user adds a new node to their workflow, these templates are used to instantiate a node with fields instantiated from the input and output field templates.
+
+##### Field Instances and Templates
+
+Field templates consist of:
+
+- Name: the identifier of the field, its variable name in python
+- Type: derived from the field's type annotation in python (e.g. IntegerField, ImageField, MainModelField)
+- Constraints: derived from the field's creation args in python (e.g. minimum value for an integer)
+- Default value: optionally provided in the field's creation args (e.g. 42 for an integer)
+
+Field instances are created from the templates and have name, type and optionally a value.
+
+The type of the field determines the UI components that are rendered for it.
+
+A field instance's name associates it with its template.
+
+##### Stateful vs Stateless Fields
+
+**Stateful** fields store their value in the frontend graph. Think primitives, model identifiers, images, etc. Fields are only stateful if the frontend allows the user to directly input a value for them.
+
+Many field types, however, are **stateless**. An example is a `UNetField`, which contains some data describing a UNet. Users cannot directly provide this data - it is created and consumed in the backend.
+
+Stateless fields do not store their value in the node, so their field instances do not have values.
+
+"Custom" fields will always be treated as stateless fields.
+
+##### Single and Collection Fields
+
+Field types have a name and cardinality property which may identify it as a **SINGLE**, **COLLECTION** or **SINGLE_OR_COLLECTION** field.
+
+- If a field is annotated in python as a singular value or class, its field type is parsed as a **SINGLE** type (e.g. `int`, `ImageField`, `str`).
+- If a field is annotated in python as a list, its field type is parsed as a **COLLECTION** type (e.g. `list[int]`).
+- If it is annotated as a union of a type and list, the type will be parsed as a **SINGLE_OR_COLLECTION** type (e.g. `Union[int, list[int]]`). Fields may not be unions of different types (e.g. `Union[int, list[str]]` and `Union[int, str]` are not allowed).
+
+## Implementation
+
+The majority of data structures in the backend are [pydantic] models. Pydantic provides OpenAPI schemas for all models and we then generate TypeScript types from those.
+
+The OpenAPI schema is parsed at runtime into our invocation templates.
+
+Workflows and all related data are modeled in the frontend using [zod]. Related types are inferred from the zod schemas.
+
+> In python, invocations are pydantic models with fields. These fields become node inputs. The invocation's `invoke()` function returns a pydantic model - its output. Like the invocation itself, the output model has any number of fields, which become node outputs.
+
+### zod Schemas and Types
+
+The zod schemas, inferred types, and type guards are in [types/].
+
+Roughly order from lowest-level to highest:
+
+- `common.ts`: stateful field data, and couple other misc types
+- `field.ts`: fields - types, values, instances, templates
+- `invocation.ts`: invocations and other node types
+- `workflow.ts`: workflows and constituents
+
+We customize the OpenAPI schema to include additional properties on invocation and field schemas. To facilitate parsing this schema into templates, we modify/wrap the types from [openapi-types] in `openapi.ts`.
+
+### OpenAPI Schema Parsing
+
+The entrypoint for OpenAPI schema parsing is [parseSchema.ts].
+
+General logic flow:
+
+- Iterate over all invocation schema objects
+ - Extract relevant invocation-level attributes (e.g. title, type, version, etc)
+ - Iterate over the invocation's input fields
+ - [Parse each field's type](#parsing-field-types)
+ - [Build a field input template](#building-field-input-templates) from the type - either a stateful template or "generic" stateless template
+ - Iterate over the invocation's output fields
+ - Parse the field's type (same as inputs)
+ - [Build a field output template](#building-field-output-templates)
+ - Assemble the attributes and fields into an invocation template
+
+Most of these involve very straightforward `reduce`s, but the less intuitive steps are detailed below.
+
+#### Parsing Field Types
+
+Field types are represented as structured objects:
+
+```ts
+type FieldType = {
+ name: string;
+ cardinality: 'SINGLE' | 'COLLECTION' | 'SINGLE_OR_COLLECTION';
+};
+```
+
+The parsing logic is in `parseFieldType.ts`.
+
+There are 4 general cases for field type parsing.
+
+##### Primitive Types
+
+When a field is annotated as a primitive values (e.g. `int`, `str`, `float`), the field type parsing is fairly straightforward. The field is represented by a simple OpenAPI **schema object**, which has a `type` property.
+
+We create a field type name from this `type` string (e.g. `string` -> `StringField`). The cardinality is `"SINGLE"`.
+
+##### Complex Types
+
+When a field is annotated as a pydantic model (e.g. `ImageField`, `MainModelField`, `ControlField`), it is represented as a **reference object**. Reference objects are pointers to another schema or reference object within the schema.
+
+We need to **dereference** the schema to pull these out. Dereferencing may require recursion. We use the reference object's name directly for the field type name.
+
+> Unfortunately, at this time, we've had limited success using external libraries to deference at runtime, so we do this ourselves.
+
+##### Collection Types
+
+When a field is annotated as a list of a single type, the schema object has an `items` property. They may be a schema object or reference object and must be parsed to determine the item type.
+
+We use the item type for field type name. The cardinality is `"COLLECTION"`.
+
+##### Single or Collection Types
+
+When a field is annotated as a union of a type and list of that type, the schema object has an `anyOf` property, which holds a list of valid types for the union.
+
+After verifying that the union has two members (a type and list of the same type), we use the type for field type name, with cardinality `"SINGLE_OR_COLLECTION"`.
+
+##### Optional Fields
+
+In OpenAPI v3.1, when an object is optional, it is put into an `anyOf` along with a primitive schema object with `type: 'null'`.
+
+Handling this adds a fair bit of complexity, as we now must filter out the `'null'` types and work with the remaining types as described above.
+
+If there is a single remaining schema object, we must recursively call to `parseFieldType()` to get parse it.
+
+#### Building Field Input Templates
+
+Now that we have a field type, we can build an input template for the field.
+
+Stateful fields all get a function to build their template, while stateless fields are constructed directly. This is possible because stateless fields have no default value or constraints.
+
+See [buildFieldInputTemplate.ts].
+
+#### Building Field Output Templates
+
+Field outputs are similar to stateless fields - they do not have any value in the frontend. When building their templates, we don't need a special function for each field type.
+
+See [buildFieldOutputTemplate.ts].
+
+### Managing reactflow State
+
+As described above, the workflow editor state is the essentially the reactflow state, plus some extra metadata.
+
+We provide reactflow with an array of nodes and edges via redux, and a number of [event handlers][reactflow-events]. These handlers dispatch redux actions, managing nodes and edges.
+
+The pieces of redux state relevant to workflows are:
+
+- `state.nodes.nodes`: the reactflow nodes state
+- `state.nodes.edges`: the reactflow edges state
+- `state.nodes.workflow`: the workflow metadata
+
+#### Building Nodes and Edges
+
+A reactflow node has a few important top-level properties:
+
+- `id`: unique identifier
+- `type`: a string that maps to a react component to render the node
+- `position`: XY coordinates
+- `data`: arbitrary data
+
+When the user adds a node, we build **invocation node data**, storing it in `data`. Invocation properties (e.g. type, version, label, etc.) are copied from the invocation template. Inputs and outputs are built from the invocation template's field templates.
+
+See [buildInvocationNode.ts].
+
+Edges are managed by reactflow, but briefly, they consist of:
+
+- `source`: id of the source node
+- `sourceHandle`: id of the source node handle (output field)
+- `target`: id of the target node
+- `targetHandle`: id of the target node handle (input field)
+
+> Edge creation is gated behind validation logic. This validation compares the input and output field types and overall graph state.
+
+#### Building a Workflow
+
+Building a workflow entity is as simple as dropping the nodes, edges and metadata into an object.
+
+Each node and edge is parsed with a zod schema, which serves to strip out any unneeded data.
+
+See [buildWorkflow.ts].
+
+#### Loading a Workflow
+
+Workflows may be loaded from external sources or the user's local instance. In all cases, the workflow needs to be handled with care, as an untrusted object.
+
+Loading has a few stages which may throw or warn if there are problems:
+
+- Parsing the workflow data structure itself, [migrating](#workflow-migrations) it if necessary (throws)
+- Check for a template for each node (warns)
+- Check each node's version against its template (warns)
+- Validate the source and target of each edge (warns)
+
+This validation occurs in [validateWorkflow.ts].
+
+If there are no fatal errors, the workflow is then stored in redux state.
+
+### Workflow Migrations
+
+When the workflow schema changes, we may need to perform some data migrations. This occurs as workflows are loaded. zod schemas for each workflow schema version is retained to facilitate migrations.
+
+Previous schemas are in folders in `invokeai/frontend/web/src/features/nodes/types/`, eg `v1/`.
+
+Migration logic is in [migrations.ts].
+
+[pydantic]: https://github.com/pydantic/pydantic 'pydantic'
+[zod]: https://github.com/colinhacks/zod 'zod'
+[openapi-types]: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-types 'openapi-types'
+[reactflow]: https://github.com/xyflow/xyflow 'reactflow'
+[reactflow-concepts]: https://reactflow.dev/learn/concepts/terms-and-definitions
+[reactflow-events]: https://reactflow.dev/api-reference/react-flow#event-handlers
+[buildWorkflow.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts
+[nodesSlice.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+[buildLinearTextToImageGraph.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearTextToImageGraph.ts
+[buildNodesGraph.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/graph/buildNodesGraph.ts
+[buildInvocationNode.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/node/buildInvocationNode.ts
+[validateWorkflow.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/workflow/validateWorkflow.ts
+[migrations.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts
+[parseSchema.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/schema/parseSchema.ts
+[buildFieldInputTemplate.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldInputTemplate.ts
+[buildFieldOutputTemplate.ts]: https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/src/features/nodes/util/schema/buildFieldOutputTemplate.ts
diff --git a/docs/src/content/docs/development/Guides/api-development.mdx b/docs/src/content/docs/development/Guides/api-development.mdx
new file mode 100644
index 00000000000..3911dad0081
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/api-development.mdx
@@ -0,0 +1,50 @@
+---
+title: API Development
+---
+
+import InvocationContextDocs from '@lib/components/InvocationContextDocs.astro'
+
+Each invocation's `invoke` method is provided a single arg - the Invocation Context.
+
+This object provides an API the invocation can use to interact with application services, for example:
+
+- Saving images
+- Logging messages
+- Loading models
+
+```py
+class MyInvocation(BaseInvocation):
+ ...
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Load an image
+ image_pil = context.images.get_pil(self.image.image_name)
+ # Do something to the image
+ output_image = do_something_cool(image_pil)
+ # Save the image
+ image_dto = context.images.save(output_image)
+ # Log a message
+ context.logger.info(f"Did something cool, image saved!")
+ # Return the output
+ return ImageOutput.build(image_dto)
+ ...
+```
+
+The full generated API reference is documented below.
+
+## Mixins
+
+Two important mixins are provided to facilitate working with metadata and gallery boards.
+
+### `WithMetadata`
+
+Inherit from this class (in addition to `BaseInvocation`) to add a `metadata` input to your node. When you do this, you can access the metadata dict from `self.metadata` in the `invoke()` function.
+
+The dict will be populated via the node's input, and you can add any metadata you'd like to it. When you call `context.images.save()`, if the metadata dict has any data, it be automatically embedded in the image.
+
+### `WithBoard`
+
+Inherit from this class (in addition to `BaseInvocation`) to add a `board` input to your node. This renders as a drop-down to select a board. The user's selection will be accessible from `self.board` in the `invoke()` function.
+
+When you call `context.images.save()`, if a board was selected, the image will added to that board as it is saved.
+
+
diff --git a/docs/src/content/docs/development/Guides/assets/html-detail.png b/docs/src/content/docs/development/Guides/assets/html-detail.png
new file mode 100644
index 00000000000..055218002f7
Binary files /dev/null and b/docs/src/content/docs/development/Guides/assets/html-detail.png differ
diff --git a/docs/src/content/docs/development/Guides/assets/html-overview.png b/docs/src/content/docs/development/Guides/assets/html-overview.png
new file mode 100644
index 00000000000..1f288fde118
Binary files /dev/null and b/docs/src/content/docs/development/Guides/assets/html-overview.png differ
diff --git a/docs/src/content/docs/development/Guides/creating-node-pack.mdx b/docs/src/content/docs/development/Guides/creating-node-pack.mdx
new file mode 100644
index 00000000000..01a51afbcb5
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/creating-node-pack.mdx
@@ -0,0 +1,157 @@
+---
+title: Creating Node Packs
+lastUpdated: 2026-05-23
+---
+
+import { FileTree } from '@astrojs/starlight/components'
+
+This guide explains how to structure your Git repository so it can be installed via InvokeAI's Custom Node Manager.
+
+## Repository Structure
+
+Your repository **is** the node pack. When a user installs it, the entire repo is cloned into the `nodes` directory.
+
+### Minimum Required Structure
+
+
+ - my-node-pack/
+ - `__init__.py` Required: Imports all node classes
+ - my_node.py Your node implementation(s)
+ - README.md Recommended: Describe how your nodes work
+
+
+The `__init__.py` at the root is **mandatory**. Without it, the pack will not be loaded.
+
+### Recommended Structure
+
+
+ - my-node-pack/
+ - `__init__.py` Required: Imports all node classes
+ - requirements.txt Python dependencies (user-installed)
+ - README.md Description, usage & examples
+ - node_one.py Node implementation
+ - node_two.py Node implementation
+ - utils.py Shared utilities
+ - workflows/ Optional: Included workflow files
+ - example_workflow.json
+ - advanced_workflow.json
+
+
+## The `__init__.py` File
+
+This file must import all invocation classes you want to register. Only classes imported here will be available in InvokeAI.
+
+```python title="__init__.py"
+from .node_one import MyFirstInvocation
+from .node_two import MySecondInvocation
+```
+
+If you have nodes in subdirectories:
+
+```python
+from .nodes.image_tools import CropInvocation, ResizeInvocation
+from .nodes.text_tools import ConcatInvocation
+```
+
+## Dependencies (`requirements.txt` or `pyproject.toml`)
+
+If your nodes require additional Python packages, list them in a `requirements.txt` (or `pyproject.toml`) at the repository root:
+
+```txt title="requirements.txt"
+numpy>=1.24
+opencv-python>=4.8
+```
+
+The Custom Node Manager **does not** install these dependencies automatically — auto-installing into the running InvokeAI environment risks pulling in incompatible versions and breaking the application. After install, the UI shows the user a toast telling them that manual installation is required, and your README should document the exact install command (e.g. `pip install -r requirements.txt` from inside an activated InvokeAI environment).
+
+**Important:** Avoid pinning versions too tightly. InvokeAI has its own dependencies, and version conflicts can cause issues. Use minimum version constraints (`>=`) where possible.
+
+## Including Workflows
+
+If your repository contains workflow `.json` files, they will be **automatically imported** into the user's workflow library during installation.
+
+### Workflow Detection
+
+The installer recursively scans your repository for `.json` files. A file is recognized as a workflow if it contains both `nodes` and `edges` keys at the top level.
+
+### Tagging
+
+Imported workflows are automatically tagged with `node-pack:` so users can filter for them in the workflow library. When the node pack is uninstalled, these workflows are also removed.
+
+### Workflow Format
+
+Workflows should follow the standard InvokeAI workflow format:
+
+```json title="example_workflow.json"
+{
+ "name": "My Example Workflow",
+ "author": "Your Name",
+ "description": "Demonstrates how to use MyFirstInvocation",
+ "version": "1.0.0",
+ "contact": "",
+ "tags": "example, my-node-pack",
+ "notes": "",
+ "meta": {
+ "version": "3.0.0",
+ "category": "user"
+ },
+ "exposedFields": [],
+ "nodes": [...],
+ "edges": [...]
+}
+```
+
+**Tip:** The easiest way to create a workflow file is to build the workflow in InvokeAI's workflow editor, then export it via **Save As** and copy the `.json` file into your repository.
+
+## Node Implementation
+
+Each node is a Python class decorated with `@invocation()`. Here's a minimal example:
+
+```python title="example_node.py"
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import InputField, OutputField
+from invokeai.invocation_api import BaseInvocationOutput, invocation_output
+
+@invocation_output("my_output")
+class MyOutput(BaseInvocationOutput):
+ result: str = OutputField(description="The result")
+
+@invocation(
+ "my_node",
+ title="My Node",
+ tags=["example", "custom"],
+ category="custom",
+ version="1.0.0",
+)
+class MyInvocation(BaseInvocation):
+ """Does something useful."""
+
+ input_text: str = InputField(default="", description="Input text")
+
+ def invoke(self, context) -> MyOutput:
+ return MyOutput(result=f"Processed: {self.input_text}")
+```
+
+For full details on the invocation API, see the [Invocation API documentation](invocation-api.md).
+
+## Best Practices
+
+- **Use a descriptive repository name** — it becomes the pack name shown in the UI
+- **Include a README.md** with description, screenshots, and usage instructions
+- **Version your nodes** using semver in the `@invocation()` decorator
+- **Don't include large binary files** in your repository (models, weights, etc.)
+- **Test your nodes** by placing the repo in the `nodes` directory before publishing
+- **Include example workflows** so users can get started quickly
+- **Tag your GitHub repository** with `invokeai-node` for discoverability
+- **Avoid name collisions** — choose unique invocation type strings (e.g. `my_pack_resize` instead of just `resize`)
+
+## Testing Your Pack
+
+Before publishing, verify your pack works with the Custom Node Manager:
+
+1. Create a Git repository with your node pack
+2. Push it to GitHub (or any Git host)
+3. In InvokeAI, go to the Nodes tab and install it via the Git URL
+4. Verify your nodes appear in the workflow editor
+5. Verify any included workflows are imported
+6. Test uninstalling — nodes and workflows should be removed
diff --git a/docs/src/content/docs/development/Guides/creating-nodes.mdx b/docs/src/content/docs/development/Guides/creating-nodes.mdx
new file mode 100644
index 00000000000..f2dbee639bc
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/creating-nodes.mdx
@@ -0,0 +1,42 @@
+---
+title: Creating Nodes
+---
+
+import { Steps, LinkCard } from '@astrojs/starlight/components';
+
+
+ 1. Learn about the specifics of creating a new node in our Node Creation Documentation.
+
+
+
+ 2. 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.
+
+ 3. 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](../../../workflows/community-nodes) 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.
+
+ 4. 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.
+
+
+### Community Node Template
+
+Append the following template to your pull request and the [Community Nodes](../../../workflows/community-nodes) page when submitting a node to be added to the community nodes list:
+
+```md
+---
+
+### Super Cool Node Template
+
+**Description:** This node allows you to do super cool things with InvokeAI.
+
+**Node Link:** https://github.com/invoke-ai/InvokeAI/fake_node.py
+
+**Example Node Graph:** https://github.com/invoke-ai/InvokeAI/fake_node_graph.json
+
+**Output Examples**
+
+
+```
diff --git a/docs/src/content/docs/development/Guides/models.mdx b/docs/src/content/docs/development/Guides/models.mdx
new file mode 100644
index 00000000000..8657cc97818
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/models.mdx
@@ -0,0 +1,556 @@
+---
+title: Integrating a New Model Architecture
+description: A comprehensive guide to integrating new foundational model architectures into InvokeAI.
+lastUpdated: 2026-02-19
+---
+
+import { Steps, FileTree } from '@astrojs/starlight/components';
+
+This guide walks you through the end-to-end process of integrating a **new foundational model architecture** into InvokeAI. This is required when adding a completely new family of models (e.g., Stable Diffusion 3, FLUX, Hunyuan, etc.), rather than just adding a new checkpoint for an existing architecture.
+
+:::note
+The code examples in this guide use a hypothetical `NewModel` architecture. The implementations of `FLUX`, `SD3`, and `SDXL` in the InvokeAI codebase serve as excellent real-world references.
+:::
+
+## Architectural Overview
+
+Integrating a new model touches several parts of the InvokeAI stack, from the lowest-level PyTorch inference code up to the React frontend:
+
+1. **Taxonomy & Configuration (Backend)**: Declaring the model's existence and defining how to detect it from its weights on disk.
+2. **Model Loading (Backend)**: Defining how to load the detected files into PyTorch models in memory.
+3. **Sampling & Denoising (Backend)**: Implementing the core math for noise generation, scheduling, and the denoising loop.
+4. **Invocations (Backend)**: Wrapping the PyTorch logic into isolated "nodes" that can be executed by InvokeAI's graph engine.
+5. **Graph Building (Frontend)**: Instructing the UI on how to wire these nodes together based on user settings.
+6. **State & UI (Frontend)**: Adding the necessary UI controls and state management for the new model's unique parameters.
+
+---
+
+## 1. Taxonomy & Defaults
+
+The first step is to declare your model in the system's taxonomy and provide reasonable default settings.
+
+
+1. **Add `BaseModelType`**
+
+ Update the base model taxonomy to include your new model.
+
+ ```python title="invokeai/backend/model_manager/taxonomy.py" ins={7}
+ class BaseModelType(str, Enum):
+ # Existing types
+ StableDiffusion1 = "sd-1"
+ StableDiffusion2 = "sd-2"
+ StableDiffusionXL = "sdxl"
+ Flux = "flux"
+ NewModel = "newmodel"
+ ```
+
+2. **Add Variant Type (if needed)**
+
+ If your model comes in different structural variants (e.g., different parameter counts or distilled versions like `schnell` vs `dev`), define a variant enum.
+
+ ```python title="invokeai/backend/model_manager/taxonomy.py"
+ class NewModelVariantType(str, Enum):
+ VariantA = "variant_a"
+ VariantB = "variant_b"
+ ```
+
+3. **Define Default Settings**
+
+ Provide default generation parameters (steps, CFG scale, etc.) for the UI to use when this model is selected.
+
+ ```python title="invokeai/backend/model_manager/configs/main.py" ins={5-6}
+ class MainModelDefaultSettings:
+ @staticmethod
+ def from_base(base: BaseModelType, variant: AnyVariant | None = None):
+ match base:
+ case BaseModelType.NewModel:
+ return MainModelDefaultSettings(steps=20, cfg_scale=7.0)
+ ```
+
+
+:::tip[Checklist: Taxonomy]{icon="approve-check"}
+ - [ ] Extend `BaseModelType` enum in `taxonomy.py`
+ - [ ] Create variant enum if needed in `taxonomy.py`
+ - [ ] Update `AnyVariant` union in `taxonomy.py`
+ - [ ] Add default settings in `from_base()` in `configs/main.py`
+:::
+
+---
+
+## 2. Model Configs & Detection
+
+InvokeAI needs to know how to identify your model from a `.safetensors` file or a diffusers folder.
+
+
+1. **Create Main Model Config**
+
+ Define the configuration schemas for your model format(s).
+
+ ```python title="invokeai/backend/model_manager/configs/main.py"
+ # Checkpoint Format (Single File)
+ @ModelConfigFactory.register
+ class Main_Checkpoint_NewModel_Config(Checkpoint_Config_Base):
+ type: Literal[ModelType.Main] = ModelType.Main
+ base: Literal[BaseModelType.NewModel] = BaseModelType.NewModel
+ format: Literal[ModelFormat.Checkpoint] = ModelFormat.Checkpoint
+ variant: NewModelVariantType = NewModelVariantType.VariantA
+
+ @classmethod
+ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict) -> Self:
+ if not cls._validate_is_newmodel(mod):
+ raise NotAMatchError("Not a NewModel")
+ variant = cls._get_variant_or_raise(mod)
+ return cls(..., variant=variant)
+
+ # Diffusers Format (Folder)
+ @ModelConfigFactory.register
+ class Main_Diffusers_NewModel_Config(Diffusers_Config_Base):
+ type: Literal[ModelType.Main] = ModelType.Main
+ base: Literal[BaseModelType.NewModel] = BaseModelType.NewModel
+ format: Literal[ModelFormat.Diffusers] = ModelFormat.Diffusers
+ ```
+
+2. **Implement Detection Logic**
+
+ Write helper functions to inspect the state dictionary keys and shape to uniquely identify your architecture.
+
+ ```python title="invokeai/backend/model_manager/configs/main.py"
+ def _is_newmodel(state_dict: dict) -> bool:
+ """Detect if state dict belongs to NewModel architecture."""
+ # Example: check for a highly specific layer name or shape
+ required_keys = ["transformer_blocks.0.attn.to_q.weight"]
+ return all(key in state_dict for key in required_keys)
+
+ def _get_newmodel_variant(state_dict: dict) -> NewModelVariantType:
+ """Determine variant from state dict."""
+ # Example: distinguish variants based on hidden dimension size
+ context_dim = state_dict["context_embedder.weight"].shape[1]
+ if context_dim == 7680:
+ return NewModelVariantType.VariantA
+ return NewModelVariantType.VariantB
+ ```
+
+3. **Submodels (VAE & Text Encoder)**
+
+ If your model uses a novel VAE or Text Encoder not already in InvokeAI, you must repeat this process to create configs for them (e.g., in `configs/vae.py` and `configs/[encoder_type].py`).
+
+4. **Update the Configuration Union**
+
+ Register your new configs so the application knows to check them when scanning directories.
+
+ ```python title="invokeai/backend/model_manager/configs/factory.py" ins={4-5}
+ AnyModelConfig = Annotated[
+ # ... existing configs
+ Main_Checkpoint_NewModel_Config |
+ Main_Diffusers_NewModel_Config,
+ Discriminator(...)
+ ]
+ ```
+
+
+:::tip[Checklist: Configs]{icon="approve-check"}
+ - [ ] Create main checkpoint config (`configs/main.py`)
+ - [ ] Create main diffusers config (`configs/main.py`)
+ - [ ] Create detection helper functions (`_is_newmodel()`, `_get_variant()`)
+ - [ ] Create VAE and Text Encoder configs if they use novel architectures
+ - [ ] Update `AnyModelConfig` union (`configs/factory.py`)
+:::
+
+---
+
+## 3. Model Loaders
+
+Loaders are responsible for converting the files on disk (described by the config) into PyTorch models in memory.
+
+
+1. **Create the Model Loader**
+
+ ```python title="invokeai/backend/model_manager/load/model_loaders/[newmodel].py"
+ @ModelLoaderRegistry.register(
+ base=BaseModelType.NewModel,
+ type=ModelType.Main,
+ format=ModelFormat.Checkpoint
+ )
+ class NewModelLoader(ModelLoader):
+ def _load_model(self, config: AnyModelConfig, submodel_type: SubModelType | None) -> AnyModel:
+ # 1. Load the raw weights from disk
+ state_dict = self._load_state_dict(config.path)
+
+ # 2. Convert state dict keys if necessary (e.g. from original repo format to Diffusers)
+ if self._is_original_format(state_dict):
+ state_dict = self._convert_to_diffusers_format(state_dict)
+
+ # 3. Instantiate the empty PyTorch model
+ model = NewModelTransformer(config=model_config)
+
+ # 4. Load weights into the model
+ model.load_state_dict(state_dict)
+ return model
+ ```
+
+2. **Custom VAE/Encoder Loaders (If Applicable)**
+
+ If you created custom configs for the VAE or Text Encoder, you must also create loaders for them, registering them with the appropriate `ModelType`.
+
+
+:::tip[Checklist: Loaders]{icon="approve-check"}
+- [ ] Create and register the main model loader
+- [ ] Create VAE/Encoder loaders if necessary
+- [ ] Implement state dict conversion if supporting non-diffusers formats
+:::
+
+---
+
+## 4. Sampling and Denoising Core
+
+This is where the actual mathematical implementation of the model lives.
+
+
+
+1. **Sampling Utilities**
+
+ Create utility functions specific to how your model handles noise, packing, and scheduling.
+
+ ```python title="invokeai/backend/[newmodel]/sampling_utils.py"
+ def get_noise_newmodel(num_samples: int, height: int, width: int, seed: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
+ # Models often have different latent channel counts (e.g., SD1.5 has 4, FLUX has 16)
+ latent_channels = 32
+ latent_h, latent_w = height // 8, width // 8
+ generator = torch.Generator(device=device).manual_seed(seed)
+ return torch.randn((num_samples, latent_channels, latent_h, latent_w), generator=generator, device=device, dtype=dtype)
+
+ def pack_newmodel(x: torch.Tensor) -> torch.Tensor:
+ # Some transformer-based models require packing latents into a sequence
+ return rearrange(x, "b c (h ph) (w pw) -> b (h w) (c ph pw)", ph=2, pw=2)
+ ```
+
+ If the architecture supports external noise, prefer extending the standard
+ `invokeai/app/invocations/noise.py` node's `noise_type` selector instead of
+ adding a brand new noise node. Only add a dedicated noise invocation when the
+ architecture's noise tensor rank or layout cannot be expressed by the
+ standard node.
+
+2. **The Denoising Loop**
+
+ Implement the core sampling loop. This interacts with schedulers and handles classifier-free guidance (CFG).
+
+ ```python title="invokeai/backend/[newmodel]/denoise.py"
+ def denoise(model: nn.Module, img: torch.Tensor, txt: torch.Tensor, timesteps: list[float], cfg_scale: list[float], scheduler: Any = None) -> torch.Tensor:
+ """Main denoising loop."""
+ total_steps = len(timesteps) - 1
+
+ for step_index in range(total_steps):
+ t_curr = timesteps[step_index]
+
+ # Handle CFG (Classifier-Free Guidance)
+ if cfg_scale[step_index] > 1.0:
+ # Batch positive and negative prompts if applicable
+ pred_pos = model(img, t_curr, txt)
+ # ...
+ else:
+ pred = model(img, t_curr, txt)
+
+ # Step the scheduler
+ img = scheduler.step(pred, t_curr, img).prev_sample
+
+ return img
+ ```
+
+3. **Schedulers**
+
+ If your model requires a novel scheduler, add it to the scheduler mapping (e.g., `invokeai/backend/[newmodel]/schedulers.py`).
+
+
+:::tip[Checklist: Core Inference]{icon="approve-check"}
+ - [ ] Noise generation (`get_noise_newmodel()`)
+ - [ ] Pack/unpack functions (if transformer-based)
+ - [ ] Timestep schedule generation
+ - [ ] Denoise loop implementation
+ - [ ] Map supported schedulers
+:::
+
+---
+
+## 5. Invocations
+
+Invocations expose your PyTorch functions as isolated execution nodes in InvokeAI's graph.
+
+
+1. **Model Loader Invocation**
+
+ Loads the components (Transformer, VAE, etc.) and provides them to downstream nodes.
+
+ ```python title="invokeai/app/invocations/[newmodel]_model_loader.py"
+ @invocation("newmodel_model_loader", title="NewModel Loader", category="model_loader")
+ class NewModelModelLoaderInvocation(BaseInvocation):
+ model: ModelIdentifierField = InputField(description="Main model")
+
+ def invoke(self, context: InvocationContext) -> NewModelLoaderOutput:
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ return NewModelLoaderOutput(transformer=transformer, vae=vae)
+ ```
+
+2. **Text Encoder Invocation**
+
+ Tokenizes the prompt and runs the text encoder(s).
+
+ ```python title="invokeai/app/invocations/[newmodel]_text_encoder.py"
+ @invocation("newmodel_text_encode", title="NewModel Text Encoder", category="conditioning")
+ class NewModelTextEncoderInvocation(BaseInvocation):
+ prompt: str = InputField()
+ encoder: EncoderField = InputField()
+
+ def invoke(self, context: InvocationContext) -> ConditioningOutput:
+ # 1. Tokenize prompt
+ # 2. Run encoder to get embeddings
+ # 3. Save to context and return
+ conditioning_name = context.conditioning.save(ConditioningFieldData(...))
+ return ConditioningOutput(conditioning=ConditioningField(conditioning_name=conditioning_name))
+ ```
+
+3. **Denoise Invocation**
+
+ Wraps the `denoise` loop you wrote in the previous section.
+
+ ```python title="invokeai/app/invocations/[newmodel]_denoise.py"
+ @invocation("newmodel_denoise", title="NewModel Denoise", category="latents")
+ class NewModelDenoiseInvocation(BaseInvocation):
+ latents: LatentsField | None = InputField(default=None)
+ noise: LatentsField | None = InputField(default=None)
+ positive_conditioning: ConditioningField = InputField()
+ transformer: TransformerField = InputField()
+ steps: int = InputField(default=20)
+ cfg_scale: float = InputField(default=7.0)
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ # Generate noise, get schedule, and call your denoise() function
+ pass
+ ```
+
+ If you add external noise support, keep it optional so seed-driven workflows
+ continue to work. Validate connected noise against the architecture's
+ expected shape before using it.
+
+4. **VAE Encode / Decode Invocations**
+
+ Create nodes to transition between pixel space (images) and latent space.
+
+
+:::tip[Checklist: Invocations]{icon="approve-check"}
+ - [ ] Define output classes (e.g., `NewModelLoaderOutput`)
+ - [ ] Model loader invocation (`[newmodel]_model_loader.py`)
+ - [ ] Text encoder invocation (`[newmodel]_text_encoder.py`)
+ - [ ] Denoise invocation (`[newmodel]_denoise.py`)
+ - [ ] Extend the standard `noise` invocation if the architecture supports external noise
+ - [ ] VAE encode/decode invocations (`[newmodel]_vae_encode.py`, `[newmodel]_vae_decode.py`)
+:::
+
+---
+
+## 6. Frontend: Graph Building
+
+The UI doesn't know about Python functions; it only knows how to build graphs of Invocations.
+
+
+1. **Create the Graph Builder**
+
+ Write a TypeScript function that constructs the node graph for your model.
+
+ ```typescript title="invokeai/frontend/web/src/features/nodes/util/graph/generation/buildNewModelGraph.ts"
+ export const buildNewModelGraph = async (arg: GraphBuilderArg): Promise => {
+ const { state, manager } = arg;
+ const { model } = state.params;
+ const g = new Graph();
+
+ // 1. Add Loader
+ const modelLoader = g.addNode({
+ id: NEWMODEL_MODEL_LOADER,
+ type: 'newmodel_model_loader',
+ model: Graph.getModelMetadataField(model),
+ });
+
+ // 2. Add Text Encoders
+ const positivePrompt = g.addNode({
+ id: POSITIVE_CONDITIONING,
+ type: 'newmodel_text_encode',
+ prompt: state.params.positivePrompt,
+ });
+ g.addEdge(modelLoader, 'encoder', positivePrompt, 'encoder');
+
+ // 3. Add Denoise
+ const denoise = g.addNode({
+ id: NEWMODEL_DENOISE,
+ type: 'newmodel_denoise',
+ steps: state.params.steps,
+ cfg_scale: state.params.cfg,
+ });
+ g.addEdge(modelLoader, 'transformer', denoise, 'transformer');
+ g.addEdge(positivePrompt, 'conditioning', denoise, 'positive_conditioning');
+
+ // 4. Add VAE Decode
+ const l2i = g.addNode({
+ id: NEWMODEL_VAE_DECODE,
+ type: 'newmodel_vae_decode',
+ });
+ g.addEdge(modelLoader, 'vae', l2i, 'vae');
+ g.addEdge(denoise, 'latents', l2i, 'latents');
+
+ return { g, denoise, posCond: positivePrompt };
+ };
+ ```
+
+2. **Register the Graph Builder**
+
+ Hook your graph builder into the main routing logic.
+
+ ```typescript title="invokeai/frontend/web/src/features/queue/hooks/useEnqueueCanvas.ts" ins={5-6}
+ switch (base) {
+ case 'sdxl':
+ return buildSDXLGraph(arg);
+ case 'flux':
+ return buildFLUXGraph(arg);
+ case 'newmodel':
+ return buildNewModelGraph(arg);
+ }
+ ```
+
+3. **Update Type Definitions**
+
+ Add your new nodes to the strict frontend type unions.
+
+ ```typescript title="invokeai/frontend/web/src/features/nodes/util/graph/types.ts" ins="| 'newmodel_vae_decode'"
+ export type ImageOutputNodes =
+ | 'l2i' | 'flux_vae_decode' | 'sd3_l2i' | 'newmodel_vae_decode';
+ ```
+
+4. **Generation Modes**
+
+ Update `invokeai/app/invocations/metadata.py` to include your new modes in `GENERATION_MODES` (e.g., `"newmodel_txt2img"`, `"newmodel_img2img"`).
+
+
+:::tip[Checklist: Graph Building]{icon="approve-check"}
+ - [ ] Create graph builder (`buildNewModelGraph.ts`)
+ - [ ] Register graph builder in `useEnqueueCanvas.ts`
+ - [ ] Update node unions in `types.ts`
+ - [ ] Add generation modes to python `metadata.py`
+:::
+
+---
+
+## 7. Frontend: State & UI
+
+Finally, add any custom UI controls (like a specific scheduler dropdown) and manage their state.
+
+
+1. **Add to Redux State**
+
+ Update the parameters slice for your model-specific settings.
+
+ ```typescript title="invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts"
+ interface ParamsState {
+ // ...
+ newmodelScheduler: 'euler' | 'heun';
+ }
+
+ const initialState: ParamsState = {
+ // ...
+ newmodelScheduler: 'euler',
+ };
+
+ // Add reducers and export selectors...
+ ```
+
+2. **Parameter Recall**
+
+ Ensure users can extract parameters from previously generated images by updating `invokeai/frontend/web/src/features/metadata/parsing.tsx`.
+
+ ```typescript title="invokeai/frontend/web/src/features/metadata/parsing.tsx"
+ const recallNewmodelScheduler = (metadata: CoreMetadata) => {
+ if (metadata.scheduler) {
+ dispatch(setNewmodelScheduler(metadata.scheduler));
+ }
+ };
+ ```
+
+
+:::tip[Checklist: State & UI]{icon="approve-check"}
+ - [ ] Extend state interface for model-specific parameters
+ - [ ] Create reducers and selectors
+ - [ ] Add parameter recall handlers in `parsing.tsx`
+:::
+
+---
+
+## 8. Optional Features
+
+Depending on the model, you may want to support additional features.
+
+### ControlNet Support
+Requires backend configuration (`configs/controlnet.py`), a custom invocation (`[newmodel]_controlnet.py`), and frontend graph integration (`addControlNets`).
+
+### LoRA Support
+Requires defining a LoRA config (`configs/lora.py`), updating the model loader to pass LoRA fields, and wiring `addLoRAs` in the frontend graph builder.
+
+### IP-Adapter
+Requires a custom invocation for image prompting (`[newmodel]_ip_adapter.py`) and frontend integration via `addIPAdapters`.
+
+---
+
+## 9. Starter Models
+
+To allow users to easily download your model from the Model Manager UI, add it to the starter models list.
+
+```python title="invokeai/backend/model_manager/starter_models.py"
+newmodel_main = StarterModel(
+ name="NewModel Main",
+ base=BaseModelType.NewModel,
+ source="organization/newmodel-main", # HuggingFace repo
+ description="NewModel main transformer.",
+ type=ModelType.Main,
+)
+
+STARTER_MODELS.append(newmodel_main)
+```
+
+:::tip[Checklist: Starter Models]{icon="approve-check"}
+- [ ] Define main model StarterModel
+- [ ] Define VAE/Encoder StarterModels if separate
+- [ ] Set dependencies correctly if required
+- [ ] Add to `STARTER_MODELS` list
+:::
+
+---
+
+## Summary of Integration Files
+
+A complete minimal `txt2img` integration touches the following areas:
+
+
+- invokeai
+ - app/invocations
+ - metadata.py
+ - `[newmodel]_model_loader.py`
+ - `[newmodel]_text_encoder.py`
+ - `[newmodel]_denoise.py`
+ - `[newmodel]_vae_decode.py`
+ - backend
+ - model_manager
+ - taxonomy.py
+ - configs
+ - main.py
+ - factory.py
+ - load/model_loaders
+ - `[newmodel].py`
+ - starter_models.py
+ - `[newmodel]`
+ - sampling_utils.py
+ - denoise.py
+ - frontend/web/src/features
+ - nodes/util/graph
+ - generation/buildNewModelGraph.ts
+ - types.ts
+ - queue/hooks/useEnqueueCanvas.ts
+ - controlLayers/store/paramsSlice.ts
+ - metadata/parsing.tsx
+
diff --git a/docs/src/content/docs/development/Guides/recall-api.mdx b/docs/src/content/docs/development/Guides/recall-api.mdx
new file mode 100644
index 00000000000..f366da79b33
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/recall-api.mdx
@@ -0,0 +1,523 @@
+---
+title: Recall Parameters API
+---
+
+## Overview
+
+The Recall Parameters API is a REST endpoint on the InvokeAI backend that
+lets external processes set recallable generation parameters on the
+frontend. Supported parameters include:
+
+- Core text and numeric parameters (prompts, model, steps, CFG, dimensions, seed, ...)
+- LoRAs
+- Control Layers (ControlNet, T2I Adapter, Control LoRA) with optional control images
+- IP Adapters and FLUX Redux reference images with optional images
+- Model-free reference images (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit)
+
+When parameters are updated via the API, the backend stores them in client
+state persistence for the target queue and broadcasts a `recall_parameters_updated`
+WebSocket event. Any frontend client subscribed to that queue applies the
+new values immediately — no manual reload required.
+
+Typical use cases:
+
+- An external image browser that wants to "recall" or "remix" the
+ generation parameters saved into a PNG's metadata.
+- A script that pre-populates parameters before the user runs generation.
+- Automated testing or batch workflows that want to reuse existing model
+ and adapter configurations.
+
+## How It Works
+
+1. **API request** — your client POSTs a JSON body of parameters to
+ `/api/v1/recall/{queue_id}`.
+2. **Storage** — non-null parameters are stored under
+ `recall_*` keys in the client state persistence service, scoped to the
+ given `queue_id`.
+3. **Resolution** — models are resolved from human-readable names to the
+ internal model keys used by the frontend, and image filenames are
+ validated against `{INVOKEAI_ROOT}/outputs/images`.
+4. **Broadcast** — a `recall_parameters_updated` event is emitted on the
+ websocket room for `queue_id`.
+5. **Frontend update** — any connected client subscribed to that queue
+ applies the update to its Redux store, so UI fields, LoRAs, control
+ layers, IP adapters, and reference images all populate immediately.
+
+## Endpoint
+
+**Base URL:** `http://localhost:9090/api/v1/recall/{queue_id}`
+
+The queue id is usually `default`.
+
+### POST — Update Recall Parameters
+
+Updates recallable parameters for the given `queue_id`.
+
+```http
+POST /api/v1/recall/{queue_id}
+Content-Type: application/json
+
+{
+ "positive_prompt": "a beautiful landscape",
+ "negative_prompt": "blurry, low quality",
+ "model": "sd-1.5",
+ "steps": 20,
+ "cfg_scale": 7.5,
+ "width": 512,
+ "height": 512,
+ "seed": 12345
+}
+```
+
+All parameters are optional — only send the fields you want to update.
+
+### GET — Retrieve Recall Parameters
+
+```http
+GET /api/v1/recall/{queue_id}
+```
+
+```json
+{
+ "status": "success",
+ "queue_id": "queue_123",
+ "note": "Use the frontend to access stored recall parameters, or set specific parameters using POST"
+}
+```
+
+## Request Schema
+
+### Core parameters
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `positive_prompt` | string | Positive prompt text |
+| `negative_prompt` | string | Negative prompt text |
+| `model` | string | Main model name/identifier |
+| `refiner_model` | string | Refiner model name/identifier |
+| `vae_model` | string | VAE model name/identifier |
+| `scheduler` | string | Scheduler name |
+| `steps` | integer | Number of generation steps (≥1) |
+| `refiner_steps` | integer | Number of refiner steps (≥0) |
+| `cfg_scale` | number | CFG scale for guidance |
+| `cfg_rescale_multiplier` | number | CFG rescale multiplier |
+| `refiner_cfg_scale` | number | Refiner CFG scale |
+| `guidance` | number | Guidance scale |
+| `width` | integer | Image width in pixels (≥64) |
+| `height` | integer | Image height in pixels (≥64) |
+| `seed` | integer | Random seed (≥0) |
+| `denoise_strength` | number | Denoising strength (0–1) |
+| `refiner_denoise_start` | number | Refiner denoising start (0–1) |
+| `clip_skip` | integer | CLIP skip layers (≥0) |
+| `seamless_x` | boolean | Enable seamless X tiling |
+| `seamless_y` | boolean | Enable seamless Y tiling |
+| `refiner_positive_aesthetic_score` | number | Refiner positive aesthetic score |
+| `refiner_negative_aesthetic_score` | number | Refiner negative aesthetic score |
+
+### Collection parameters
+
+```typescript
+{
+ // LoRAs
+ loras?: Array<{
+ model_name: string; // LoRA model name
+ weight?: number; // Default: 0.75, Range: -10 to 10
+ is_enabled?: boolean; // Default: true
+ }>;
+
+ // Control Layers (ControlNet, T2I Adapter, Control LoRA)
+ control_layers?: Array<{
+ model_name: string; // Control adapter model name
+ image_name?: string; // Optional image filename from outputs/images
+ weight?: number; // Default: 1.0, Range: -1 to 2
+ begin_step_percent?: number; // Default: 0.0, Range: 0 to 1
+ end_step_percent?: number; // Default: 1.0, Range: 0 to 1
+ control_mode?: "balanced" | "more_prompt" | "more_control"; // ControlNet only
+ }>;
+
+ // IP Adapters (includes FLUX Redux)
+ ip_adapters?: Array<{
+ model_name: string; // IP Adapter / FLUX Redux model name
+ image_name?: string; // Optional reference image filename from outputs/images
+ weight?: number; // Default: 1.0, Range: -1 to 2
+ begin_step_percent?: number; // Default: 0.0, Range: 0 to 1
+ end_step_percent?: number; // Default: 1.0, Range: 0 to 1
+ method?: "full" | "style" | "composition"; // Default: "full"
+ image_influence?: "lowest" | "low" | "medium" | "high" | "highest"; // FLUX Redux only
+ }>;
+
+ // Model-free reference images (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit)
+ reference_images?: Array<{
+ image_name: string; // Reference image filename from outputs/images
+ }>;
+}
+```
+
+## Model Name Resolution
+
+The backend resolves model names to their internal keys:
+
+1. **Main models** — resolved from the name to the model key.
+2. **LoRAs** — searched in the LoRA model database.
+3. **Control adapters** — tried in order: ControlNet → T2I Adapter → Control LoRA.
+4. **IP Adapters** — searched in the IP Adapter database; falls back to FLUX Redux.
+
+Models that cannot be resolved are skipped with a warning in the logs —
+the rest of the parameters are still applied.
+
+## Image File Handling
+
+When an `image_name` is supplied, the backend:
+
+1. Resolves `{INVOKEAI_ROOT}/outputs/images/{image_name}` via the image
+ files service (which also validates the path).
+2. Opens the image to extract width/height.
+3. Includes the image metadata in the event sent to the frontend.
+4. Logs whether the image was found.
+
+Images must be referenced by their filename as it appears in the
+outputs/images directory:
+
+- ✅ `"image_name": "example.png"`
+- ✅ `"image_name": "my_control_image_20240110.jpg"`
+- ❌ `"image_name": "outputs/images/example.png"` (no prefix)
+- ❌ `"image_name": "/full/path/to/example.png"` (no absolute paths)
+
+Missing images are logged as warnings but **do not** fail the request —
+remaining parameters are still applied.
+
+## Feature Details
+
+### LoRAs
+
+- Existing LoRAs are cleared before new ones are added.
+- Each LoRA's model config is fetched and applied with the specified weight.
+- LoRAs appear in the LoRA selector panel.
+
+### Control Layers
+
+- Fully supported with optional images from `outputs/images`.
+- Configuration includes model, weights, step percentages, control mode,
+ and an image reference.
+- Image availability is logged in the frontend console.
+
+### IP Adapters / FLUX Redux
+
+- Reference images loaded from `outputs/images` are validated and passed
+ through.
+- Configuration includes model, weights, step percentages, method, and an
+ image reference.
+- FLUX Redux uses `image_influence` instead of a numeric weight.
+
+### Model-free reference images
+
+Used by architectures that consume a reference image directly, with no
+separate adapter model:
+
+- **FLUX.2 Klein** — built-in reference image support.
+- **FLUX Kontext** — reference image associated with the main model.
+- **Qwen Image Edit** — reference image associated with the main model.
+
+Because there is no adapter model to resolve, these entries carry only
+`image_name`. When the frontend receives them, it picks the appropriate
+config flavor (`flux2_reference_image`, `flux_kontext_reference_image`,
+or `qwen_image_reference_image`) based on the currently-selected main
+model, matching the behavior of a manual drag-and-drop.
+
+## Usage Examples
+
+### cURL
+
+```bash
+# Core parameters
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "positive_prompt": "a cyberpunk city at night",
+ "negative_prompt": "dark, unclear",
+ "model": "sd-1.5",
+ "steps": 30
+ }'
+
+# Just the seed
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{"seed": 99999}'
+```
+
+### LoRAs only
+
+```bash
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "loras": [
+ {"model_name": "add-detail-xl", "weight": 0.8, "is_enabled": true},
+ {"model_name": "sd_xl_offset_example-lora_1.0", "weight": 0.5}
+ ]
+ }'
+```
+
+### Control layers with an image
+
+```bash
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "control_layers": [
+ {
+ "model_name": "controlnet-canny-sdxl-1.0",
+ "image_name": "my_control_image.png",
+ "weight": 0.75,
+ "begin_step_percent": 0.0,
+ "end_step_percent": 0.8,
+ "control_mode": "balanced"
+ }
+ ]
+ }'
+```
+
+### IP adapters with a reference image
+
+```bash
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "ip_adapters": [
+ {
+ "model_name": "ip-adapter-plus-face_sd15",
+ "image_name": "reference_face.png",
+ "weight": 0.7,
+ "method": "composition"
+ }
+ ]
+ }'
+```
+
+### Model-free reference images (FLUX.2 Klein / FLUX Kontext / Qwen Image Edit)
+
+```bash
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "FLUX.2 Klein",
+ "reference_images": [
+ {"image_name": "style_reference.png"}
+ ]
+ }'
+```
+
+### Complete configuration
+
+```bash
+curl -X POST http://localhost:9090/api/v1/recall/default \
+ -H "Content-Type: application/json" \
+ -d '{
+ "positive_prompt": "masterpiece, detailed photo with specific style",
+ "negative_prompt": "blurry, low quality",
+ "model": "FLUX Schnell",
+ "steps": 25,
+ "cfg_scale": 8.0,
+ "width": 1024,
+ "height": 768,
+ "seed": 42,
+ "loras": [
+ {"model_name": "add-detail-xl", "weight": 0.6}
+ ],
+ "control_layers": [
+ {
+ "model_name": "controlnet-depth-sdxl-1.0",
+ "image_name": "depth_map.png",
+ "weight": 1.0,
+ "end_step_percent": 0.7
+ }
+ ],
+ "ip_adapters": [
+ {
+ "model_name": "ip-adapter-plus-face_sd15",
+ "image_name": "style_reference.png",
+ "weight": 0.5,
+ "method": "style"
+ }
+ ]
+ }'
+```
+
+### Python
+
+```python
+import requests
+
+API_URL = "http://localhost:9090/api/v1/recall/default"
+
+params = {
+ "positive_prompt": "a serene forest",
+ "negative_prompt": "people, buildings",
+ "steps": 25,
+ "cfg_scale": 7.0,
+ "seed": 42,
+}
+
+response = requests.post(API_URL, json=params)
+result = response.json()
+print(f"Status: {result['status']}")
+print(f"Updated {result['updated_count']} parameters")
+```
+
+### JavaScript
+
+```javascript
+const API_URL = 'http://localhost:9090/api/v1/recall/default';
+
+fetch(API_URL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ positive_prompt: 'a beautiful sunset',
+ steps: 20,
+ width: 768,
+ height: 768,
+ seed: 12345,
+ }),
+})
+ .then((res) => res.json())
+ .then((data) => console.log(data));
+```
+
+## Response Format
+
+```json
+{
+ "status": "success",
+ "queue_id": "default",
+ "updated_count": 15,
+ "parameters": {
+ "positive_prompt": "...",
+ "steps": 25,
+ "loras": [
+ {"model_key": "abc123...", "weight": 0.6, "is_enabled": true}
+ ],
+ "control_layers": [
+ {
+ "model_key": "controlnet-xyz...",
+ "weight": 1.0,
+ "image": {"image_name": "depth_map.png", "width": 1024, "height": 768}
+ }
+ ],
+ "ip_adapters": [
+ {
+ "model_key": "ip-adapter-xyz...",
+ "weight": 0.5,
+ "image": {"image_name": "style_reference.png", "width": 1024, "height": 1024}
+ }
+ ],
+ "reference_images": [
+ {"image": {"image_name": "style_reference.png", "width": 1024, "height": 1024}}
+ ]
+ }
+}
+```
+
+## WebSocket Events
+
+Parameter updates emit a `recall_parameters_updated` event to the queue
+room. Connected frontend clients automatically:
+
+1. Apply standard parameters (prompts, steps, dimensions, etc.).
+2. Load and add LoRAs to the LoRA list.
+3. Apply control-layer configurations.
+4. Apply IP Adapter / FLUX Redux configurations with their images.
+5. Append model-free reference images, using the config flavor that
+ matches the currently-selected main model.
+
+## Error Handling
+
+- **400 Bad Request** — invalid parameters or parameter values.
+- **500 Internal Server Error** — server-side storage or retrieval failure.
+
+Errors include detailed messages. Missing images and unresolved model
+names are **not** errors — they are logged and the remaining parameters
+are still applied.
+
+## Logging
+
+### Backend
+
+```
+INFO: Resolved ControlNet model name 'controlnet-canny-sdxl-1.0' to key 'controlnet-xyz...'
+INFO: Found image file: depth_map.png (1024x768)
+INFO: Updated 12 recall parameters for queue default
+INFO: Resolved 1 LoRA(s)
+INFO: Resolved 1 control layer(s)
+INFO: Resolved 1 IP adapter(s)
+INFO: Resolved 1 reference image(s)
+```
+
+### Frontend
+
+Set `localStorage.ROARR_FILTER = 'debug'` in the browser to see all debug
+messages under the `events` namespace.
+
+```
+INFO: Applied 5 recall parameters to store
+INFO: Applied 1 IP adapter(s), replacing existing list
+INFO: Applied 1 model-free reference image(s)
+DEBUG: Built IP adapter ref image state: ip-adapter-xyz... (weight: 0.7)
+DEBUG: IP adapter image: outputs/images/depth_map.png (1024x768)
+```
+
+## Implementation Details
+
+- Parameters are stored in the client state persistence service under
+ `recall_*` keys, scoped to the `queue_id`.
+- Numeric validation runs at the FastAPI layer (e.g. `steps ≥ 1`, `width ≥ 64`).
+- Only non-null parameters are processed, stored, and broadcast.
+- Model-key resolution runs **after** the raw parameters are stored, so
+ an unresolvable model name simply drops out of the broadcast but does
+ not corrupt the persisted state.
+- The broadcast payload contains resolved model keys and image metadata
+ (width/height) so the frontend can populate its store without extra
+ round-trips.
+
+## Troubleshooting
+
+### Image not found
+
+If you see "Image file not found" in the logs:
+
+1. Verify the filename matches exactly (case-sensitive).
+2. Ensure the image is in `{INVOKEAI_ROOT}/outputs/images/`.
+3. Check that the filename does not include the `outputs/images/` prefix.
+
+### Model not found
+
+If you see "Could not find model":
+
+1. Verify the model name matches exactly (case-sensitive).
+2. Ensure the model is installed.
+3. Check the name via the Models Manager panel.
+
+### Event not received
+
+1. Check the browser console for socket connection errors.
+2. Verify the `queue_id` matches the frontend's queue (usually `default`).
+3. Check backend logs for event emission errors.
+
+## Limitations
+
+- **Model availability** — models referenced in the payload must be installed.
+- **Image availability** — images must exist in `outputs/images`; remote
+ URLs are not supported.
+- **Canvas auto-layer creation** — control layers and IP adapters with
+ images populate the recall state, but creating a canvas layer from
+ them still happens through the UI.
+
+## Future enhancements
+
+Potential improvements not yet implemented:
+
+1. Auto-create canvas layers from control-layer images in the payload.
+2. Auto-create reference-image layers from IP Adapter images in the payload.
+3. Support remote image URLs in addition to local `outputs/images` filenames.
+4. Image upload capability (accept base64 or file upload directly via the API).
+5. Batch operations that target multiple `queue_id`s in a single request.
diff --git a/docs/src/content/docs/development/Guides/tests.mdx b/docs/src/content/docs/development/Guides/tests.mdx
new file mode 100644
index 00000000000..c2dfd52b98c
--- /dev/null
+++ b/docs/src/content/docs/development/Guides/tests.mdx
@@ -0,0 +1,102 @@
+---
+title: Writing Tests
+lastUpdated: 2026-02-20
+---
+
+## Frontend Tests
+
+We use `vitest` to run the frontend tests. (See [vite.config.ts](https://github.com/invoke-ai/InvokeAI/blob/main/invokeai/frontend/web/vite.config.mts) for the default `vitest` options.)
+
+{/* TODO: Finish frontend tests docs */}
+
+## Backend Tests
+
+We use `pytest` to run the backend python tests. (See [pyproject.toml](https://github.com/invoke-ai/InvokeAI/blob/main/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/`](https://github.com/invoke-ai/InvokeAI/tree/main/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`.
+
+:::note HTML coverage report output example
+ 
+
+ 
+:::
diff --git a/docs/src/content/docs/development/Process/pr-merge-policy.mdx b/docs/src/content/docs/development/Process/pr-merge-policy.mdx
new file mode 100644
index 00000000000..ebd08feaaf0
--- /dev/null
+++ b/docs/src/content/docs/development/Process/pr-merge-policy.mdx
@@ -0,0 +1,72 @@
+---
+title: PR Merge Policy
+lastUpdated: 2026-02-19
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+This document outlines the process for reviewing and merging pull requests (PRs) into the InvokeAI repository.
+
+## Review Process
+
+
+ 1. Assignment
+
+ One of the repository maintainers will assign collaborators to review a pull request. The assigned reviewer(s) will be responsible for conducting the code review.
+
+ 2. Review and Iteration
+
+ The assignee is responsible for:
+ - Reviewing the PR thoroughly
+ - Providing constructive feedback
+ - Iterating with the PR author until the assignee is satisfied that the PR is fit to merge
+ - Ensuring the PR meets code quality standards, follows project conventions, and doesn't introduce bugs or regressions
+
+ 3. Approval and Notification
+
+ Once the assignee is satisfied with the PR:
+ - The assignee approves the PR
+ - The assignee alerts one of the maintainers that the PR is ready for merge using the **#request-reviews Discord channel**
+
+ 4. Final Merge
+
+ One of the maintainers is responsible for:
+ - Performing a final check of the PR
+ - Merging the PR into the appropriate branch
+
+ :::caution[Important]
+ Collaborators are strongly discouraged from merging PRs on their own, except in case of emergency (e.g., critical bug fix and no maintainer is available).
+ :::
+
+ 5. Release Policy
+
+ Once a feature release candidate is published, no feature PRs are to
+ be merged into main. Only bugfixes are allowed until the final
+ release.
+
+
+## Best Practices
+
+### Clean Commit History
+
+To encourage a clean development log, PR authors are encouraged to use `git rebase -i` to suppress trivial commit messages (e.g., `ruff` and `prettier` formatting fixes) after the PR is accepted but before it is merged.
+
+### Merge Strategy
+
+The maintainer will perform either a **3-way merge** or **squash merge** when merging a PR into the `main` branch. This approach helps avoid rebase conflict hell and maintains a cleaner project history.
+
+### Attribution
+
+The PR author should reference any papers, source code or
+documentation that they used while creating the code both in the PR
+and as comments in the code itself. If there are any licensing
+restrictions, these should be linked to and/or reproduced in the repo
+root.
+
+## Summary
+
+This policy ensures that:
+- All PRs receive proper review from assigned collaborators
+- Maintainers have final oversight before code enters the main branch
+- The commit history remains clean and meaningful
+- Merge conflicts are minimized through appropriate merge strategies
diff --git a/docs/src/content/docs/development/Process/release-process.mdx b/docs/src/content/docs/development/Process/release-process.mdx
new file mode 100644
index 00000000000..9869e4940e2
--- /dev/null
+++ b/docs/src/content/docs/development/Process/release-process.mdx
@@ -0,0 +1,157 @@
+---
+title: Release Process
+lastUpdated: 2025-12-26
+---
+
+The Invoke application is published as a python package on [PyPI]. This includes both a source distribution and built distribution (a wheel).
+
+Most users install it with the [Launcher](https://github.com/invoke-ai/launcher/), others with `pip`.
+
+The launcher uses GitHub as the source of truth for available releases.
+
+## Broad Strokes
+
+- Merge all changes and bump the version in the codebase.
+- Tag the release commit.
+- Wait for the release workflow to complete.
+- Approve the PyPI publish jobs.
+- Write GH release notes.
+
+## General Prep
+
+Make a developer call-out for PRs to merge. Merge and test things
+out. Create a branch with a name like user/chore/vX.X.X-prep and bump the version by editing
+`invokeai/version/invokeai_version.py` and commit locally.
+
+## Release Workflow
+
+The `release.yml` workflow runs a number of jobs to handle code checks, tests, build and publish on PyPI.
+
+It is triggered on **tag push**, when the tag matches `v*`.
+
+### Triggering the Workflow
+
+Ensure all commits that should be in the release are merged into this branch, and that you have pulled them locally.
+
+Run `make tag-release` to tag the current commit and kick off the workflow. You will be prompted to provide a message - use the version specifier.
+
+If this version's tag already exists for some reason (maybe you had to make a last minute change), the script will overwrite it.
+
+Push the commit to trigger the workflow.
+
+> In case you cannot use the Make target, the release may also be dispatched [manually] via GH.
+
+### Workflow Jobs and Process
+
+The workflow consists of a number of concurrently-run checks and tests, then two final publish jobs.
+
+The publish jobs require manual approval and are only run if the other jobs succeed.
+
+#### `check-version` Job
+
+This job ensures that the `invokeai` python package version specifier matches the tag for the release. The version specifier is pulled from the `__version__` variable in `invokeai/version/invokeai_version.py`.
+
+This job uses [samuelcolvin/check-python-version].
+
+> Any valid [version specifier] works, so long as the tag matches the version. The release workflow works exactly the same for `RC`, `post`, `dev`, etc.
+
+#### Check and Test Jobs
+
+Next, these jobs run and must pass. They are the same jobs that are run for every PR.
+
+- **`python-tests`**: runs `pytest` on matrix of platforms
+- **`python-checks`**: runs `ruff` (format and lint)
+- **`frontend-tests`**: runs `vitest`
+- **`frontend-checks`**: runs `prettier` (format), `eslint` (lint), `dpdm` (circular refs), `tsc` (static type check) and `knip` (unused imports)
+- **`typegen-checks`**: ensures the frontend and backend types are synced
+
+#### `build-wheel` Job
+
+This sets up both python and frontend dependencies and builds the python package. Internally, this runs `./scripts/build_wheel.sh` and uploads `dist.zip`, which contains the wheel and unarchived build.
+
+You don't need to download or test these artifacts.
+
+#### Sanity Check & Smoke Test
+
+At this point, the release workflow pauses as the remaining publish jobs require approval.
+
+It's possible to test the python package before it gets published to PyPI. We've never had problems with it, so it's not necessary to do this.
+
+But, if you want to be extra-super careful, here's how to test it:
+
+- Download the `dist.zip` build artifact from the `build-wheel` job
+- Unzip it and find the wheel file
+- Create a fresh Invoke install by following the [manual install guide](/start-here/manual/) - but instead of installing from PyPI, install from the wheel
+- Test the app
+
+##### Something isn't right
+
+If testing reveals any issues, no worries. Cancel the workflow, which will cancel the pending publish jobs (you didn't approve them prematurely, right?) and start over.
+
+#### PyPI Publish Jobs
+
+The publish jobs will not run if any of the previous jobs fail.
+
+They use [GitHub environments], which are configured as [trusted publishers] on PyPI.
+
+Both jobs require a @lstein or @blessedcoolant to approve them from the workflow's **Summary** tab.
+
+- Click the **Review deployments** button
+- Select the environment (either `testpypi` or `pypi` - typically you select both)
+- Click **Approve and deploy**
+
+> **If the version already exists on PyPI, the publish jobs will fail.** PyPI only allows a given version to be published once - you cannot change it. If version published on PyPI has a problem, you'll need to "fail forward" by bumping the app version and publishing a followup release.
+
+##### Failing PyPI Publish
+
+Check the [python infrastructure status page] for incidents.
+
+If there are no incidents, contact @lstein or @blessedcoolant, who have owner access to GH and PyPI, to see if access has expired or something like that.
+
+#### `publish-testpypi` Job
+
+Publishes the distribution on the [Test PyPI] index, using the `testpypi` GitHub environment.
+
+This job is not required for the production PyPI publish, but included just in case you want to test the PyPI release for some reason:
+
+- Approve this publish job without approving the prod publish
+- Let it finish
+- Create a fresh Invoke install by following the [manual install guide](/start-here/manual/), making sure to use the Test PyPI index URL: `https://test.pypi.org/simple/`
+- Test the app
+
+#### `publish-pypi` Job
+
+Publishes the distribution on the production PyPI index, using the `pypi` GitHub environment.
+
+It's a good idea to wait to approve and run this job until you have the release notes ready!
+
+## Prep and publish the GitHub Release
+
+1. [Draft a new release] on GitHub, choosing the tag that triggered the release.
+2. The **Generate release notes** button automatically inserts the changelog and new contributors. Make sure to select the correct tags for this release and the last stable release. GH often selects the wrong tags - do this manually.
+3. Write the release notes, describing important changes. Contributions from community members should be shouted out. Use the GH-generated changelog to see all contributors. If there are Weblate translation updates, open that PR and shout out every person who contributed a translation.
+4. Check **Set as a pre-release** if it's a pre-release.
+5. Approve and wait for the `publish-pypi` job to finish if you haven't already.
+6. Publish the GH release.
+7. Post the release in Discord in the [releases](https://discord.com/channels/1020123559063990373/1149260708098359327) channel with abbreviated notes. For example:
+ > Invoke v5.7.0 (stable): [https://github.com/invoke-ai/InvokeAI/releases/tag/v5.7.0](https://github.com/invoke-ai/InvokeAI/releases/tag/v5.7.0)
+ >
+ > It's a pretty big one - Form Builder, Metadata Nodes (thanks @SkunkWorxDark!), and much more.
+8. Right click the message in releases and copy the link to it. Then, post that link in the [new-release-discussion](https://discord.com/channels/1020123559063990373/1149506274971631688) channel. For example:
+ > Invoke v5.7.0 (stable): [https://discord.com/channels/1020123559063990373/1149260708098359327/1344521744916021248](https://discord.com/channels/1020123559063990373/1149260708098359327/1344521744916021248)
+
+## Manual Release
+
+The `release` workflow can be dispatched manually. You must dispatch the workflow from the right tag, else it will fail the version check.
+
+This functionality is available as a fallback in case something goes wonky. Typically, releases should be triggered via tag push as described above.
+
+[PyPI]: https://pypi.org/
+[Draft a new release]: https://github.com/invoke-ai/InvokeAI/releases/new
+[Test PyPI]: https://test.pypi.org/
+[version specifier]: https://packaging.python.org/en/latest/specifications/version-specifiers/
+[GitHub environments]: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment
+[trusted publishers]: https://docs.pypi.org/trusted-publishers/
+[samuelcolvin/check-python-version]: https://github.com/samuelcolvin/check-python-version
+[manually]: #manual-release
+[python infrastructure status page]: https://status.python.org/
diff --git a/docs/src/content/docs/development/Setup/dev-environment.mdx b/docs/src/content/docs/development/Setup/dev-environment.mdx
new file mode 100644
index 00000000000..a92223e3ebb
--- /dev/null
+++ b/docs/src/content/docs/development/Setup/dev-environment.mdx
@@ -0,0 +1,275 @@
+---
+title: Development Environment
+lastUpdated: 2026-02-19
+---
+
+import { LinkCard, Steps, Tabs, TabItem, FileTree, LinkButton } from '@astrojs/starlight/components'
+import SystemRequirementsLink from '@components/SystemRequirmentsLink.astro'
+
+:::caution
+ Invoke uses a SQLite database. When you run the application as a dev install, you accept responsibility for your database. This means making regular backups (especially before pulling) and/or fixing it yourself in the event that a PR introduces a schema change.
+
+ If you don't need to persist your db, you can use an ephemeral in-memory database by setting `use_memory_db: true` in your `invokeai.yaml` file. You'll also want to set `scan_models_on_startup: true` so that your models are registered on startup.
+:::
+
+## Initial Setup
+
+
+ 1. Refer to the system requirements.
+
+
+
+ 2. Fork and clone the InvokeAI git repository.
+
+
+ Fork Repository
+
+
+ Next, clone your fork to your local machine. You can use either HTTPS or SSH, depending on your git configuration.
+
+ 3. This repository uses Git LFS to manage large files. To ensure all assets are downloaded:
+ - Install git-lfs → [Download here](https://git-lfs.com/)
+ - Enable automatic LFS fetching for this repository:
+ ```shell
+ git config lfs.fetchinclude "*"
+ ```
+ - Fetch files from LFS (only needs to be done once; subsequent `git pull` will fetch changes automatically):
+ ```shell
+ git lfs pull
+ ```
+ 4. Create a directory for user data (images, models, db, etc). This is typically at `~/invokeai`, but if you already have a non-dev install, you may want to create a separate directory for the dev install.
+ 5. Follow the [manual install](/start-here/manual/) guide, with some modifications to the install command:
+
+ - Use `.` instead of `invokeai` to install from the current directory. You don't need to specify the version.
+ - Use `uv sync` instead of `uv pip install` so the environment is synchronized from the repository lockfile.
+ - The current project is installed as an editable install by default. That means your changes to the python code will be reflected when you restart the Invoke server.
+ - Add the `dev`, `test`, `docs`, and appropriate GPU package options with `--extra`. You may or may not need the `xformers` option - follow the manual install guide to figure that out.
+
+ With the modifications made, the sync command should look something like this:
+
+ ```sh
+ uv sync --frozen \
+ --python 3.12 \
+ --managed-python \
+ --extra dev \
+ --extra test \
+ --extra docs \
+ --extra cuda \
+ --extra xformers
+ ```
+ 6. At this point, you should have Invoke installed, a venv set up and activated, and the server running. But you will see a warning in the terminal that no UI was found. If you go to the URL for the server, you won't get a UI.
+
+ This is because the UI build is not distributed with the source code. You need to build it manually. End the running server instance.
+
+ *(If you only want to edit the docs, you can stop here and skip to the **Documentation** section below.)*
+
+ 7. Install the frontend dev toolchain, paying attention to versions:
+
+ - [`nodejs`](https://nodejs.org/) (tested on LTS, v22)
+ - [`pnpm`](https://pnpm.io/installation) (tested on v10)
+
+ 8. Do a production build of the frontend:
+
+ ```sh
+ cd /invokeai/frontend/web
+ pnpm i
+ pnpm build
+ ```
+
+ 9. Restart the server and navigate to the URL. You should get a UI. After making changes to the python code, restart the server to see those changes.
+
+
+## Backend Development
+
+Experimenting with changes to the Python source code is a drag if you have to re-start the server and re-load multi-gigabyte models after every change.
+
+For a faster development workflow, add the `--dev_reload` flag when starting the server. The server will watch for changes to all the Python files in the `invokeai` directory and apply those changes to the running server on the fly.
+
+This will allow you to avoid restarting the server (and reloading models) in most cases, but there are some caveats; see the [jurigged documentation](https://github.com/breuleux/jurigged#caveats) for details.
+
+### Testing
+
+The backend tests require the `test` dependency group, which you installed during the initial setup.
+
+See the [Tests](../tests) documentation for information about running and writing tests.
+
+## Frontend Development
+
+You'll need to run `pnpm build` every time you pull in new changes to the frontend.
+
+Another option is to skip the build and instead run the UI in dev mode:
+
+```sh
+cd invokeai/frontend/web
+pnpm dev
+```
+
+This starts a vite dev server for the UI at `127.0.0.1:5173`, which you will use instead of `127.0.0.1:9090`.
+
+The dev mode is substantially slower than the production build but may be more convenient if you just need to test things out. It will hot-reload the UI as you make changes to the frontend code. Sometimes the hot-reload doesn't work, and you need to manually refresh the browser tab.
+
+## Documentation
+
+This documentation is built on [Astro Starlight](https://starlight.astro.build/). It provides a pleasant developer environment for writing engaging documentation, and is built on top of the Astro static site generator, which provides a powerful and flexible framework for building fast, modern websites.
+
+To contribute to the documentation, simply edit the markdown files in the `./docs` directory. You can run a local dev server with hot-reloading for changes made to the docs.
+
+
+ - **docs**
+ - public/
+ - src
+ - content docs content lives here
+ - docs
+ - lib
+ - components/
+ - utils/
+ - content.config.ts
+ - scripts/
+ - tests/
+ - invokeai/
+ - docker/
+ - coverage/
+
+
+
+ 1. Navigate to the `docs` directory and install the dependencies:
+
+ ```sh
+ cd docs
+ pnpm install
+ ```
+ 2. Start the dev server:
+
+ ```sh
+ pnpm run dev
+ ```
+
+
+## VSCode Setup
+
+VSCode offers excellent tools for InvokeAI development, including a python debugger, automatic virtual environment activation, and remote development capabilities.
+
+### Prerequisites
+
+First, ensure you have the following extensions installed:
+- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
+- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
+
+It's also highly recommended to install the Jupyter extensions if you plan on working with notebooks:
+- [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter)
+- [Jupyter Cell Tags](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-cell-tags)
+- [Jupyter Notebook Renderers](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter-renderers)
+- [Jupyter Slide Show](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-jupyter-slideshow)
+
+### Configuration
+
+
+
+ Creating a VSCode workspace for working on InvokeAI is highly recommended to hold InvokeAI-specific settings and configs.
+
+ 1. Open the InvokeAI repository directory in VSCode
+ 2. Go to `File` > `Save Workspace As` and save it *outside* the repository
+
+ **Default Python Interpreter**
+
+ To enable automatic virtual environment activation:
+ 1. Open the command palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) and run `Preferences: Open Workspace Settings (JSON)`
+ 2. Add `python.defaultInterpreterPath` to your settings, pointing to your virtual environment's python executable:
+
+ ```jsonc
+ {
+ "folders": [
+ { "path": "InvokeAI" },
+ { "path": "/path/to/invokeai_root" }
+ ],
+ "settings": {
+ "python.defaultInterpreterPath": "/path/to/invokeai_root/.venv/bin/python"
+ }
+ }
+ ```
+ Now, opening the integrated terminal or running python will automatically use your InvokeAI virtual environment.
+
+
+
+ We use Python's typing system in InvokeAI. PR reviews will include checking that types are present and correct.
+
+ Pylance provides type checking in the editor. To enable it:
+
+ 1. Open a Python file
+ 2. Look along the status bar in VSCode for `{ } Python`
+ 3. Click the `{ }`
+ 4. 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.
+
+
+
+ Debugging configs are managed in a `launch.json` file. Follow the [official guide](https://code.visualstudio.com/docs/python/debugging) to set up your `launch.json` and try it out.
+
+ Add these InvokeAI debugging configurations to your `launch.json`:
+
+ ```jsonc
+ {
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "InvokeAI Web",
+ "type": "python",
+ "request": "launch",
+ "program": "scripts/invokeai-web.py",
+ "args": [
+ "--root", "/path/to/invokeai_root",
+ "--host", "0.0.0.0"
+ ],
+ "justMyCode": true
+ },
+ {
+ "name": "InvokeAI CLI",
+ "type": "python",
+ "request": "launch",
+ "program": "scripts/invokeai-cli.py",
+ "justMyCode": true
+ },
+ {
+ "name": "InvokeAI Test",
+ "type": "python",
+ "request": "launch",
+ "module": "pytest",
+ "args": ["--capture=no"],
+ "justMyCode": true
+ },
+ {
+ "name": "InvokeAI Single Test",
+ "type": "python",
+ "request": "launch",
+ "module": "pytest",
+ "args": ["tests/nodes/test_invoker.py"],
+ "justMyCode": true
+ }
+ ]
+ }
+ ```
+
+
+
+ This provides a smooth experience for running the backend on a powerful Linux machine while developing on another device.
+
+ Consult the [official guide](https://code.visualstudio.com/docs/remote/remote-overview) to get it set up. We suggest using VSCode's included settings sync so that your remote dev host has all the same app settings and extensions automatically.
+
+ :::tip[Port Forwarding]
+ Automatic port forwarding can be flaky. You can disable it in `Preferences: Open Remote Settings (ssh: hostname)` by unticking `remote.autoForwardPorts`.
+
+ To forward ports reliably, use SSH on the remote dev client:
+ ```bash
+ ssh -L 9090:localhost:9090 -L 5173:localhost:5173 user@remote-dev-host
+ ```
+ Run this outside the VSCode integrated terminal so it persists across VSCode restarts.
+ :::
+
+
diff --git a/docs/src/content/docs/development/index.mdx b/docs/src/content/docs/development/index.mdx
new file mode 100644
index 00000000000..77e362bc10d
--- /dev/null
+++ b/docs/src/content/docs/development/index.mdx
@@ -0,0 +1,48 @@
+---
+title: InvokeAI Development
+sidebar:
+ order: 1
+lastUpdated: 2026-02-19
+---
+
+import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components';
+
+This section of the documentation is for developers interested in contributing to the InvokeAI codebase, or building on top of it. It includes guides for setting up your development environment, understanding the project structure, and making your first contribution.
+
+
+
+ Instructions for setting up your local development environment, including how to run the project locally and how to set up your tooling.
+
+
+ Learn more
+
+
+
+ An introduction to the front end codebase, including the technologies used and how to get started.
+
+
+ Learn more
+
+
+
+ A collection of guides for common development tasks, such as adding new model architectures, making tests, and more.
+
+
+ Learn more
+
+
+
+ An overview of the InvokeAI architecture, including the major components and how they interact.
+
+
+ Learn more
+
+
+
+ An overview of the development processes we follow, including our pull request merge policy and release process.
+
+
+ Learn more
+
+
+
diff --git a/docs/src/content/docs/features/Canvas/canvas-projects.mdx b/docs/src/content/docs/features/Canvas/canvas-projects.mdx
new file mode 100644
index 00000000000..92c549abe2b
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/canvas-projects.mdx
@@ -0,0 +1,60 @@
+---
+title: Canvas Projects
+sidebar:
+ badge: New
+ order: 7
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+Canvas Projects let you save the entire state of a canvas — including all layers, masks, reference images, generation parameters, and LoRAs — into a single `.invk` file that you can reopen later or share with someone else.
+
+`.invk` files are ZIP archives. When saved images can be fetched successfully from the server, they embed the actual image bytes for every layer and reference image, so a project is self-contained: opening it on another machine or after wiping the gallery can restore those images.
+
+## Saving a project
+
+
+1. Open the canvas and arrange your layers, masks, reference images, and parameters the way you want them.
+2. Open the archive menu in the canvas toolbar, or open the canvas context menu and choose **Project**.
+3. Choose **Save Canvas Project**.
+4. Optionally rename the project (the default is **Canvas Project**).
+5. Save the `.invk` file to disk.
+
+
+What gets saved:
+
+- All raster, inpaint, and control layers, with their image data, transforms, opacity, and lock state.
+- All masks.
+- Reference images.
+- Currently configured generation parameters (model, prompts, scheduler, seed, dimensions, etc.).
+- LoRAs and their weights.
+
+## Loading a project
+
+
+1. Open the archive menu in the canvas toolbar, or open the canvas context menu and choose **Project**.
+2. Choose **Load Canvas Project**.
+3. Pick the `.invk` file.
+
+
+When a project is loaded, the canvas is replaced with the project's state. LoRAs are reset first, then re-applied from the project, so opening a project never leaves stale LoRAs from your previous session attached.
+
+### Image deduplication
+
+Loading a project does **not** blindly re-upload every embedded image. Invoke compares each embedded image against what is already in your gallery and only uploads the images that are missing. Re-opening the same project a second time, or opening it shortly after saving it, is therefore very fast — most or all images will already be on the server.
+
+This also means a project shared with another user will upload its missing embedded images the first time it is opened on that user's machine, then become nearly free to re-open after that.
+
+To keep the gallery responsive during large imports, image fetches and uploads are limited to a small number of concurrent requests.
+
+## What `.invk` does *not* save
+
+A `.invk` file is a canvas state snapshot. It does **not** contain:
+
+- The models, LoRAs, or embeddings themselves — only references to them. If you share a project, the recipient needs the same models installed (or compatible substitutes).
+- Workflow editor state (use **Save Workflow** in the workflow editor for that).
+- Gallery boards or images outside the canvas.
+
+## Sharing projects
+
+`.invk` files are safe to share directly. The recipient loads the file from the canvas toolbar archive menu or canvas context menu. They'll need any referenced models / LoRAs installed locally; if a referenced model is missing, the parameter slot will be empty and they can pick a substitute before generating.
diff --git a/docs/src/content/docs/features/Canvas/gradient-tool.mdx b/docs/src/content/docs/features/Canvas/gradient-tool.mdx
new file mode 100644
index 00000000000..0c9dee0f929
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/gradient-tool.mdx
@@ -0,0 +1,75 @@
+---
+title: Gradient Tool
+description: Learn how to paint linear and radial gradients on canvas raster layers.
+lastUpdated: 2026-05-16
+sidebar:
+ order: 4
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+The Gradient tool paints a smooth transition between your current foreground and background colors on the canvas.
+
+You can activate the Gradient tool from the canvas toolbar.
+
+## Where Gradient Draws
+
+Gradient only draws into the **active raster layer**:
+
+- It does not draw into inpaint masks.
+- It does not draw into other non-raster layer types.
+- The result is always clipped to the current **generation bounding box**.
+
+If a raster layer is not selected, the tool is unavailable.
+
+## Common Behavior
+
+- Click and drag to define the gradient.
+- Release the pointer to commit the gradient.
+- Press Esc to discard the in-progress gradient.
+- Hold Alt to temporarily switch to the color picker.
+- Hold Space to temporarily switch to panning.
+
+The Gradient tool uses the current **FG/BG color pair**:
+
+- The **active** color swatch becomes the start color.
+- The **inactive** color swatch becomes the end color.
+
+## Gradient Modes
+
+
+
+ Click and drag to set the gradient direction. The drag defines the transition from the start color to the end
+ color.
+
+
+ Click to place the center, then drag outward to set the radius. The gradient fades from the start color at the
+ center to the end color toward the outside.
+
+
+
+## Clip Gradient
+
+The toolbar includes a **Clip Gradient** toggle:
+
+- **Enabled:** Limits the gradient to the dragged region.
+- **Disabled:** Lets the gradient extend across the full current bounding box.
+
+In practice:
+
+- A clipped **linear** gradient is limited to the span you dragged.
+- A clipped **radial** gradient is limited to the circle you dragged out.
+- With clipping disabled, both modes can be used to wash the entire bbox with a full gradient transition.
+
+## Practical Examples
+
+- Use **Linear** for sky fades, shadow ramps, and broad directional lighting.
+- Use **Radial** for vignettes, glows, spotlights, and soft falloff around a focal point.
+- Disable **Clip Gradient** when you want a full-bbox color transition.
+- Keep **Clip Gradient** enabled when you only want to affect a localized area.
+
+## Summary
+
+The Gradient tool is a raster-only canvas tool for painting linear and radial color transitions. Use it when you want
+soft blends between your FG and BG colors, and use **Clip Gradient** to decide whether the effect stays local or fills
+the full bbox.
diff --git a/docs/src/content/docs/features/Canvas/lasso-tool.mdx b/docs/src/content/docs/features/Canvas/lasso-tool.mdx
new file mode 100644
index 00000000000..7cc676aa6f9
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/lasso-tool.mdx
@@ -0,0 +1,77 @@
+---
+title: Lasso Tool
+description: Learn how to create and refine inpaint masks with the Lasso tool.
+lastUpdated: 2026-05-15
+sidebar:
+ order: 2
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+The Lasso tool is the canvas's dedicated masking tool. It always draws into **inpaint mask layers** and is designed
+for quickly defining irregular regions for inpainting.
+
+You can activate the Lasso tool from the canvas toolbar or with the default hotkey L .
+
+## Where Lasso Draws
+
+Lasso always targets an **enabled inpaint mask**:
+
+- If an enabled inpaint mask is currently selected, Lasso draws into that mask.
+- If no enabled inpaint mask is available, Lasso creates a new inpaint mask automatically and commits the contour
+ there.
+
+:::note
+If a disabled inpaint mask is selected, Lasso does not draw into the disabled mask. It creates a new enabled mask for
+the next contour instead.
+:::
+
+## Common Behavior
+
+- Lasso always commits a **closed contour**.
+- Hold Ctrl on Windows/Linux or Cmd on macOS to switch to **subtractive** mode and remove area
+ from the mask instead of adding to it.
+- Press Esc to cancel the current lasso session.
+- Hold Space during an active session to pan the viewport without discarding the unfinished contour.
+
+## Lasso Modes
+
+
+
+ Click and drag to sketch an irregular contour. Releasing the pointer closes and commits the contour automatically.
+
+
+ Click to place vertices. Click the first point to close and commit the contour. Hold Shift while
+ placing the next edge to snap it to horizontal, vertical, and 45 degree angles.
+
+
+
+## Moving and Panning During Drawing
+
+The Lasso tool uses Space for panning in both modes:
+
+- **Freehand:** While drawing, hold Space to pan the viewport without discarding the unfinished contour.
+ Release Space to continue drawing.
+- **Polygon:** During an active polygon session, hold Space to pan the viewport without discarding the
+ unfinished contour. Release Space and continue placing points.
+
+This is especially useful when drawing large mask regions that extend beyond the current viewport.
+
+## Working With Masks
+
+- Use **Freehand** for organic shapes like hair, smoke, foliage, fabric, and quick blocking.
+- Use **Polygon** when you need straight edges and deliberate corner placement.
+- Use **subtractive mode** to trim or punch holes in an existing inpaint mask.
+- Use Lasso when you want mask-first editing behavior without first creating a mask layer by hand.
+
+## Practical Notes
+
+- Polygon mode shows the starting point so you can close the contour precisely.
+- After at least three polygon points, moving near the start point lets you click it to finish the shape.
+- Freehand is faster for loose silhouettes. Polygon is better when edge placement matters.
+
+## Summary
+
+The Lasso tool is the fastest way to create and refine inpaint masks on the canvas. Use Freehand for organic regions,
+Polygon for hard edges, and hold Ctrl /Cmd whenever you need to subtract from the mask instead of
+adding to it.
diff --git a/docs/src/content/docs/features/Canvas/layers-and-drops.mdx b/docs/src/content/docs/features/Canvas/layers-and-drops.mdx
new file mode 100644
index 00000000000..eadc002d696
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/layers-and-drops.mdx
@@ -0,0 +1,35 @@
+---
+title: Layer Tips
+sidebar:
+ order: 6
+---
+
+A couple of layer-related behaviors that aren't obvious from the canvas UI alone.
+
+## Drag & drop targets
+
+Dragging an image onto the canvas reveals **five** drop zones, arranged as two zones on top and three on the bottom:
+
+| Top row | |
+| :--- | :--- |
+| **New Raster Layer** | Create a regular raster layer from the dropped image. |
+| **New Control Layer** | Create a control layer from the dropped image. |
+
+| Bottom row | |
+| :--- | :--- |
+| **New Regional Reference** | Use the image as a regional reference. |
+| **New Inpaint Mask** | Create a new inpaint mask layer using the image as the mask source. |
+| **New Resized Control Layer** | Create a control layer resized to the current canvas dimensions. |
+
+You can drop from the gallery, from disk, or from any panel that shows a draggable image.
+
+## Lock transparency on raster layers
+
+Each raster layer has a **Lock Transparency** toggle (drop icon) in its layer header. When enabled, brush strokes only affect existing non-transparent pixels — painting over transparent areas does nothing. This behaves like Photoshop's "Lock Transparent Pixels".
+
+Typical uses:
+
+- **Recolor an existing shape** without bleeding paint into the empty space around it.
+- **Refine details on a subject** that was painted on an otherwise transparent layer, with no risk of growing its silhouette.
+
+Toggle it off to resume normal painting. The lock is per-layer, so different layers can be locked or unlocked independently. Pressure-sensitive pen input and undo/redo both respect the lock.
diff --git a/docs/src/content/docs/features/Canvas/run-workflow.mdx b/docs/src/content/docs/features/Canvas/run-workflow.mdx
new file mode 100644
index 00000000000..4134072c7f8
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/run-workflow.mdx
@@ -0,0 +1,70 @@
+---
+title: Run Workflow on Canvas
+sidebar:
+ order: 5
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+You can run any workflow against a raster layer directly from the canvas. The selected layer is passed in as the workflow's image input, and the results land in the canvas staging area where you can review and accept them — without leaving the canvas tab.
+
+## Requirements for a workflow
+
+For a workflow to be available from the canvas, it must satisfy three conditions:
+
+1. **Form Builder is enabled.** The workflow's parameters are presented through the Form Builder UI when the workflow is launched from the canvas, so the workflow needs to have a form configured.
+2. **At least one image input field.** The layer you right-click on is passed into the first eligible image field as the workflow's input.
+3. **At least one `Canvas Output` node.** This is the node that marks which images should be routed back to the canvas staging area.
+
+Workflows that do not meet all three are filtered out of the canvas workflow selector.
+
+## The `Canvas Output` node
+
+`Canvas Output` is a dedicated workflow node that explicitly marks the images you want shown in the canvas staging area. Add it at the end of any branch whose output should appear on the canvas.
+
+A workflow can include **multiple `Canvas Output` nodes**. Each one becomes its own entry in the staging area, with an individually selectable thumbnail. You can navigate between entries with the arrow keys and accept just one of them onto the canvas.
+
+:::note[Why an explicit node?]
+Earlier versions detected output images heuristically (by scanning for `board` fields). That was fragile and caused unrelated nodes — for example, `save_image` — to be mistaken for canvas outputs. `Canvas Output` makes the routing intentional.
+:::
+
+## Running a workflow
+
+
+1. On the canvas, **right-click a raster layer** to open its context menu.
+2. Choose **Run Workflow**.
+3. Pick a workflow from the list. Only workflows that meet the [requirements](#requirements-for-a-workflow) appear here.
+4. Adjust any exposed parameters in the form. All form field types are supported: text, numbers, booleans, enums, schedulers, boards, models, and images.
+5. Click **Run**. The workflow is queued and the results stream into the staging area as they complete.
+
+
+The current layer is automatically passed into the workflow's image input — you do not need to select an image manually.
+
+## Reviewing and accepting results
+
+Results appear in the canvas staging area strip at the bottom of the canvas:
+
+- If the workflow has a single `Canvas Output`, you get one thumbnail per run.
+- If it has multiple `Canvas Output` nodes, each run produces multiple thumbnails, one per output node.
+- Use the staging area's next / previous controls (or arrow keys) to cycle through entries. Navigation wraps across run boundaries.
+- Click **Accept** to commit the currently selected entry onto the canvas. Only that single image is committed — siblings stay in staging until you accept or discard them.
+
+## Troubleshooting
+
+### My workflow doesn't appear in the selector
+
+Check, in order:
+
+- The workflow has Form Builder enabled.
+- The workflow has at least one image input field.
+- The workflow contains at least one `Canvas Output` node.
+
+If any of these is missing, the workflow is hidden.
+
+### Queueing fails with a "BoardField" validation error
+
+This was a known issue with workflows that combined `save_image` and `canvas_output` nodes. It is fixed — update Invoke and try again.
+
+### Errors during execution
+
+Workflow errors are surfaced as toasts and the staging area is cleaned up so it returns to a usable state. Open the queue panel for the full error message.
diff --git a/docs/src/content/docs/features/Canvas/shapes-tool.mdx b/docs/src/content/docs/features/Canvas/shapes-tool.mdx
new file mode 100644
index 00000000000..ba3fb782a07
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/shapes-tool.mdx
@@ -0,0 +1,99 @@
+---
+title: Shapes Tool
+description: Learn how to draw filled shapes on raster and inpaint mask layers with the Shapes tool.
+lastUpdated: 2026-05-11
+sidebar:
+ order: 1
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+The Shapes tool is a general-purpose filled-shape drawing tool for the canvas. It replaces the old Rectangle tool and
+adds four shape modes under a single toolbar button:
+
+- **Rect**
+- **Oval**
+- **Polygon**
+- **Freehand**
+
+You can activate the Shapes tool from the canvas toolbar or with the default hotkey U .
+
+## Where Shapes Draws
+
+Shapes always draws into the **active raster target**:
+
+- On a regular raster layer, Shapes adds filled pixels to that layer.
+- On an active inpaint mask layer, Shapes draws directly into the mask.
+
+:::note
+Shapes overlaps with some Lasso workflows on mask layers, but the tools are not identical. Lasso is still the more
+specialized masking tool and can create a new mask layer automatically when one does not already exist. See the
+[Lasso tool guide](./lasso-tool/) for mask-specific behavior.
+:::
+
+## Common Behavior
+
+- Shapes preview live while you draw.
+- The fill color uses the current active color.
+- On a raster layer, the active color's alpha is respected when adding pixels.
+- Hold Ctrl on Windows/Linux or Cmd on macOS to switch to **subtractive** mode and cut pixels
+ out of the active layer.
+- In subtractive mode, alpha is ignored and the shape fully clears pixels.
+- Press Esc to cancel the current shape session.
+
+:::tip
+When subtractive mode is active, the canvas cursor shows a small minus badge so you can tell at a glance that the next
+shape will erase instead of fill.
+:::
+
+## Shape Modes
+
+
+
+ Drag to draw a rectangle. Hold Shift to constrain to a square. Hold Alt to draw from the
+ center instead of from a corner.
+
+
+ Drag to draw an ellipse. Hold Shift to constrain to a perfect circle. Hold Alt to draw from
+ the center.
+
+
+ Click to place vertices. Click the first point to close and commit the shape. Hold Shift to snap the
+ pending edge to horizontal, vertical, and 45 degree angles.
+
+
+ Click and drag to sketch a filled freehand contour. Release the pointer to commit the shape.
+
+
+
+## Moving and Panning During Drawing
+
+The Shapes tool supports different Space behavior depending on the current mode:
+
+- **Rect / Oval:** While the pointer is still down, hold Space to move the uncommitted shape instead of
+ resizing it. Release Space to continue resizing.
+- **Polygon / Freehand:** Hold Space during an active session to pan the viewport without discarding the
+ unfinished shape.
+
+This is especially useful when drawing large shapes that extend beyond the current viewport.
+
+## Color Picking While Using Shapes
+
+The Alt key behaves differently depending on the active Shapes mode:
+
+- **Rect / Oval:** Before you start dragging, Alt can be used for the temporary color-picker quick-switch.
+ Once a drag is active, Alt is reserved for drawing from the center.
+- **Polygon:** Alt remains available for the temporary color-picker quick-switch between vertex placements.
+- **Freehand:** Alt is available before the stroke starts, but not during an active stroke.
+
+## Practical Examples
+
+- Use **Rect** or **Oval** to quickly add clean filled regions.
+- Use **Polygon** when you need straight edges and deliberate corner placement.
+- Use **Freehand** for irregular organic regions.
+- Use **subtractive mode** to cut holes back out of an existing filled region.
+
+## Summary
+
+The Shapes tool is the fastest way to add filled geometric or freeform regions to canvas layers. Use it for structured
+fills, mask authoring, and precise subtractive edits without switching away from the current raster target.
diff --git a/docs/src/content/docs/features/Canvas/text-tool.mdx b/docs/src/content/docs/features/Canvas/text-tool.mdx
new file mode 100644
index 00000000000..6e223767d8e
--- /dev/null
+++ b/docs/src/content/docs/features/Canvas/text-tool.mdx
@@ -0,0 +1,33 @@
+---
+title: Text Tool
+sidebar:
+ order: 3
+---
+
+import { LinkCard } from '@astrojs/starlight/components';
+
+## Font selection
+
+The Text tool uses a set of predefined font stacks. When you choose a font, the app resolves the first available font on your system from that stack and uses it for both the editor overlay and the rasterized result. This provides consistent styling across platforms while still falling back to safe system fonts if a preferred font is missing.
+
+## Size and spacing
+
+- **Size** controls the font size in pixels.
+- **Spacing** controls the line height multiplier (Dense, Normal, Spacious). This affects the distance between lines while editing the text.
+
+## Uncommitted state
+
+While text is uncommitted, it remains editable on-canvas. Access to other tools is blocked. Switching to other tabs (Generate, Upascaling, Workflows etc.) discards the text. The uncommitted box can be moved and rotated:
+
+- **Move:** Hold Ctrl (Windows/Linux) or Command (macOS) and drag to move the text box.
+- **Rotate:** Drag the rotation handle above the box. Hold **Shift** while rotating to snap to 15 degree increments.
+
+The text is committed to a raster layer when you press **Enter**. Press **Esc** to discard the current text session.
+
+## For Developers
+
+
diff --git a/docs/src/content/docs/features/External Models/alibabacloud.mdx b/docs/src/content/docs/features/External Models/alibabacloud.mdx
new file mode 100644
index 00000000000..0809b60442d
--- /dev/null
+++ b/docs/src/content/docs/features/External Models/alibabacloud.mdx
@@ -0,0 +1,54 @@
+---
+title: Alibaba Cloud DashScope
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+Invoke supports Alibaba Cloud's **DashScope** image generation service, giving access to the **Qwen Image** family and **Wan 2.6** text-to-image. Qwen Image is particularly strong at bilingual (Chinese / English) text rendering.
+
+## Getting an API Key
+
+
+1. Sign in to [Alibaba Cloud Model Studio](https://www.alibabacloud.com/en/product/modelstudio) (the international DashScope portal).
+2. Enable **DashScope** and activate the image generation models you plan to use.
+3. Create an API key from the **API Keys** section of the console.
+
+
+## Configuration
+
+Add your key to `api_keys.yaml` in your Invoke root directory:
+
+```yaml
+external_alibabacloud_api_key: "your-dashscope-api-key"
+
+# Optional — default is the international endpoint. Use the China endpoint if your account lives there:
+# https://dashscope.aliyuncs.com
+external_alibabacloud_base_url: "https://dashscope-intl.aliyuncs.com"
+```
+
+Restart Invoke for the change to take effect.
+
+:::note[International vs. China endpoints]
+DashScope has separate international (`dashscope-intl.aliyuncs.com`) and China (`dashscope.aliyuncs.com`) deployments. Your API key only works on the deployment it was issued on — if you get authentication errors, check that `external_alibabacloud_base_url` matches.
+:::
+
+## Available Models
+
+| Model | Modes | Aspect Ratios | Batch | Notes |
+| --- | --- | --- | --- | --- |
+| **Qwen Image 2.0 Pro** | txt2img | 1:1, 4:3, 3:4, 16:9, 9:16 | up to 4 | Best quality, 2K output, excellent bilingual text. |
+| **Qwen Image 2.0** | txt2img | 1:1, 4:3, 3:4, 16:9, 9:16 | up to 4 | Faster / cheaper 2K sibling of 2.0 Pro. |
+| **Qwen Image Max** | txt2img | 1:1, 4:3, 3:4, 16:9, 9:16 | up to 4 | High quality at ~1.3K native size. |
+| **Qwen Image Edit Max** | txt2img (with reference images) | 1:1, 4:3, 3:4, 16:9, 9:16 | up to 4 | Reference-image-driven generation with industrial / geometric reasoning. Accepts up to 14 reference images. |
+| **Wan 2.6 Text-to-Image** | txt2img | 1:1, 4:3, 3:4, 16:9, 9:16 | up to 4 | Photorealistic T2I at 1K. |
+
+All models support **seed**. Negative prompts are not currently plumbed through to DashScope, so the negative prompt input is ignored for these providers. None of the Alibaba Cloud models support img2img (denoising-strength edits) or inpaint (mask-based edits) in Invoke today.
+
+## Tips
+
+
+1. Bilingual prompts. Qwen Image is unusually good at rendering Chinese text and mixed-language prompts — it's a strong choice when your prompt or desired output contains non-Latin script.
+2. Reference-image input is only accepted by Qwen Image Edit Max — provide images via the reference-images panel. Masks and denoising strength are not supported for any Alibaba Cloud model.
+3. Batching is capped at 4 images per request. Larger batches are split across multiple API calls.
+4. Costs vary per model — Qwen Image 2.0 Pro is the most expensive, Qwen Image 2.0 the cheapest of the 2.0 family. Check Alibaba Cloud's pricing page before running large batches.
+
diff --git a/docs/src/content/docs/features/External Models/gemini.mdx b/docs/src/content/docs/features/External Models/gemini.mdx
new file mode 100644
index 00000000000..53067376488
--- /dev/null
+++ b/docs/src/content/docs/features/External Models/gemini.mdx
@@ -0,0 +1,48 @@
+---
+title: Google Gemini
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+Invoke supports Google's Gemini image generation models through the Gemini API. This provider is a good fit if you want high-quality text-to-image and reference-based image edits without running a local model.
+
+## Getting an API Key
+
+
+1. Open [Google AI Studio](https://aistudio.google.com/) and sign in with your Google account.
+2. Generate a new API key.
+3. Note the key — it will only be shown once.
+
+
+## Configuration
+
+Add your key to `api_keys.yaml` in your Invoke root directory:
+
+```yaml
+external_gemini_api_key: "your-gemini-api-key"
+
+# Optional — only set this if you need to route requests through a different endpoint
+external_gemini_base_url: "https://generativelanguage.googleapis.com"
+```
+
+Restart Invoke for the change to take effect.
+
+## Available Models
+
+| Model | Modes | Reference Images | Notes |
+| --- | --- | --- | --- |
+| **Gemini 2.5 Flash Image** | txt2img | Yes | 10 aspect ratios, fixed per-ratio resolutions. |
+| **Gemini 3 Pro Image Preview** | txt2img | Up to 14 (6 object + 5 character) | 1K / 2K / 4K resolution presets. |
+| **Gemini 3.1 Flash Image Preview** | txt2img | Up to 14 (10 object + 4 character) | 512 / 1K / 2K / 4K resolution presets. |
+
+Reference-image input is used to condition generation but counts as txt2img — neither img2img (denoising strength) nor inpaint (mask) is supported for Gemini.
+
+All Gemini models are single-image-per-request — batch size is fixed at 1. To generate multiple variations, queue multiple invocations.
+
+## Tips
+
+
+1. Reference images are sent directly to the API as inlined PNG data. Large references increase request latency and cost — crop tightly where possible.
+2. Aspect ratios are mapped to the closest Gemini-supported ratio. For Gemini 3 models, use the resolution presets to stay at the provider's native output sizes and avoid unnecessary rescaling.
+3. Pricing varies by model and region. Check Google's documentation before running large batches.
+
diff --git a/docs/src/content/docs/features/External Models/index.mdx b/docs/src/content/docs/features/External Models/index.mdx
new file mode 100644
index 00000000000..358b68fe68e
--- /dev/null
+++ b/docs/src/content/docs/features/External Models/index.mdx
@@ -0,0 +1,58 @@
+---
+title: External Models
+---
+
+External models let you generate images in Invoke by calling third-party image generation APIs instead of running a model locally. This is useful when:
+
+- You don't have the GPU or VRAM to run a model locally.
+- You want access to closed-source models (e.g. GPT Image, Gemini).
+- You need a specific provider capability (very high resolutions, fast batches, bilingual text rendering, etc.).
+
+External models appear in the model picker alongside locally installed models. Generations are routed to the provider's API, billed against your provider account, and the resulting images are imported back into Invoke like any other generation.
+
+## Supported Providers
+
+- [Google Gemini](/features/external-models/gemini/) — Gemini 2.5 Flash Image, Gemini 3 Pro Image Preview, Gemini 3.1 Flash Image Preview
+- [OpenAI](/features/external-models/openai/) — GPT Image 1 / 1.5 / 1-mini, DALL·E 3
+- [BytePlus Seedream](/features/external-models/seedream/) — Seedream 5.0, 5.0 Lite, 4.5, 4.0
+- [Alibaba Cloud DashScope](/features/external-models/alibabacloud/) — Qwen Image 2.0 / 2.0 Pro / Max / Edit Max, Wan 2.6 T2I
+
+## Configuring API Keys
+
+External provider credentials are stored in a dedicated `api_keys.yaml` file alongside `invokeai.yaml` in your Invoke root directory.
+
+```yaml
+# api_keys.yaml
+external_gemini_api_key: "your-gemini-api-key"
+external_openai_api_key: "your-openai-api-key"
+
+# Optional: override the provider base URL (e.g. for a compatible proxy or regional endpoint)
+external_gemini_base_url: "https://generativelanguage.googleapis.com"
+external_openai_base_url: "https://api.openai.com"
+```
+
+Restart Invoke after editing `api_keys.yaml` so the new values are picked up.
+
+!!! warning "Keep your keys private"
+ `api_keys.yaml` contains secrets. Do not commit it to version control and do not share it with other users of your machine.
+
+## Installing External Models
+
+External models are listed in the starter models dialog under their provider. Install them like any other starter model — Invoke records a model reference but does not download weights (there are no weights to download).
+
+Once installed, external models show up everywhere a model can be selected. Choose one, set the usual parameters (prompt, dimensions, num images, etc.), and invoke as normal.
+
+## Capabilities and Settings Visibility
+
+Each external model declares its own **capabilities** — for example:
+
+- Which generation modes it supports (`txt2img`, `img2img`). Inpainting is not currently supported by any external provider.
+- Whether it accepts reference images, and how many.
+- Which aspect ratios and resolutions it allows.
+- Whether it supports a negative prompt, seed, or batch size > 1.
+
+Invoke uses these capabilities to drive the UI: only the settings a given model actually supports will be shown in the parameters panel. If a field you expect is missing, it's because the selected model does not support it.
+
+## Costs and Rate Limits
+
+External providers charge for each request. Check the provider's pricing page before running large batches. Rate-limit errors from the provider are surfaced in Invoke as generation failures — wait a moment and try again, or lower your concurrent batch size.
diff --git a/docs/src/content/docs/features/External Models/openai.mdx b/docs/src/content/docs/features/External Models/openai.mdx
new file mode 100644
index 00000000000..2ee4628ebd3
--- /dev/null
+++ b/docs/src/content/docs/features/External Models/openai.mdx
@@ -0,0 +1,65 @@
+---
+title: OpenAI
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+Invoke supports OpenAI's image generation models — the GPT Image family and DALL·E 3 — through the OpenAI API.
+
+:::note[DALL·E 2 removed]
+DALL·E 2 was deprecated by OpenAI and is scheduled for shutdown on 2026-05-12. It is no longer offered as a starter model in Invoke.
+:::
+
+## Getting an API Key
+
+
+1. Open the [OpenAI API Platform](https://platform.openai.com/api-keys) and sign in.
+2. Create a new secret API key.
+3. Make sure your account has billing set up — image endpoints are paid per request.
+
+
+## Configuration
+
+Add your key to `api_keys.yaml` in your Invoke root directory:
+
+```yaml
+external_openai_api_key: "sk-..."
+
+# Optional — use this to point at a compatible proxy or Azure OpenAI deployment
+external_openai_base_url: "https://api.openai.com"
+```
+
+Restart Invoke for the change to take effect.
+
+## Available Models
+
+| Model | Modes | Aspect Ratios | Batch | Notes |
+| --- | --- | --- | --- | --- |
+| **GPT Image 1.5** | txt2img, img2img | 1:1, 3:2, 2:3 | up to 10 | Fastest and cheapest GPT Image model. |
+| **GPT Image 1** | txt2img, img2img | 1:1, 3:2, 2:3 | up to 10 | Highest quality of the GPT Image family. |
+| **GPT Image 1 Mini** | txt2img, img2img | 1:1, 3:2, 2:3 | up to 10 | ~80% cheaper than GPT Image 1. |
+| **DALL·E 3** | txt2img only | 1:1, 7:4, 4:7 | 1 | No reference-image / edit support. |
+
+Inpainting (mask-based editing) is not currently supported for any OpenAI model in Invoke. img2img on the GPT Image family routes through the `/v1/images/edits` endpoint without a mask.
+
+## Provider-Specific Options
+
+For **GPT Image** models, Invoke surfaces two provider-specific options in the parameters panel:
+
+- **Quality** — `low`, `medium`, `high`, or `auto`. Higher quality costs more and takes longer.
+- **Background** — `auto`, `transparent`, or `opaque`. Use `transparent` for PNG output with an alpha channel.
+
+DALL·E 2 and DALL·E 3 do not expose these options.
+
+## How Requests Are Routed
+
+- Pure text-to-image requests hit `/v1/images/generations`.
+- Any request with an init image or reference images is sent to `/v1/images/edits` instead. This is done transparently — you don't need to pick an endpoint.
+
+## Tips
+
+
+1. Batching on GPT Image tops out at 10 per request. Larger batches are split into multiple API calls.
+2. Costs can climb quickly with high-quality GPT Image generations. Start with GPT Image 1 Mini when iterating on prompts.
+3. Rate limits from OpenAI surface as failed invocations — retry after a short wait.
+
diff --git a/docs/src/content/docs/features/External Models/seedream.mdx b/docs/src/content/docs/features/External Models/seedream.mdx
new file mode 100644
index 00000000000..de25273d289
--- /dev/null
+++ b/docs/src/content/docs/features/External Models/seedream.mdx
@@ -0,0 +1,68 @@
+---
+title: BytePlus Seedream
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+Invoke supports BytePlus's **Seedream** image generation family through the BytePlus Ark API. Seedream is a strong fit for 2K/4K generations and multi-reference image composition.
+
+## Getting an API Key
+
+
+1. Open the [BytePlus Console](https://console.byteplus.com/) and sign in.
+2. Enable the **Ark** (model serving) product.
+3. Create an API key with access to the Seedream models you plan to use.
+
+
+## Configuration
+
+Add your key to `api_keys.yaml` in your Invoke root directory:
+
+```yaml
+external_seedream_api_key: "your-seedream-api-key"
+
+# Optional — change only if you need a different regional endpoint
+external_seedream_base_url: "https://ark.ap-southeast.bytepluses.com"
+```
+
+Restart Invoke for the change to take effect.
+
+## Available Models
+
+| Model | Modes | Reference Images | Batch | Native Size |
+| --- | --- | --- | --- | --- |
+| **Seedream 5.0** | txt2img, img2img | up to 14 | up to 15 | 2K |
+| **Seedream 5.0 Lite** | txt2img, img2img | up to 14 | up to 15 | 2K |
+| **Seedream 4.5** | txt2img, img2img | up to 14 | up to 15 | 2K |
+| **Seedream 4.0** | txt2img, img2img | up to 14 | up to 15 | 2K |
+
+The 4.x / 5.x models are batch-capable and accept up to 14 reference images per request.
+
+:::note[Model IDs]
+BytePlus uses date-stamped model IDs (e.g. `seedream-5-0-260128`). When BytePlus releases a new dated revision, the starter model IDs in Invoke need to be updated. Seedream 3.0 T2I (`seedream-3-0-t2i-250415`) was deprecated by BytePlus and replaced by Seedream 4.0.
+:::
+
+### Supported Aspect Ratios
+
+All Seedream models share the same aspect ratio set: `1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `9:16`, `16:9`, `21:9`, rendered at 2K.
+
+## Provider-Specific Options
+
+Seedream exposes two provider-specific toggles in the parameters panel:
+
+- **Watermark** — When enabled, BytePlus adds a small watermark to the output. Off by default.
+- **Optimize Prompt** — When enabled, BytePlus rewrites your prompt server-side for better generation quality. Useful for short prompts; disable if you want the exact wording preserved.
+
+**Seed** and **guidance scale** are not accepted by the 4.x / 5.x family.
+
+## Reference Images
+
+4.x and 5.x Seedream models accept up to 14 reference images alongside the prompt. Invoke's standard reference-image panel is used — drag images in, and they are forwarded as base64 PNGs to the API.
+
+## Tips
+
+
+1. For multi-image composition (e.g. character + product), Seedream 4.5 is a good default.
+2. When running large batches (`num_images > 1` on 4.x / 5.x), Invoke uses the `sequential_image_generation` API flag — each image is returned as it completes.
+3. Set `external_seedream_base_url` if you need to route through a region-specific Ark endpoint.
+
diff --git a/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx
new file mode 100644
index 00000000000..bcef946fb3c
--- /dev/null
+++ b/docs/src/content/docs/features/Multi-User Mode/admin-guide.mdx
@@ -0,0 +1,642 @@
+---
+title: Multi-User Administrator Guide
+description: How to set up and manage a multi-user InvokeAI installation.
+sidebar:
+ order: 4
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+## Overview
+
+This guide is for administrators managing a multi-user InvokeAI
+installation. It covers initial setup, user management, security best
+practices, and troubleshooting.
+
+## Prerequisites
+
+Before enabling multi-user support, ensure you have:
+
+- InvokeAI installed and running
+- Access to the server filesystem (for initial setup)
+- Understanding of your deployment environment
+- Backup of your existing data (recommended)
+
+## Initial Setup
+
+### Activating Multiuser Mode
+
+To put InvokeAI into multiuser mode, you will need to add the option `multiuser: true` to its configuration file. This file is located at `INVOKEAI_ROOT/invokeai.yaml`. With the InvokeAI backend halted, add the new configuration option to the end of the file with a text editor so that it looks like this:
+
+```yaml
+# Internal metadata - do not edit:
+schema_version: 4.0.2
+
+# Enable/disable multi-user mode
+multiuser: true
+```
+
+Then restart the InvokeAI server backend from the command line or using the launcher.
+
+:::note[Reverting to single-user mode]
+If at any time you wish to revert to single-user mode, simply comment out the `multiuser` line, or change "true" to "false". Then restart the server. Because of the way that browsers cache pages, users with open InvokeAI sessions may need to force-refresh their browsers.
+:::
+
+### First Administrator Account
+
+When InvokeAI starts for the first time in multi-user mode, you'll see the **Administrator Setup** dialog.
+
+**Setup Steps:**
+
+
+1. **Email Address**: Enter a valid email address (this becomes your username)
+
+ - Example: `admin@example.com` or `admin@localhost` for testing
+ - Must be a valid email format
+ - Cannot be changed later without database access
+
+2. **Display Name**: Enter a friendly name
+
+ - Example: "System Administrator" or your real name
+ - Can be changed later in your profile
+ - Visible to other users in shared contexts
+
+3. **Password**: Create a strong administrator password
+
+ - **Minimum requirements:**
+
+ - At least 8 characters long
+ - Contains uppercase letters (A-Z)
+ - Contains lowercase letters (a-z)
+ - Contains numbers (0-9)
+
+ - **Recommended:**
+
+ - Use 12+ characters
+ - Include special characters (!@#$%^&*)
+ - Use a password manager to generate and store
+ - Don't reuse passwords from other services
+
+4. **Confirm Password**: Re-enter the password
+
+5. Click **Create Administrator Account**
+
+
+:::caution[Important]
+Store these credentials securely! The first administrator account can reset the password to something new, but cannot retrieve a lost one.
+:::
+
+### Configuration
+
+InvokeAI can run in single-user or multi-user mode, controlled by the `multiuser` configuration option in `invokeai.yaml`:
+
+```yaml
+# Enable/disable multi-user mode
+multiuser: true # Enable multi-user mode (requires authentication)
+
+# Optional password policy
+strict_password_checking: true # Enforce uppercase/lowercase/number requirements
+```
+
+JWT secrets are generated automatically and stored in the database. Session lifetimes default to 24 hours, or 7 days when the user selects "Remember me". See Secret Key Management below if you need to rotate the JWT secret.
+
+:::caution[Mode Switching Behavior]
+**Switching to Single-User Mode:** If boards or images were created in multi-user mode, they will all be combined into a single unified view when switching to single-user mode.
+
+**Switching to Multi-User Mode:** Legacy boards and images created under single-user mode will be owned by an internal user named "system." Only the Administrator will have access to these legacy assets. A utility to migrate these legacy assets to another user will be part of a future release.
+:::
+
+### Migration from Single-User
+
+When upgrading from a single-user installation or switching modes:
+
+
+1. **Automatic Migration**: The database will automatically migrate to multi-user schema when multi-user mode is first enabled
+2. **Legacy Data Ownership**: Existing data (boards, images, workflows) created in single-user mode is assigned to an internal user named "system"
+3. **Administrator Access**: Only administrators will have access to legacy "system"-owned assets when in multi-user mode
+4. **No Data Loss**: All existing content is preserved
+
+
+**Migration Process:**
+
+```bash
+# Backup your database first
+cp databases/invokeai.db databases/invokeai.db.backup
+
+# Enable multi-user mode in invokeai.yaml
+# multiuser: true
+
+# Start InvokeAI (migration happens automatically)
+invokeai-web
+
+# Complete the administrator setup dialog
+# Legacy data will be owned by "system" user
+```
+
+:::note[Legacy Asset Migration]
+A utility to migrate legacy "system"-owned assets to specific user accounts will be available in a future release. Until then, administrators can access and manage all legacy content.
+:::
+
+## User Management
+
+### Creating Users
+
+Administrators can create and modify users (including other
+administrators) via a built-in web interface or using command-line
+scripts.
+
+#### **Via the Web Frontend:**
+
+Please see the Multi-User Guide's section on [Adding and Modifying Users](./user-guide#adding-and-modifying-users)
+for a walk-through.
+
+#### **Via Command Line Scripts:**
+
+##### Command-line User Management Scripts
+
+Administrators can also use a series of command-line scripts to add, modify, or delete users. If you use the launcher, click the ">" icon to enter the command-line interface. Otherwise, if you are a native command-line user, activate the InvokeAI environment from your terminal.
+
+All command-line arguments are optional. The scripts will prompt you to provide any missing arguments.
+
+The commands are:
+
+| Name | Function | Example CLI Usage |
+|--------------------|---------------|--------------------|
+|**invoke-useradd** | add a user | `invoke-useradd --email user@example.com --name "Example User" --password "badpassword"` |
+|**invoke-usermod** | modify a user | `invoke-usermod --email user@example.com --name "Mr. Example User" --password "8adsf2**%"` |
+|**invoke-userdel** | delete a user | `invoke-userdel --email user@example.com --force` |
+|**invoke-userlist** | list all users| `invoke-userlist` |
+
+Pass the `--help` argument to get the usage of each script. For example:
+
+```bash
+> invoke-useradd --help
+usage: invoke-useradd [-h] [--root ROOT] [--email EMAIL] [--password PASSWORD] [--name NAME] [--admin]
+
+Add a user to the InvokeAI database
+
+options:
+ -h, --help show this help message and exit
+ --root ROOT, -r ROOT Path to the InvokeAI root directory. If omitted, the root is resolved in this order: the $INVOKEAI_ROOT environment
+ variable, the active virtual environment's parent directory, or $HOME/invokeai.
+ --email EMAIL, -e EMAIL
+ User email address
+ --password PASSWORD, -p PASSWORD
+ User password
+ --name NAME, -n NAME User display name (optional)
+ --admin, -a Make user an administrator
+
+If no arguments are provided, the script will run in interactive mode.
+```
+
+:::danger[Data Loss]
+Deleting a user removes the user record and cascades to their sessions, board shares, sent
+invitations, and per-user client state. It does **not** delete the boards, images, workflows, queue
+items, or style presets they created — those rows remain in the database, owned by a user_id that no
+longer exists, and will not appear in any user's gallery. Physical image files in `outputs/images`
+are also left in place until a gallery maintenance script is run to remove orphan images.
+
+If you want their content gone as well, reassign or delete it before deleting the user. Back up the
+database first if recovery might be needed.
+:::
+
+### Viewing User Activity
+
+**Queue Management:**
+
+There is no separate admin-only queue view. When signed in as an administrator, the regular queue
+panel automatically shows every user's queue items (each item is labelled with the submitting user's
+display name or email), and you can cancel or clear any of them. There is no built-in UI to filter
+the queue by user; use your browser's find-in-page to scan by name if needed.
+
+## Model Management
+
+As an administrator, you have full access to the [Model
+Manager](/concepts/models) and can install, edit and delete
+models just as in single-user mode. Unprivileged users, however, can
+view the models previously installed, but cannot add or modify them.
+
+## Security
+
+:::note[Strict Password Checking]
+It is recommended that you enable strict password checking. This will
+force all users to select good passwords that follow the
+"minimal requirements" below. Do this by adding `strict_password_checking` to
+the `invokeai.yaml` configuration file:
+
+```
+strict_password_checking: true
+```
+:::
+
+### Password Policies
+
+**Minimal Requirements:**
+
+- Minimum 8 characters
+- Must contain uppercase letters
+- Must contain lowercase letters
+- Must contain numbers
+
+If `strict_password_checking` is active (recommended), then these
+minimal requirements will be enforced and users will not be able to
+proceed until they have picked a password that satisfies
+them. Otherwise, the user will simply be warned when they use a weak
+password.
+
+**Recommended Policies:**
+
+- Require 12+ character passwords
+- Include special characters
+- Implement password rotation every 90 days
+- Prevent password reuse
+
+### Session Management
+
+**Session Security and Token Management:**
+
+This system uses stateless JWT tokens with HMAC signatures to identify users after they provide their initial credentials. The tokens will persist for 24 hours by default, or for 7 days if the user clicks the "Remember me" checkbox at login. Expired tokens are automatically rejected and the user will have to log in again.
+
+At the client side, tokens are stored in browser localStorage. Logging out clears them. No server-side session storage is required.
+
+The tokens include the user's ID, email, and admin status, along with an HMAC signature.
+
+### Secret Key Management
+
+**Important:** The JWT secret key must be kept confidential.
+
+To generate tokens, each InvokeAI instance has a distinct secret JWT
+key that must be kept confidential. The key is stored in the
+`app_settings` table of the InvokeAI database within a field value
+named `jwt_secret`.
+
+The secret key is automatically generated during database creation or
+migration. If you wish to change the key, you may generate a
+replacement using either of these commands:
+
+```bash
+# Python
+python -c "import secrets; print(secrets.token_urlsafe(32))"
+
+# OpenSSL
+openssl rand -base64 32
+```
+
+Then cut and paste the printed secret into this Sqlite3 command:
+
+```bash
+sqlite3 INVOKE_ROOT/databases/invokeai.db 'update app_settings set value="THE_SECRET" where key="jwt_secret"'
+```
+
+(replace INVOKE_ROOT with your InvokeAI root directory and THE_SECRET with the new secret).
+
+After this, restart the server. All logged in users will be logged out and will need to provide their usernames and passwords again.
+
+### Hosting a Shared InvokeAI Instance
+
+The multiuser feature allows you to run an InvokeAI backend that can be accessed by your friends and family across your home network. It is also possible to host a backend that is accessible over the Internet.
+
+By default, InvokeAI runs on `localhost`, IP address `127.0.0.1`, which is only accessible to browsers running on the same machine as the backend. To make the backend accessible to any machine on your home or work LAN, add the line `host: 0.0.0.0` to the InvokeAI configuration file, usually stored at `INVOKE_ROOT/invokeai.yaml`.
+
+Here is a minimal example.
+
+```yaml
+# Internal metadata - do not edit:
+schema_version: 4.0.2
+
+# Put user settings here - see https://invoke-ai.github.io/InvokeAI/configuration/:
+multiuser: true
+host: 0.0.0.0
+```
+
+After relaunching the backend you will be able to reach the server from other machines on the LAN using the server machine's IP address or hostname and port 9090.
+
+#### Making InvokeAI Accessible to the Internet
+
+:::danger[Use at your own risk]
+The InvokeAI team has done its best to make the software free of exploitable bugs, but the software has not undergone a rigorous security audit or intrusion testing. Use at your own risk.
+:::
+
+It is also possible to create a (semi) public server accessible from the Internet. The details of how to do this depend very much on your home or corporate router/firewall system and are beyond the scope of this document.
+
+If you expose InvokeAI to the Internet, there are a number of precautions to take. Here is a brief list of recommended network security practices.
+
+**HTTPS Configuration:**
+
+For internet deployments, always use HTTPS:
+
+```nginx
+# Use a reverse proxy like nginx or Traefik
+# Example nginx configuration:
+
+server {
+ listen 443 ssl http2;
+ server_name invoke.example.com;
+
+ ssl_certificate /path/to/cert.pem;
+ ssl_certificate_key /path/to/key.pem;
+
+ location / {
+ proxy_pass http://localhost:9090;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket support
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+}
+```
+
+**Firewall Rules:**
+
+It is best to restrict access to trusted networks and remote IP addresses, or use a VPN to connect to your home network. Rate limit connections to InvokeAI's authentication endpoint `http://your.host:9090/api/v1/auth/login`.
+
+**Backup and Recovery:**
+
+It is always a good idea to periodically backup your InvokeAI database and images, but especially
+so if the server is publicly accessible to the Internet.
+
+**Manual Backup:**
+
+```bash
+# Stop InvokeAI
+# Copy database file
+cd INVOKE_ROOT
+cp databases/invokeai.db databases/invokeai.db.$(date +%Y%m%d)
+
+# Or create compressed backup
+tar -czf invokeai_backup_$(date +%Y%m%d).tar.gz databases/
+```
+
+**Automated Backup Script:**
+
+```bash
+#!/bin/bash
+# backup_invokeai.sh
+
+INVOKE_ROOT="/path/to/invoke_root"
+BACKUP_DIR="/path/to/backups"
+DB_PATH="$INVOKE_ROOT/databases/invokeai.db"
+DATE=$(date +%Y%m%d_%H%M%S)
+
+# Create backup directory
+mkdir -p "$BACKUP_DIR"
+
+# Copy database
+cp "$DB_PATH" "$BACKUP_DIR/invokeai_$DATE.db"
+
+# Keep only last 30 days
+find "$BACKUP_DIR" -name "invokeai_*.db" -mtime +30 -delete
+
+echo "Backup completed: invokeai_$DATE.db"
+```
+
+**Schedule with cron:**
+
+```bash
+# Edit crontab
+crontab -e
+
+# Add daily backup at 2 AM
+0 2 * * * /path/to/backup_invokeai.sh
+```
+
+**Restore from Backup:**
+
+```bash
+# Stop InvokeAI
+# Replace current database with backup
+cd INVOKE_ROOT
+cp databases/invokeai.db databases/invokeai.db.old # Save current
+cp databases/invokeai_backup.db databases/invokeai.db
+
+# Restart InvokeAI
+invokeai-web
+```
+
+**Disaster Recovery — Complete System Backup:**
+
+Include these directories/files:
+
+- `databases/` — All database files
+- `models/` — Installed models (if locally stored)
+- `outputs/` — Generated images
+- `invokeai.yaml` — Configuration file
+- Any custom scripts or modifications
+
+**Recovery Process:**
+
+
+1. Install InvokeAI on new system
+2. Restore configuration file
+3. Restore database directory
+4. Restore models and outputs
+5. Verify file permissions
+6. Start InvokeAI and test
+
+
+## Troubleshooting
+
+### User Cannot Login
+
+**Symptom:** User reports unable to log in
+
+**Diagnosis:**
+
+1. Verify account exists and is active
+
+ ```bash
+ sqlite3 databases/invokeai.db "SELECT * FROM users WHERE email = 'user@example.com';"
+ ```
+
+2. Check password (have user try resetting)
+3. Verify account is active (`is_active = 1`)
+4. Check for account lockout (if implemented)
+
+**Solutions:**
+
+- Reset user password
+- Reactivate disabled account
+- Verify email address is correct
+- Check system logs for auth errors
+
+### Database Locked Errors
+
+**Symptom:** "Database is locked" errors
+
+**Causes:**
+
+- Concurrent write operations
+- Long-running transactions
+- Backup process accessing database
+- File system issues
+
+**Solutions:**
+
+```bash
+# Check for locks
+fuser databases/invokeai.db
+
+# Increase timeout (in config)
+# Or switch to WAL mode:
+sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;"
+```
+
+### Forgotten Admin Password
+
+**Recovery Process:**
+
+
+1. Stop InvokeAI
+2. Direct database access:
+
+ ```bash
+ sqlite3 databases/invokeai.db
+ ```
+
+3. Reset admin password (requires password hash):
+
+ ```sql
+ -- Generate hash first using Python:
+ -- from passlib.context import CryptContext
+ -- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+ -- print(pwd_context.hash("NewPassword123"))
+
+ UPDATE users
+ SET password_hash = '$2b$12$...'
+ WHERE email = 'admin@example.com';
+ ```
+
+4. Restart InvokeAI
+
+
+:::note[Alternative Step 3]
+Remove the admin from the database entirely in order
+to trigger the setup process when InvokeAI restarts:
+
+```sql
+DELETE FROM users
+WHERE email = 'admin@example.com';
+```
+:::
+
+
+### Performance Issues
+
+**Symptom:** Slow generation or UI
+
+**Diagnosis:**
+
+
+1. Check active generation count
+2. Review resource usage (CPU/GPU/RAM)
+3. Check database size and performance
+4. Review network latency
+
+
+**Solutions:**
+
+- Limit concurrent generations
+- Increase hardware resources
+- Optimize database (`VACUUM`, `ANALYZE`)
+- Add indexes for slow queries
+- Consider load balancing
+
+### Migration Failures
+
+**Symptom:** Database migration fails on upgrade
+
+**Prevention:**
+
+- Always backup before upgrading
+- Test migration on copy of database
+- Review migration logs
+
+**Recovery:**
+
+```bash
+# Restore backup
+cp databases/invokeai.db.backup databases/invokeai.db
+
+# Try migration again with verbose logging
+invokeai-web --log-level DEBUG
+```
+
+## Configuration Reference
+
+### Complete Configuration Example for a Public Site
+
+```yaml
+# invokeai.yaml - Multi-user configuration
+
+# Internal metadata - do not edit:
+schema_version: 4.0.2
+
+# Put user settings here
+multiuser: true
+
+# Server
+host: "0.0.0.0"
+port: 9090
+
+# Performance
+enable_partial_loading: true
+precision: float16
+pytorch_cuda_alloc_conf: "backend:cudaMallocAsync"
+hashing_algorithm: blake3_multi
+```
+
+## Frequently Asked Questions
+
+### How many users can InvokeAI support?
+
+The backend will support dozens of concurrent users. However, because the image generation queue is single-threaded, image generation tasks are processed on a first-come, first-serve basis. This means that a user may have to wait for all the other users' image generation jobs to complete before their generation job starts to execute.
+
+A future version of InvokeAI may support concurrent execution on systems with multiple GPUs/graphics cards.
+
+### Can I integrate with existing authentication systems?
+
+OAuth2/OpenID Connect support is planned for a future release. Currently, InvokeAI uses its own authentication system.
+
+### How do I audit user actions?
+
+Full audit logging is planned for a future release. Currently, you can:
+
+- Monitor the generation queue
+- Review database changes
+- Check application logs
+
+### Can users have different model access?
+
+Currently all users can view and use all installed models. Per-user
+model access is a possible enhancement. Please let the development
+team know if you want this feature.
+
+### How do I handle user data when they leave?
+
+Best practice:
+
+
+1. Deactivate the account first
+2. Transfer ownership of shared boards
+3. After transition period, delete the account
+4. Or keep the account deactivated for audit purposes
+
+
+### What's the licensing impact of multi-user mode?
+
+InvokeAI remains under its existing license. Multi-user mode does not change licensing terms.
+
+## Getting Help
+
+### Support
+
+- **General Documentation**: [InvokeAI Docs](https://invoke.ai/)
+- **User Guide**: [For Users](/features/multi-user-mode/user-guide/)
+- **API Guide**: [For Developers](/features/multi-user-mode/api-guide/)
+- **Discord**: [Join Community](https://discord.gg/ZmtBAhwWhy)
+- **GitHub Issues**: [Report Problems](https://github.com/invoke-ai/InvokeAI/issues)
diff --git a/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx
new file mode 100644
index 00000000000..fa7064b4031
--- /dev/null
+++ b/docs/src/content/docs/features/Multi-User Mode/api-guide.mdx
@@ -0,0 +1,1230 @@
+---
+title: Multi-User API Guide
+description: How to authenticate and interact with the InvokeAI API in multi-user mode.
+sidebar:
+ order: 5
+---
+import { Steps } from '@astrojs/starlight/components'
+
+## Overview
+
+This guide explains how to interact with InvokeAI's API in both single-user and multi-user modes. The API behavior depends on the `multiuser` configuration setting.
+
+### Single-User vs Multi-User Mode
+
+**Single-User Mode** (`multiuser: false` or option absent):
+
+- No authentication required
+- All API endpoints accessible without tokens
+- Direct API access like previous InvokeAI versions
+- All content visible in unified view
+
+**Multi-User Mode** (`multiuser: true`):
+
+- JWT token authentication required
+- User-scoped access to resources
+- Role-based authorization (admin vs regular user)
+- Data isolation between users
+
+## Authentication (Multi-User Mode Only)
+
+### Authentication Flow
+
+When multi-user mode is enabled, most API endpoints require authentication using JWT bearer tokens. The unauthenticated authentication endpoints are `GET /api/v1/auth/status`, `POST /api/v1/auth/setup`, and `POST /api/v1/auth/login`.
+
+**Authentication Process:**
+
+
+1. **Obtain Token**: POST credentials to `/api/v1/auth/login`
+2. **Store Token**: Save the JWT token securely
+3. **Use Token**: Include token in `Authorization` header for all requests
+4. **Refresh**: Re-authenticate when token expires
+
+
+:::note[Single-User Mode]
+When running in single-user mode (`multiuser: false`), authentication endpoints are not available and authentication headers are not required.
+:::
+
+### Login Endpoint
+
+**Endpoint:** `POST /api/v1/auth/login`
+
+**Request:**
+
+```json
+{
+ "email": "user@example.com",
+ "password": "SecurePassword123",
+ "remember_me": false
+}
+```
+
+**Response (Success):**
+
+```json
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "user": {
+ "user_id": "abc123",
+ "email": "user@example.com",
+ "display_name": "John Doe",
+ "is_admin": false,
+ "is_active": true,
+ "created_at": "2024-01-15T10:00:00Z",
+ "updated_at": "2024-01-15T10:00:00Z",
+ "last_login_at": "2024-01-15T15:30:00Z"
+ },
+ "expires_in": 86400
+}
+```
+
+**Response (Error):**
+
+```json
+{
+ "detail": "Incorrect email or password"
+}
+```
+
+**Status Codes:**
+
+- `200 OK` — Authentication successful
+- `401 Unauthorized` — Invalid credentials
+- `403 Forbidden` — Account disabled
+- `422 Unprocessable Entity` — Invalid request format
+
+### Using the Token
+
+Include the JWT token in the `Authorization` header with the `Bearer` scheme:
+
+**HTTP Header:**
+
+```
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+```
+
+**Example HTTP Request:**
+
+```http
+GET /api/v1/boards HTTP/1.1
+Host: localhost:9090
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+Content-Type: application/json
+```
+
+### Token Expiration
+
+Tokens have a limited lifetime:
+
+- **Default**: 24 hours (86400 seconds)
+- **Remember Me**: 7 days (604800 seconds)
+
+**Handling Expiration:**
+
+```python
+import requests
+import time
+
+def api_request(url, token, max_retries=1):
+ headers = {"Authorization": f"Bearer {token}"}
+ response = requests.get(url, headers=headers)
+
+ if response.status_code == 401: # Token expired
+ # Re-authenticate and retry
+ new_token = login()
+ headers = {"Authorization": f"Bearer {new_token}"}
+ response = requests.get(url, headers=headers)
+
+ return response
+```
+
+### Logout Endpoint
+
+**Endpoint:** `POST /api/v1/auth/logout`
+
+**Request:**
+
+```http
+POST /api/v1/auth/logout HTTP/1.1
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+```
+
+**Response:**
+
+```json
+{
+ "success": true
+}
+```
+
+**Note:** With JWT tokens, logout is primarily client-side (delete token). Server-side session invalidation may be added in future releases.
+
+## Code Examples
+
+### Python
+
+**Using `requests` library:**
+
+```python
+import requests
+import json
+
+class InvokeAIClient:
+ def __init__(self, base_url="http://localhost:9090"):
+ self.base_url = base_url
+ self.token = None
+
+ def login(self, email, password, remember_me=False):
+ """Authenticate and store token."""
+ url = f"{self.base_url}/api/v1/auth/login"
+ payload = {
+ "email": email,
+ "password": password,
+ "remember_me": remember_me
+ }
+
+ response = requests.post(url, json=payload)
+ response.raise_for_status()
+
+ data = response.json()
+ self.token = data["token"]
+ return data["user"]
+
+ def _get_headers(self):
+ """Get headers with authentication token."""
+ if not self.token:
+ raise Exception("Not authenticated. Call login() first.")
+
+ return {
+ "Authorization": f"Bearer {self.token}",
+ "Content-Type": "application/json"
+ }
+
+ def get_boards(self):
+ """Get user's boards."""
+ url = f"{self.base_url}/api/v1/boards/"
+ response = requests.get(url, headers=self._get_headers())
+ response.raise_for_status()
+ return response.json()
+
+ def create_board(self, board_name):
+ """Create a new board."""
+ url = f"{self.base_url}/api/v1/boards/"
+ response = requests.post(
+ url,
+ params={"board_name": board_name},
+ headers=self._get_headers()
+ )
+ response.raise_for_status()
+ return response.json()
+
+ def logout(self):
+ """Logout and clear token."""
+ url = f"{self.base_url}/api/v1/auth/logout"
+ response = requests.post(url, headers=self._get_headers())
+ self.token = None
+ return response.json()
+
+# Usage
+client = InvokeAIClient()
+user = client.login("user@example.com", "SecurePassword123")
+print(f"Logged in as: {user['display_name']}")
+
+boards = client.get_boards()
+print(f"User has {len(boards['items'])} boards")
+
+new_board = client.create_board("My New Board")
+print(f"Created board: {new_board['board_name']}")
+
+client.logout()
+```
+
+**Error Handling:**
+
+```python
+import requests
+from requests.exceptions import HTTPError
+
+def safe_api_call(client, method, *args, **kwargs):
+ """Make API call with error handling."""
+ try:
+ func = getattr(client, method)
+ return func(*args, **kwargs)
+
+ except HTTPError as e:
+ if e.response.status_code == 401:
+ print("Authentication failed or token expired")
+ # Re-authenticate
+ client.login(email, password)
+ # Retry
+ return func(*args, **kwargs)
+ elif e.response.status_code == 403:
+ print("Permission denied")
+ elif e.response.status_code == 404:
+ print("Resource not found")
+ else:
+ print(f"API error: {e.response.status_code}")
+ print(e.response.text)
+
+ raise
+
+# Usage
+try:
+ boards = safe_api_call(client, "get_boards")
+except Exception as e:
+ print(f"Failed to get boards: {e}")
+```
+
+### JavaScript/TypeScript
+
+**Using `fetch` API:**
+
+```javascript
+class InvokeAIClient {
+ constructor(baseUrl = 'http://localhost:9090') {
+ this.baseUrl = baseUrl;
+ this.token = null;
+ }
+
+ async login(email, password, rememberMe = false) {
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ email,
+ password,
+ remember_me: rememberMe,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Login failed: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ this.token = data.token;
+
+ // Store token in localStorage
+ localStorage.setItem('invokeai_token', data.token);
+
+ return data.user;
+ }
+
+ getHeaders() {
+ if (!this.token) {
+ throw new Error('Not authenticated. Call login() first.');
+ }
+
+ return {
+ 'Authorization': `Bearer ${this.token}`,
+ 'Content-Type': 'application/json',
+ };
+ }
+
+ async getBoards() {
+ const response = await fetch(`${this.baseUrl}/api/v1/boards/`, {
+ headers: this.getHeaders(),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to get boards: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ async createBoard(boardName) {
+ const url = new URL(`${this.baseUrl}/api/v1/boards/`);
+ url.searchParams.set('board_name', boardName);
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to create board: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ async logout() {
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/logout`, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ });
+
+ this.token = null;
+ localStorage.removeItem('invokeai_token');
+
+ return response.json();
+ }
+}
+
+// Usage
+(async () => {
+ const client = new InvokeAIClient();
+
+ try {
+ const user = await client.login('user@example.com', 'SecurePassword123');
+ console.log(`Logged in as: ${user.display_name}`);
+
+ const boards = await client.getBoards();
+ console.log(`User has ${boards.items.length} boards`);
+
+ const newBoard = await client.createBoard('My New Board');
+ console.log(`Created board: ${newBoard.board_name}`);
+
+ await client.logout();
+ } catch (error) {
+ console.error('Error:', error.message);
+ }
+})();
+```
+
+**TypeScript with Types:**
+
+```typescript
+interface LoginRequest {
+ email: string;
+ password: string;
+ remember_me?: boolean;
+}
+
+interface User {
+ user_id: string;
+ email: string;
+ display_name: string;
+ is_admin: boolean;
+ is_active: boolean;
+ created_at: string;
+}
+
+interface LoginResponse {
+ token: string;
+ user: User;
+ expires_in: number;
+}
+
+interface Board {
+ board_id: string;
+ board_name: string;
+ created_at: string;
+ updated_at: string;
+ deleted_at?: string;
+ cover_image_name?: string;
+}
+
+class InvokeAIClient {
+ private baseUrl: string;
+ private token: string | null = null;
+
+ constructor(baseUrl: string = 'http://localhost:9090') {
+ this.baseUrl = baseUrl;
+ }
+
+ async login(
+ email: string,
+ password: string,
+ rememberMe: boolean = false
+ ): Promise {
+ const response = await fetch(`${this.baseUrl}/api/v1/auth/login`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password, remember_me: rememberMe }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.detail || 'Login failed');
+ }
+
+ const data: LoginResponse = await response.json();
+ this.token = data.token;
+ return data.user;
+ }
+
+ private getHeaders(): HeadersInit {
+ if (!this.token) {
+ throw new Error('Not authenticated');
+ }
+ return {
+ 'Authorization': `Bearer ${this.token}`,
+ 'Content-Type': 'application/json',
+ };
+ }
+
+ async getBoards(): Promise<{ items: Board[] }> {
+ const response = await fetch(`${this.baseUrl}/api/v1/boards/`, {
+ headers: this.getHeaders(),
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to get boards');
+ }
+
+ return response.json();
+ }
+}
+```
+
+### cURL
+
+**Login:**
+
+```bash
+# Login and extract token
+TOKEN=$(curl -X POST http://localhost:9090/api/v1/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "email": "user@example.com",
+ "password": "SecurePassword123",
+ "remember_me": false
+ }' | jq -r '.token')
+
+echo "Token: $TOKEN"
+```
+
+**Get Boards:**
+
+```bash
+curl -X GET http://localhost:9090/api/v1/boards/ \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json"
+```
+
+**Create Board:**
+
+```bash
+curl -X POST "http://localhost:9090/api/v1/boards/?board_name=My%20API%20Board" \
+ -H "Authorization: Bearer $TOKEN"
+```
+
+## API Endpoint Changes
+
+### Authentication Required
+
+All endpoints now require authentication except:
+
+- `GET /api/v1/auth/status` — Check whether multi-user setup is required
+- `POST /api/v1/auth/setup` — Initial admin setup
+- `POST /api/v1/auth/login` — User login
+- `GET /api/v1/images/i/{image_name}/full` — Full-resolution image file
+- `GET /api/v1/images/i/{image_name}/thumbnail` — Image thumbnail
+- `GET /api/v1/workflows/i/{workflow_id}/thumbnail` — Workflow thumbnail
+
+The image and thumbnail endpoints are intentionally unauthenticated because browsers load these resources via ` ` tags, which cannot send `Authorization` headers. Security relies on the fact that image and workflow IDs are UUIDs and therefore unguessable.
+
+### User-Scoped Resources
+
+Resources are now filtered by the authenticated user:
+
+**Boards:**
+
+```python
+# Before (single-user)
+GET /api/v1/boards/?all=true # Returns all boards
+
+# After (multi-user)
+GET /api/v1/boards/?all=true # Returns boards the current user can access, including their own boards plus shared/public boards; admins can see all boards
+```
+
+**Images:**
+
+```python
+# Images are filtered by board ownership
+GET /api/v1/images/ # Only shows images on user's boards
+```
+
+**Workflows:**
+
+```python
+# Returns user's workflows + public workflows
+GET /api/v1/workflows/
+```
+
+**Queue:**
+
+```python
+# Regular users see their own queue items in full and may see redacted details for other users' items on queue-status endpoints
+GET /api/v1/queue/... # Queue data, sanitized for non-admin viewers
+
+# Administrators see all queue items in full
+GET /api/v1/queue/... # Full queue data
+```
+
+### Administrator Endpoints
+
+Some endpoints require administrator privileges:
+
+**User Management:**
+
+```python
+GET /api/v1/auth/users # List users (admin only)
+POST /api/v1/auth/users # Create user (admin only)
+GET /api/v1/auth/users/{id} # Get user (admin only)
+PATCH /api/v1/auth/users/{id} # Update user (admin only)
+DELETE /api/v1/auth/users/{id} # Delete user (admin only)
+```
+
+**Model Management (Write Operations):**
+
+```python
+POST /api/v2/models/install # Install model (admin only)
+DELETE /api/v2/models/i/{key} # Delete model (admin only)
+PATCH /api/v2/models/i/{key} # Update model (admin only)
+PUT /api/v2/models/convert/{key} # Convert model (admin only)
+```
+
+**Model Management (Read Operations):**
+
+```python
+GET /api/v2/models/ # List models (all users)
+GET /api/v2/models/i/{key} # Get model details (all users)
+```
+
+### Error Responses
+
+**401 Unauthorized:**
+
+```json
+{
+ "detail": "Invalid authentication credentials"
+}
+```
+
+Occurs when:
+
+- Token is missing
+- Token is invalid
+- Token is expired
+- Token signature is invalid
+
+**403 Forbidden:**
+
+```json
+{
+ "detail": "Admin privileges required"
+}
+```
+
+Occurs when:
+
+- User attempts admin-only operation
+- Account is disabled
+- Insufficient permissions
+
+**404 Not Found:**
+
+```json
+{
+ "detail": "Resource not found"
+}
+```
+
+Occurs when:
+
+- Resource doesn't exist
+- User doesn't have access to resource
+
+## Multiuser API Endpoints
+
+### Authentication Endpoints
+
+#### Check if initial administrator setup is required
+
+**Endpoint:** `GET /api/v1/auth/status`
+
+**Description:** Returns a SetupStatusResponse indicating whether setup is needed and multiuser mode status.
+
+**Request:** No parameters
+
+**Response (initial setup not yet complete):**
+```json
+{
+ "setup_required": true,
+ "multiuser_enabled": true,
+ "strict_password_checking": true,
+ "admin_email": "admin@example.com"
+}
+```
+
+**Response (setup already complete, or multiuser disabled):**
+```json
+{
+ "setup_required": false,
+ "multiuser_enabled": true,
+ "strict_password_checking": true,
+ "admin_email": null
+}
+```
+
+:::note
+`admin_email` is only populated while `setup_required` is `true` (to help locate the pre-seeded
+administrator account during initial setup). Once an admin has been created — and whenever
+multiuser mode is disabled — it is returned as `null` to avoid leaking administrator identity on
+public deployments.
+:::
+
+
+#### Setup Administrator
+
+**Endpoint:** `POST /api/v1/auth/setup`
+
+**Description:** Create initial administrator account (only works if no admin exists)
+
+**Request:**
+
+```json
+{
+ "email": "admin@example.com",
+ "display_name": "Administrator",
+ "password": "SecureAdminPass123"
+}
+```
+
+**Response:**
+
+```json
+{
+ "success": true,
+ "user": {
+ "user_id": "abc123",
+ "email": "admin@example.com",
+ "display_name": "Administrator",
+ "is_admin": true,
+ "is_active": true
+ }
+}
+```
+
+
+#### Get Current User
+
+**Endpoint:** `GET /api/v1/auth/me`
+
+**Description:** Get currently authenticated user's information
+
+**Request:**
+
+```http
+GET /api/v1/auth/me
+Authorization: Bearer
+```
+
+**Response:**
+
+```json
+{
+ "user_id": "abc123",
+ "email": "user@example.com",
+ "display_name": "John Doe",
+ "is_admin": false,
+ "is_active": true,
+ "created_at": "2024-01-15T10:00:00Z",
+ "updated_at": "2024-01-15T10:00:00Z",
+ "last_login_at": "2024-01-15T15:30:00Z"
+}
+```
+
+### User Management Endpoints (Admin Only)
+
+#### List Users
+
+**Endpoint:** `GET /api/v1/auth/users`
+
+**Request:**
+
+```http
+GET /api/v1/auth/users
+Authorization: Bearer
+```
+
+**Response:**
+
+```json
+[
+ {
+ "user_id": "abc123",
+ "email": "user@example.com",
+ "display_name": "John Doe",
+ "is_admin": false,
+ "is_active": true,
+ "created_at": "2024-01-15T10:00:00Z",
+ "updated_at": "2024-04-25T17:23:00Z",
+ "last_login_at": "2024-01-15T15:30:00Z"
+ }
+]
+```
+
+#### Create User
+
+**Endpoint:** `POST /api/v1/auth/users`
+
+**Request:**
+
+```json
+{
+ "email": "newuser@example.com",
+ "display_name": "New User",
+ "password": "TempPassword123",
+ "is_admin": false
+}
+```
+
+**Response:**
+
+```json
+{
+ "user_id": "xyz789",
+ "email": "newuser@example.com",
+ "display_name": "New User",
+ "is_admin": false,
+ "is_active": true,
+ "created_at": "2024-01-15T16:00:00Z"
+}
+```
+
+#### Update User
+
+**Endpoint:** `PATCH /api/v1/auth/users/{user_id}`
+
+**Request:**
+
+```json
+{
+ "display_name": "Updated Name",
+ "is_active": true,
+ "is_admin": false
+}
+```
+
+**Response:**
+
+```json
+{
+ "user_id": "xyz789",
+ "email": "newuser@example.com",
+ "display_name": "Updated Name",
+ "is_admin": false,
+ "is_active": true
+}
+```
+
+#### Delete User
+
+**Endpoint:** `DELETE /api/v1/auth/users/{user_id}`
+
+**Response:**
+
+Returns `204 No Content` on success.
+
+On an error, it returns `422 Unprocessable Content` and the following JSON:
+
+```json
+{
+ "detail": [
+ {
+ "loc": [
+ "string",
+ 0
+ ],
+ "msg": "string",
+ "type": "string"
+ }
+ ]
+}
+```
+
+#### List Image Boards
+
+**Endpoint:** `GET /api/v1/boards/`
+
+**Response:**
+
+```json
+{
+ "limit": 0,
+ "offset": 0,
+ "total": 0,
+ "items": [
+ {
+ "board_id": "8b31a33d-0acb-46fe-8612-83601481cf2c",
+ "board_name": "Testing Board",
+ "user_id": "string",
+ "created_at": "2026-05-07T03:04:00.738Z",
+ "updated_at": "2026-05-07T03:04:00.738Z",
+ "deleted_at": "2026-05-07T03:04:00.738Z",
+ "cover_image_name": "string",
+ "archived": false,
+ "board_visibility": "private",
+ "image_count": 0,
+ "asset_count": 0,
+ "owner_username": "string"
+ }
+ ]
+}
+```
+
+This returns a paged response. See the swagger page (`http://localhost:9090/docs#/boards/list_boards`) for details.
+The `board_visibility` field will be one of:
+
+- `private` -- private to the owner and administrator
+- `shared` -- read/write to the owner and administrator, read-only to everyone else
+- `public` -- read/write by everyone
+
+#### Get One Board
+
+**Endpoint:** `GET /api/v1/boards/{board_id}`
+
+**Response:**
+
+```json
+{
+ "board_id": "8b31a33d-0acb-46fe-8612-83601481cf2c",
+ "board_name": "Testing Board",
+ "user_id": "3c59a0ba-f4c7-4275-b96f-82179e8aaff8",
+ "created_at": "2026-03-09 16:10:47.095",
+ "updated_at": "2026-03-09 16:10:55",
+ "deleted_at": null,
+ "cover_image_name": "08689e4b-f084-4c49-83a8-4fc1edb167c4.png",
+ "archived": false,
+ "board_visibility": "shared",
+ "image_count": 55,
+ "asset_count": 0,
+ "owner_username": null
+}
+```
+
+
+## Best Practices
+
+### Token Storage
+
+**Do:**
+
+- Store tokens securely (keychain, secure storage)
+- Use HTTPS to transmit tokens
+- Clear tokens on logout
+- Handle token expiration gracefully
+
+**Don't:**
+
+- Store tokens in URL parameters
+- Log tokens in plain text
+- Share tokens between users
+- Store tokens in version control
+
+### Error Handling
+
+Always handle authentication errors:
+
+```python
+def make_request(client, func, *args, **kwargs):
+ max_retries = 3
+ retry_count = 0
+
+ while retry_count < max_retries:
+ try:
+ return func(*args, **kwargs)
+ except AuthenticationError:
+ if retry_count >= max_retries - 1:
+ raise
+ # Re-authenticate
+ client.login(email, password)
+ retry_count += 1
+ except Exception as e:
+ logger.error(f"Request failed: {e}")
+ raise
+```
+
+### Rate Limiting
+
+Be mindful of API rate limits:
+
+- Implement exponential backoff for retries
+- Cache frequently accessed data
+- Batch requests when possible
+- Don't hammer the login endpoint
+
+### Connection Management
+
+```python
+import requests
+from requests.adapters import HTTPAdapter
+from urllib3.util.retry import Retry
+
+def create_session():
+ """Create session with retry logic."""
+ session = requests.Session()
+
+ retry = Retry(
+ total=3,
+ backoff_factor=0.3,
+ status_forcelist=[500, 502, 503, 504],
+ )
+
+ adapter = HTTPAdapter(max_retries=retry)
+ session.mount('http://', adapter)
+ session.mount('https://', adapter)
+
+ return session
+```
+
+## Migration Guide
+
+### Updating Existing Code
+
+**Before (single-user mode):**
+
+```python
+import requests
+
+def get_boards():
+ response = requests.get("http://localhost:9090/api/v1/boards/")
+ return response.json()
+```
+
+**After (multi-user mode):**
+
+```python
+import requests
+
+class APIClient:
+ def __init__(self):
+ self.token = None
+
+ def login(self, email, password):
+ response = requests.post(
+ "http://localhost:9090/api/v1/auth/login",
+ json={"email": email, "password": password}
+ )
+ self.token = response.json()["token"]
+
+ def get_boards(self):
+ headers = {"Authorization": f"Bearer {self.token}"}
+ response = requests.get(
+ "http://localhost:9090/api/v1/boards/",
+ headers=headers
+ )
+ return response.json()
+
+# Usage
+client = APIClient()
+client.login("user@example.com", "password")
+boards = client.get_boards()
+```
+
+### Backward Compatibility
+
+InvokeAI supports both single-user and multi-user modes via the `multiuser` configuration option.
+
+**Configuration:**
+
+```yaml
+# invokeai.yaml
+
+# Single-user mode (no authentication)
+multiuser: false # or omit the option entirely
+
+# Multi-user mode (authentication required)
+multiuser: true
+```
+
+**Checking Mode Programmatically:**
+
+```python
+def is_multiuser_enabled(base_url):
+ response = requests.get(f"{base_url}/api/v1/auth/status")
+ response.raise_for_status()
+ return response.json()["multiuser_enabled"]
+
+# Example usage
+base_url = "http://localhost:9090"
+if is_multiuser_enabled(base_url):
+ print("Multi-user mode: authentication required")
+ # Use authenticated API calls
+else:
+ print("Single-user mode: no authentication needed")
+ # Use direct API calls
+```
+
+**Adaptive Client:**
+
+```python
+class AdaptiveInvokeAIClient:
+ def __init__(self, base_url="http://localhost:9090"):
+ self.base_url = base_url
+ self.token = None
+ self.multiuser_mode = self._check_multiuser_mode()
+
+ def _check_multiuser_mode(self):
+ """Detect if multi-user mode is enabled."""
+ try:
+ response = requests.get(f"{self.base_url}/api/v1/boards/")
+ return response.status_code == 401
+ except:
+ return False
+
+ def login(self, email, password):
+ """Login (only needed in multi-user mode)."""
+ if not self.multiuser_mode:
+ print("Single-user mode: login not required")
+ return
+
+ response = requests.post(
+ f"{self.base_url}/api/v1/auth/login",
+ json={"email": email, "password": password}
+ )
+ self.token = response.json()["token"]
+
+ def _get_headers(self):
+ """Get headers (with auth token if in multi-user mode)."""
+ if self.multiuser_mode and self.token:
+ return {"Authorization": f"Bearer {self.token}"}
+ return {}
+
+ def get_boards(self):
+ """Get boards (works in both modes)."""
+ response = requests.get(
+ f"{self.base_url}/api/v1/boards/",
+ headers=self._get_headers()
+ )
+ return response.json()
+```
+
+## OpenAPI/Swagger Documentation
+
+InvokeAI provides OpenAPI documentation for all endpoints.
+
+**Access Swagger UI:**
+
+```
+http://localhost:9090/docs
+```
+
+**Download OpenAPI Schema:**
+
+```bash
+curl http://localhost:9090/openapi.json > invokeai_openapi.json
+```
+
+**Generate Client Code:**
+
+Use tools like `openapi-generator` to generate client libraries:
+
+```bash
+# Generate Python client
+openapi-generator generate \
+ -i http://localhost:9090/openapi.json \
+ -g python \
+ -o ./invokeai-client
+
+# Generate TypeScript client
+openapi-generator generate \
+ -i http://localhost:9090/openapi.json \
+ -g typescript-fetch \
+ -o ./invokeai-client-ts
+```
+
+## Security Considerations
+
+### HTTPS
+
+Always use HTTPS in production:
+
+```python
+# Development
+client = InvokeAIClient("http://localhost:9090")
+
+# Production
+client = InvokeAIClient("https://invoke.example.com")
+```
+
+### Token Security
+
+Protect JWT tokens:
+
+```python
+# Never log tokens
+logger.info(f"User logged in") # Good
+logger.info(f"Token: {token}") # Bad!
+
+# Use environment variables for credentials
+import os
+email = os.environ.get("INVOKEAI_EMAIL")
+password = os.environ.get("INVOKEAI_PASSWORD")
+```
+
+### Input Validation
+
+Always validate user input:
+
+```python
+import re
+
+def validate_email(email):
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
+ return re.match(pattern, email) is not None
+
+def validate_password(password):
+ """Check password meets requirements."""
+ if len(password) < 8:
+ return False, "Password must be at least 8 characters"
+ if not any(c.isupper() for c in password):
+ return False, "Password must contain uppercase letters"
+ if not any(c.islower() for c in password):
+ return False, "Password must contain lowercase letters"
+ if not any(c.isdigit() for c in password):
+ return False, "Password must contain numbers"
+ return True, ""
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Issue: "Invalid authentication credentials"**
+
+- Token expired — re-authenticate
+- Token malformed — check token string
+- Token signature invalid — check secret key hasn't changed
+
+**Issue: "Admin privileges required"**
+
+- User is not an administrator
+- Use admin account for this operation
+
+**Issue: Token not being sent**
+
+- Check `Authorization` header is present
+- Verify `Bearer` prefix is included
+- Check token isn't truncated
+
+**Issue: CORS errors**
+
+Configure CORS in InvokeAI:
+
+```yaml
+# invokeai.yaml
+allow_origins:
+ - "http://localhost:3000"
+ - "https://myapp.example.com"
+allow_credentials: true
+allow_methods:
+ - "*"
+allow_headers:
+ - "*"
+```
+
+## Additional Resources
+
+- [User Guide](./user-guide/) — For end users
+- [Administrator Guide](./admin-guide/) — For administrators
+- [GitHub Repository](https://github.com/invoke-ai/InvokeAI) — Source code
+
+---
+
+**Questions?** Visit the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy) or check the [FAQ](/troubleshooting/faq/).
diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png
new file mode 100644
index 00000000000..706039d50cb
Binary files /dev/null and b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-1.png differ
diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-2.png b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-2.png
new file mode 100644
index 00000000000..44bf2e180a3
Binary files /dev/null and b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-2.png differ
diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-3.png b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-3.png
new file mode 100644
index 00000000000..708f9a85135
Binary files /dev/null and b/docs/src/content/docs/features/Multi-User Mode/assets/admin-add-user-3.png differ
diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/admin-setup.png b/docs/src/content/docs/features/Multi-User Mode/assets/admin-setup.png
new file mode 100644
index 00000000000..fbe035e5c6e
Binary files /dev/null and b/docs/src/content/docs/features/Multi-User Mode/assets/admin-setup.png differ
diff --git a/docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png b/docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png
new file mode 100644
index 00000000000..8c4bec22943
Binary files /dev/null and b/docs/src/content/docs/features/Multi-User Mode/assets/user-login-1.png differ
diff --git a/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx
new file mode 100644
index 00000000000..d595668a65e
--- /dev/null
+++ b/docs/src/content/docs/features/Multi-User Mode/user-guide.mdx
@@ -0,0 +1,366 @@
+---
+title: Multi-User Guide
+description: How to use InvokeAI in multi-user mode as an end user.
+sidebar:
+ order: 3
+---
+import { Steps } from '@astrojs/starlight/components'
+
+## Overview
+
+Multi-User mode is a recent feature (introduced in version 6.12), which allows multiple individuals to share a single InvokeAI server while keeping their work separate and organized. Each user has their own username and login password, images, assets, image boards, customization settings and workflows.
+
+Two types of users are recognized:
+
+- A user with **Administrator** status can add, remove and modify other users, and can install models. They also have the ability to view the full session queue and pause or kill other users' jobs.
+- **Non-administrator** users can modify their own profile but not others. They also do not have the ability to install or configure models, but must ask an Administrator to do this task. When viewing the generation queue, they can see the full details of their own jobs, but jobs owned by other users will have the user id, generation parameters, and other details redacted.
+
+Multiple users can be granted Administrator status.
+
+---
+
+## Getting Started
+
+To activate Multi-User mode, open the `INVOKEAI_ROOT/invokeai.yaml` configuration file in a text editor. Add this line anywhere in the file:
+
+```yaml
+multiuser: true
+```
+
+You may also wish to make InvokeAI available to other machines on your local LAN. Add an additional line to `invokeai.yaml`:
+
+```yaml
+host: 0.0.0.0
+```
+
+Restart the server. It will now be in multi-user mode. If you enabled the `host` option, other users on your home or office LAN will be able to reach it by browsing to the IP address of the machine the backend is running on (`http://host-ip-address:9090`).
+
+:::tip[Do not expose InvokeAI to the internet]
+It is not recommended to expose the InvokeAI host to the internet due to security concerns.
+:::
+
+### Initial Setup (First Time in Multi-User Mode)
+
+If you're the first person to access a fresh InvokeAI installation in multi-user mode, you'll see the **Administrator Setup** dialog:
+
+
+
+Now:
+
+
+1. Enter your email address (this will be your login name)
+2. Create a display name (this will be the name other users see)
+3. Choose a strong password. The following criteria are required with `strict_password_checking: true`.
+ - At least 8 characters long
+ - Contains uppercase letters
+ - Contains lowercase letters
+ - Contains numbers
+4. Confirm your password
+5. Click **Create Administrator Account**
+
+
+With `strict_password_checking` disabled, you'll be warned if you choose a
+weak password, but not prevented from doing so.
+
+You'll now be taken to a login screen and can enter the credentials you just created.
+
+### Adding and Modifying Users
+
+If you are logged in as Administrator, you can add additional users. Click on the small "person silhouette" icon at the bottom left of the main Invoke screen and select "User Management"
+
+
+
+This will take you to the User Management screen...
+
+
+
+...where you can click "Create User" to add a new user.
+
+
+
+The User Management screen also allows you to:
+
+
+1. Temporarily change a user's status to Inactive, preventing them from logging in to Invoke.
+2. Edit a user (by clicking on the pencil icon) to change the user's display name or password.
+3. Permanently delete a user.
+4. Grant a user Administrator privileges.
+
+
+---
+
+## Logging in as a Non-Administrative User
+
+If you are a registered user on the system, enter your email address and password to log in. The Administrator will be able to provide you with the values to use:
+
+
+
+As an unprivileged user you can do pretty much anything that's allowed under single-user mode — generating images, using LoRAs, creating and running workflows, creating image boards — but you are restricted against installing new models, changing low-level server settings, or interfering with other users. More information on user roles is given below.
+
+### Changing your Profile
+
+To change your display name or profile, click on the person silhouette icon at the bottom left of the screen and choose "My Profile". This will take you to a screen that lets you change these values. At this time you can change your display name but not your login ID (ordinarily your contact email address).
+
+---
+
+## Understanding User Roles
+
+In single-user mode, you have access to all features without restrictions. In multi-user mode, InvokeAI has two user roles:
+
+### Regular User
+
+As a regular user, you can:
+
+- Create and manage your own image boards
+- Generate images using all AI tools (Linear, Canvas, Upscale, Workflows)
+- Create, save, and load your own workflows
+- View the full details of jobs you own on the session queue
+- View redacted information for jobs being run by other users
+- Customize your UI preferences (theme, hotkeys, etc.)
+- View available models (read-only access to Model Manager)
+- View shared and public boards created by other users
+- View and use workflows marked as shared by other users
+
+You cannot:
+
+- Add, delete, or modify models
+- View or modify other users' private boards, images, or workflows
+- Manage user accounts
+- Access system configuration
+- Cancel other users' generation jobs
+
+:::tip[The generation queue]
+When two or more users are accessing InvokeAI at the same time, their image generation jobs will be placed on the session queue on a first-come, first-serve basis. This means that you will have to wait for other users' image rendering jobs to complete before yours will start.
+
+While other users' jobs are running you will see the shared image generation progress bar, and the queue badge will show a single number — the count of your own jobs that are pending or in progress. It does not show other users' counts.
+
+Open the Queue tab to see where your job sits in relation to the other queued tasks.
+:::
+
+### Administrator
+
+Administrators have all regular user capabilities, plus:
+
+- Full model management (add, delete, configure models)
+- Create and manage user accounts
+- View and manage all users' generation queues
+- View and manage all users' boards, images, and workflows (including system-owned legacy content)
+- Access system configuration
+- Grant or revoke admin privileges
+
+---
+
+## Working with Your Content in Multi-User Mode
+
+### Image Boards
+
+In multi-user mode, each user can create an unlimited number of boards and organize their images and assets as they see fit. Boards have three visibility levels:
+
+- **Private** (default): Only you (and administrators) can see and modify the board.
+- **Shared**: All users can view the board and its contents, but only you (and administrators) can modify it (rename, archive, delete, or add/remove images).
+- **Public**: All users can view the board. Only you (and administrators) can modify the board's structure (rename, archive, delete).
+
+To change a board's visibility, right-click on the board and select the desired visibility option.
+
+Administrators can see and manage all users' image boards and their contents regardless of visibility settings.
+
+### Going From Multi-User to Single-User Mode
+
+If an InvokeAI instance was in multiuser mode and then restarted in single user mode (by setting `multiuser: false` in the configuration file), all users' boards will be consolidated in one place. Any images that were in "Uncategorized" will be merged together into a single Uncategorized board. If, at a later date, the server is restarted in multi-user mode, the boards and images will be assigned to the internal 'system' user. Admins can access this legacy content, and will not be restored to original owners.
+
+### Workflows
+
+Each user has their own private workflow library. Workflows you create are visible only to you by default.
+
+You can share a workflow with other users by marking it as **shared** (public). Shared workflows appear in all users' workflow libraries and can be opened by anyone, but only the owner (or an administrator) can modify or delete them.
+
+To share a workflow, open it and use the sharing controls to toggle its public/shared status.
+
+:::caution[Preexisting workflows after enabling multi-user mode]
+When you enable multi-user mode for the first time on an existing InvokeAI installation, all workflows that were created before multi-user mode was activated will appear in the **shared workflows** section. These preexisting workflows are owned by the internal "system" account and are visible to all users. Administrators can edit or delete these shared legacy workflows. Regular users can view and use them but cannot modify them.
+:::
+
+### The Generation Queue
+
+The queue shows your pending and running generation tasks.
+
+**Queue Features:**
+
+- View your current and completed generations
+- Cancel pending tasks
+- Re-run previous generations
+- Monitor progress in real-time
+
+**Queue Isolation:**
+
+- You will see your own queue items, as well as the items generated by other users, but the generation parameters (e.g. prompts) for other users' jobs are hidden for privacy reasons.
+- Administrators can view all queues for troubleshooting.
+- Your generations won't interfere with other users' tasks.
+
+---
+
+## Customizing Your Experience
+
+### Personal Preferences
+
+Your UI preferences are saved to your account and are restored when you log in:
+
+- **Hotkeys**: Customize keyboard shortcuts
+- **Canvas Settings**: Default zoom, grid visibility, etc.
+- **Generation Defaults**: Default values for width, height, steps, etc.
+
+These settings are stored per-user and won't affect other users.
+
+---
+
+## Troubleshooting
+
+### Cannot Log In
+
+**Issue:** Login fails with "Incorrect email or password"
+
+**Solutions:**
+
+- Verify you're entering the correct email address
+- Check that Caps Lock is off
+- Try typing the password slowly to avoid mistakes
+- Contact your administrator if you've forgotten your password
+
+**Issue:** Login fails with "Account is disabled"
+
+**Solution:** Contact your administrator to reactivate your account
+
+### Session Expired
+
+**Issue:** You're suddenly logged out and see "Session expired"
+
+**Explanation:** Sessions expire after 24 hours (or 7 days with "remember me")
+
+**Solution:** Simply log in again with your credentials
+
+### Cannot Access Features
+
+**Issue:** Features like Model Manager show "Admin privileges required"
+
+**Explanation:** Some features are restricted to administrators
+
+**Solution:**
+
+- For model viewing: You can view but not modify models
+- For user management: Contact an administrator
+- For system configuration: Contact an administrator
+
+### Missing Boards or Images
+
+**Issue:** Boards or images you created are not visible
+
+**Possible Causes:**
+
+
+1. **Filter Applied:** Check if a filter is hiding content
+2. **Wrong User:** Ensure you're logged in with the correct account
+3. **Archived Board:** Check the "Show Archived" option
+
+
+**Solution:**
+
+- Clear any active filters
+- Verify you're logged in as the right user
+- Check archived items
+
+### Slow Performance
+
+**Issue:** Generation or UI feels slower than expected
+
+**Possible Causes:**
+
+- Other users generating images simultaneously
+- Server resource limits
+- Network latency
+
+**Solutions:**
+
+- Check the queue to see if others are generating
+- Wait for current generations to complete
+- Contact administrator if persistent
+
+### Generation Stuck in Queue
+
+**Issue:** Your generation is queued but not starting
+
+**Possible Causes:**
+
+- Server is processing other users' generations
+- Server resources are fully utilized
+- Technical issue with the server
+
+**Solutions:**
+
+- Wait for your turn in the queue
+- Check if your generation is paused
+- Contact administrator if stuck for extended period
+
+---
+
+## Frequently Asked Questions
+
+### Can other users see my images?
+
+Not unless you change your board's visibility to "shared" or "public". All personal boards and images are private by default.
+
+### Can I share my workflows with others?
+
+Yes. You can mark any workflow as shared (public), which makes it visible to all users. Other users can view and use shared workflows, but only you or an administrator can modify or delete them.
+
+### How long do sessions last?
+
+- 24 hours by default
+- 7 days if you check "Remember me" during login
+
+### Can I use the API with multi-user mode?
+
+Yes, but you'll need to authenticate with a JWT token. See the [API Guide](./api-guide/) for details.
+
+### What happens if I forget my password?
+
+Contact your administrator. They can reset your password for you.
+
+### Can I have multiple sessions?
+
+Yes, you can log in from multiple devices or browsers simultaneously. All sessions will use the same account and see the same content.
+
+### Why can't I see the Model Manager "Add Models" tab?
+
+Regular users can see the Models tab but with read-only access. Check that you're logged in and try refreshing the page.
+
+### How do I know if I'm an administrator?
+
+Click the user icon near the bottom of the left-hand navigation bar to open the user menu. If you are an administrator, an "Admin" badge appears under your name in that menu and a "User Management" item is shown alongside the usual Profile and Logout actions.
+
+### Can I request admin privileges?
+
+Yes, ask your current administrator to grant you admin privileges. Admin privileges will give you the ability to see all other users' boards and images, as well as to add models and change various server-wide settings.
+
+## Getting Help
+
+### Support Channels
+
+- **Administrator:** Contact your system administrator for account issues
+- **Documentation:** Check the [FAQ](/troubleshooting/faq/) for common issues
+- **Community:** Join the [Discord](https://discord.gg/ZmtBAhwWhy) for help
+- **Bug Reports:** File issues on [GitHub](https://github.com/invoke-ai/InvokeAI/issues)
+
+### Reporting Issues
+
+When reporting an issue, include:
+
+- Your role (regular user or administrator)
+- What you were trying to do
+- What happened instead
+- Any error messages you saw
+- Your browser and operating system
+
+## Additional Resources
+
+- [Administrator Guide](./admin-guide/) — For administrators managing users and the system
+- [API Guide](./api-guide/) — For developers using the InvokeAI API
diff --git a/docs/src/content/docs/features/Workflows/adding-nodes.mdx b/docs/src/content/docs/features/Workflows/adding-nodes.mdx
new file mode 100644
index 00000000000..6210d739194
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/adding-nodes.mdx
@@ -0,0 +1,170 @@
+---
+title: Adding Nodes
+description: Learn how to add, connect, and configure nodes in InvokeAI's workflow editor.
+sidebar:
+ order: 3
+lastUpdated: 2026-03-16
+---
+
+import { Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
+
+Nodes are the building blocks of workflows. Each node performs a specific operation — loading a model, generating noise, applying conditioning, denoising latents, and more. By adding nodes to the canvas and connecting them together, you create a complete image generation pipeline.
+
+## Opening the Node Picker
+
+The node picker is a searchable command palette that lists every available node. There are three ways to open it:
+
+
+
+ Press Shift + A or Space while the workflow editor is focused.
+
+
+ Click the **+** button in the top-left corner of the canvas.
+
+
+ Drag a connection from any input or output port and release it over empty canvas. The picker will open with results **filtered to compatible nodes only**.
+
+
+
+## Finding a Node
+
+When the node picker opens, you can immediately start typing to search. The search is fuzzy and matches against several properties of each node:
+
+- **Title** — the display name (e.g. "Denoise Latents")
+- **Type** — the internal identifier
+- **Description** — a short summary of what the node does
+- **Tags** — category keywords
+- **Node Pack** — the origin module (e.g. `invokeai` for built-in nodes, or a community pack name)
+
+Each entry in the picker shows:
+
+- A **classification badge** indicating stability — _Stable_, _Beta_ (yellow), _Prototype_ (red), or _Special_ (green)
+- The **node title** and **node pack** name
+- A brief **description**
+
+Click a node or press Enter to add it to the canvas. The node will be placed near the center of your current viewport, or at your cursor position if you opened the picker by dragging from a port.
+
+:::tip
+If you opened the picker by dragging from a port, the list is automatically filtered to show only nodes that have a compatible input or output for that connection. This is a fast way to discover which nodes work together.
+:::
+
+## Special Nodes
+
+In addition to invocation nodes (which perform image generation operations), the picker includes two special utility nodes:
+
+
+
+ A sticky-note text area for documenting your workflow. Useful for leaving yourself reminders or explaining sections of a complex graph to others.
+
+
+ Displays the current image being generated or the most recent output. Helpful for monitoring progress in long workflows.
+
+
+
+## Connecting Nodes
+
+Nodes have **input ports** on their left edge and **output ports** on their right edge. Ports are color-coded by data type so you can quickly identify compatible connections.
+
+
+1. **Drag from an output port** on one node toward the canvas.
+2. **Drop onto a compatible input port** on another node. Compatible ports will remain highlighted; incompatible ports will appear greyed out.
+3. A **bezier edge** is drawn between the two ports, representing the data flow.
+
+
+:::note
+You can also drop a connection onto a node without targeting a specific port. InvokeAI will automatically connect to the **first compatible port** it finds on that node.
+:::
+
+### Connection Rules
+
+- Connections must be between compatible data types (matching colors).
+- A node cannot connect to itself.
+- Each input port accepts only one connection (but an output can connect to many inputs).
+- Connections snap within a 30px radius of a port for easy targeting.
+
+### Reconnecting and Removing Edges
+
+- **Reconnect** an edge by dragging it from its current port to a new one.
+- **Remove** an edge by dragging it away from its port and releasing it over empty canvas.
+- **Delete** selected edges with Delete or Backspace .
+
+### Connectors
+
+Connectors are small editor-only nodes that exist purely to **reroute edges** for a cleaner-looking graph. They are saved with the workflow but are flattened out of the graph before execution, so the runtime never sees them — you cannot use them to add logic, only to tidy wiring.
+
+Ways to add a connector:
+
+- **Right-click empty canvas → Add Connector**, then drag connections to and from it.
+- **Double-click an existing edge** to insert a connector at that point, splicing it in.
+
+Other behaviors worth knowing:
+
+- **Target-first wiring works.** You can connect a connector's output to a downstream target field *before* hooking up its upstream source. The connector stays unresolved until a compatible source is connected; incompatible upstreams are rejected.
+- **Type compatibility is enforced** through the connector, exactly as for normal edges.
+- **Deleting a connector splices through** any edges that pass through it:
+ - `1 → 1`: the source is reconnected directly to the target.
+ - `1 → N`: the source is reconnected to every compatible downstream target.
+ - `1 → 0`: the connector is removed, no edges created.
+ - If a splice-through would produce an invalid graph, **Delete Connector** is disabled.
+- **Connectors persist** across workflow save / load.
+
+## Configuring Nodes
+
+Once a node is on the canvas, you can configure it by editing its input fields directly. Each node exposes a set of fields specific to its function — for example, a noise node has a **Seed** field, while a model loader has a **Model** selector.
+
+- **Inline editing** — Click on any input field to edit its value directly on the node.
+- **Renaming** — Right-click a node's title or any input label to rename it.
+- **Use Cache** — Toggle the caching option in the node footer to reuse previously computed values and speed up repeat runs.
+- **Collapse** — Click the collapse button on the node header to minimize it, keeping the canvas tidy.
+
+## Managing Nodes
+
+Use these shortcuts to work efficiently with nodes on the canvas:
+
+| Action | Shortcut |
+| :--- | :--- |
+| Add Node | Shift + A or Space |
+| Copy | Ctrl /Cmd + C |
+| Paste | Ctrl /Cmd + V |
+| Paste with Edges | Ctrl /Cmd + Shift + V |
+| Select All | Ctrl /Cmd + A |
+| Delete | Delete or Backspace |
+| Undo | Ctrl /Cmd + Z |
+| Redo | Ctrl /Cmd + Shift + Z |
+| Select Multiple | Shift + Click & Drag |
+
+:::tip
+All keyboard shortcuts are customizable. Open the Hotkeys modal with Shift + ? to view or change any binding.
+:::
+
+## Adding to Linear View
+
+Any input field on a node can be promoted to the **Linear View**, which provides a simplified UI for your workflow — perfect for sharing with others or for quick iteration.
+
+
+1. Right-click on an **input label** on any node.
+2. Select **"Add to Linear View"**.
+3. The input now appears in the Linear View panel, where you can adjust it without navigating the full graph.
+
+
+Custom names you set on input fields will carry over into the Linear View.
+
+## Installing Community Nodes
+
+InvokeAI's node system is extensible. Community-created nodes can add new capabilities to your workflows — from specialized image processing to LLM-powered prompt generation.
+
+The easiest way to install a community node pack is through the **[Custom Node Manager](/features/workflows/custom-node-manager/)**: paste a Git URL in the **Nodes** sidebar tab and the pack is cloned, loaded, and made available without a restart.
+
+If you prefer to install manually:
+
+
+1. Find a node pack from the [Community Nodes](/features/workflows/community-nodes/) list.
+2. Clone or download the node pack into the `nodes` folder inside your InvokeAI installation directory.
+3. In the Custom Node Manager, click **Reload** (or restart InvokeAI). The new nodes will appear in the node picker.
+
+
+:::note
+`git clone` is preferred over downloading a ZIP — it makes it easy to update node packs later with `git pull`.
+:::
+
+For more details and a full catalog of available community nodes, see the [Community Nodes](/features/workflows/community-nodes/) page.
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsallscale.png b/docs/src/content/docs/features/Workflows/assets/groupsallscale.png
new file mode 100644
index 00000000000..5a12fe9e131
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsallscale.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsconditioning.png b/docs/src/content/docs/features/Workflows/assets/groupsconditioning.png
new file mode 100644
index 00000000000..baaf2b44e0e
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsconditioning.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupscontrol.png b/docs/src/content/docs/features/Workflows/assets/groupscontrol.png
new file mode 100644
index 00000000000..a38e4e4bbaa
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupscontrol.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsimgvae.png b/docs/src/content/docs/features/Workflows/assets/groupsimgvae.png
new file mode 100644
index 00000000000..03ac8d1f4aa
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsimgvae.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsiterate.png b/docs/src/content/docs/features/Workflows/assets/groupsiterate.png
new file mode 100644
index 00000000000..50b762099a8
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsiterate.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupslora.png b/docs/src/content/docs/features/Workflows/assets/groupslora.png
new file mode 100644
index 00000000000..74ae8a70736
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupslora.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png b/docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png
new file mode 100644
index 00000000000..dcd64c77581
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsmultigenseeding.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/groupsnoise.png b/docs/src/content/docs/features/Workflows/assets/groupsnoise.png
new file mode 100644
index 00000000000..d95b7ba3073
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/groupsnoise.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/linearview.png b/docs/src/content/docs/features/Workflows/assets/linearview.png
new file mode 100644
index 00000000000..fb6b3efca0e
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/linearview.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/nodescontrol.png b/docs/src/content/docs/features/Workflows/assets/nodescontrol.png
new file mode 100644
index 00000000000..8b179e43acd
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/nodescontrol.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/nodesi2i.png b/docs/src/content/docs/features/Workflows/assets/nodesi2i.png
new file mode 100644
index 00000000000..99088338042
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/nodesi2i.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/nodest2i.png b/docs/src/content/docs/features/Workflows/assets/nodest2i.png
new file mode 100644
index 00000000000..7e882dbf1b6
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/nodest2i.png differ
diff --git a/docs/src/content/docs/features/Workflows/assets/workflow_library.png b/docs/src/content/docs/features/Workflows/assets/workflow_library.png
new file mode 100644
index 00000000000..a17593d3b6b
Binary files /dev/null and b/docs/src/content/docs/features/Workflows/assets/workflow_library.png differ
diff --git a/docs/src/content/docs/features/Workflows/comfyui-migration.mdx b/docs/src/content/docs/features/Workflows/comfyui-migration.mdx
new file mode 100644
index 00000000000..164a778925b
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/comfyui-migration.mdx
@@ -0,0 +1,119 @@
+---
+title: ComfyUI Migration
+lastUpdated: 2026-05-23
+---
+
+import { Card, CardGrid } from '@astrojs/starlight/components';
+
+If you're coming to InvokeAI from ComfyUI, welcome! You'll find things are similar but different - the good news is that you already know how things should work, and it's just a matter of wiring them up!
+
+
+ InvokeAI's nodes tend to be more granular than default nodes in Comfy. This means each node in Invoke will do a specific task, and you might need to use multiple nodes to achieve the same result. The added granularity improves the control you have over your workflows.
+
+
+ InvokeAI's backend and ComfyUI's backend are very different, which means Comfy workflows are not able to be imported directly into InvokeAI. However, we have created a [list of popular workflows](../community-nodes) for you to get started with Nodes in InvokeAI!
+
+
+## Node Equivalents
+
+Finding the right node is the hardest part of switching. Use the categories below to find the InvokeAI equivalents for the ComfyUI nodes you are used to.
+
+### Sampling
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| KSampler | Denoise Latents |
+| Ksampler Advanced | Denoise Latents |
+
+### Loaders
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| Load Checkpoint | Main Model Loader _or_ SDXL Main Model Loader |
+| Load VAE | VAE Loader |
+| Load Lora | LoRA Loader _or_ SDXL Lora Loader |
+| Load ControlNet Model | ControlNet |
+| Load ControlNet Model (diff) | ControlNet |
+| Load Style Model | Reference Only ControlNet will be coming in a future version of InvokeAI |
+| unCLIPCheckpointLoader | N/A |
+| GLIGENLoader | N/A |
+| Hypernetwork Loader | N/A |
+| Load Upscale Model | Occurs within "Upscale (RealESRGAN)" |
+
+### Conditioning
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| CLIP Text Encode (Prompt) | Compel (Prompt) or SDXL Compel (Prompt) |
+| CLIP Set Last Layer | CLIP Skip |
+| Conditioning (Average) | Use the .blend() feature of prompts |
+| Conditioning (Combine) | N/A |
+| Conditioning (Concat) | See the Prompt Tools Community Node |
+| Conditioning (Set Area) | N/A |
+| Conditioning (Set Mask) | Mask Edge |
+| CLIP Vision Encode | N/A |
+| unCLIPConditioning | N/A |
+| Apply ControlNet | ControlNet |
+| Apply ControlNet (Advanced) | ControlNet |
+
+### Latent
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| VAE Decode | Latents to Image |
+| VAE Encode | Image to Latents |
+| Empty Latent Image | Noise |
+| Upscale Latent | Resize Latents |
+| Upscale Latent By | Scale Latents |
+| Latent Composite | Blend Latents |
+| LatentCompositeMasked | N/A |
+
+### Image
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| Save Image | Image |
+| Preview Image | Current |
+| Load Image | Image |
+| Empty Image | Blank Image |
+| Invert Image | Invert Lerp Image |
+| Batch Images | Link "Image" nodes into an "Image Collection" node |
+| Pad Image for Outpainting | Outpainting is easily accomplished in the Unified Canvas |
+| ImageCompositeMasked | Paste Image |
+| Upscale Image | Resize Image |
+| Upscale Image By | Upscale Image |
+| Upscale Image (using Model) | Upscale Image |
+| ImageBlur | Blur Image |
+| ImageQuantize | N/A |
+| ImageSharpen | N/A |
+| Canny | Canny Processor |
+
+### Mask
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| Load Image (as Mask) | Image |
+| Convert Mask to Image | Image |
+| Convert Image to Mask | Image |
+| SolidMask | N/A |
+| InvertMask | Invert Lerp Image |
+| CropMask | Crop Image |
+| MaskComposite | Combine Mask |
+| FeatherMask | Blur Image |
+
+### Advanced
+
+| ComfyUI Node | Invoke Equivalent |
+| :--- | :--- |
+| Load CLIP | Main Model Loader _or_ SDXL Main Model Loader |
+| UNETLoader | Main Model Loader _or_ SDXL Main Model Loader |
+| DualCLIPLoader | Main Model Loader _or_ SDXL Main Model Loader |
+| Load Checkpoint | Main Model Loader _or_ SDXL Main Model Loader |
+| ConditioningZeroOut | N/A |
+| ConditioningSetTimestepRange | N/A |
+| CLIPTextEncodeSDXLRefiner | Compel (Prompt) or SDXL Compel (Prompt) |
+| CLIPTextEncodeSDXL | Compel (Prompt) or SDXL Compel (Prompt) |
+| ModelMergeSimple | Model Merging is available in the Model Manager |
+| ModelMergeBlocks | Model Merging is available in the Model Manager |
+| CheckpointSave | Model saving is available in the Model Manager |
+| CLIPMergeSimple | N/A |
diff --git a/docs/src/content/docs/features/Workflows/community-nodes.mdx b/docs/src/content/docs/features/Workflows/community-nodes.mdx
new file mode 100644
index 00000000000..66ca6bbdf64
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/community-nodes.mdx
@@ -0,0 +1,731 @@
+---
+title: Community Nodes
+---
+
+These are nodes that have been developed by the community, for the community. If you're not sure what a node is, you can learn more about nodes [here](/concepts/nodes-workflows/).
+
+If you'd like to submit a node for the community, please refer to the [node creation overview](/development/guides/creating-nodes/).
+
+To use a node, add the node to the `nodes` folder found in your InvokeAI install location.
+
+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 whole node folder from the linked repository and add it to the `nodes` folder.
+
+To use a community workflow, download the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor.
+
+---
+
+### Anamorphic Tools
+
+**Description:** A set of nodes to perform anamorphic modifications to images, like lens blur, streaks, spherical distortion, and vignetting.
+
+**Node Link:** https://github.com/JPPhoto/anamorphic-tools
+
+---
+
+### Adapters Linked Nodes
+
+**Description:** A set of nodes for linked adapters (ControlNet, IP-Adaptor & T2I-Adapter). This allows multiple adapters to be chained together without using a `collect` node which means it can be used inside an `iterate` node without any collecting on every iteration issues.
+
+- `ControlNet-Linked` - Collects ControlNet info to pass to other nodes.
+- `IP-Adapter-Linked` - Collects IP-Adapter info to pass to other nodes.
+- `T2I-Adapter-Linked` - Collects T2I-Adapter info to pass to other nodes.
+
+Note: These are inherited from the core nodes so any update to the core nodes should be reflected in these.
+
+**Node Link:** https://github.com/skunkworxdark/adapters-linked-nodes
+
+---
+
+### Autostereogram Nodes
+
+**Description:** Generate autostereogram images from a depth map. This is not a very practically useful node but more a 90s nostalgic indulgence as I used to love these images as a kid.
+
+**Node Link:** https://github.com/skunkworxdark/autostereogram_nodes
+
+**Example Usage:**
+
+ -> ->
+
+---
+
+### Average Images
+
+**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.
+
+**Node Link:** https://github.com/JPPhoto/average-images-node
+
+---
+
+### BiRefNet Background Removal
+
+**Description:** Remove image backgrounds using BiRefNet (Bilateral Reference Network), a high-quality segmentation model. Supports multiple model variants including standard, high-resolution, matting, portrait, and specialized models for different use cases.
+
+**Node Link:** https://github.com/veeliks/invoke_birefnet
+
+**Output Examples**
+
+
+
+
+
+
+---
+
+### Clean Image Artifacts After Cut
+
+Description: Removes residual artifacts after an image is separated from its background.
+
+Node Link: https://github.com/VeyDlin/clean-artifact-after-cut-node
+
+View:
+
+
+
+---
+
+### Close Color Mask
+
+Description: Generates a mask for images based on a closely matching color, useful for color-based selections.
+
+Node Link: https://github.com/VeyDlin/close-color-mask-node
+
+View:
+
+
+
+---
+
+### Clothing Mask
+
+Description: Employs a U2NET neural network trained for the segmentation of clothing items in images.
+
+Node Link: https://github.com/VeyDlin/clothing-mask-node
+
+View:
+
+
+
+---
+
+### Contrast Limited Adaptive Histogram Equalization
+
+Description: Enhances local image contrast using adaptive histogram equalization with contrast limiting.
+
+Node Link: https://github.com/VeyDlin/clahe-node
+
+View:
+
+
+
+---
+
+### Curves
+
+**Description:** Adjust an image's curve based on a user-defined string.
+
+**Node Link:** https://github.com/JPPhoto/curves-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:**
+
+
+
+---
+
+### Enhance Detail
+
+**Description:** A single node that can enhance the detail in an image. Increase or decrease details in an image using a guided filter (as opposed to the typical Gaussian blur used by most sharpening filters.) Based on the `Enhance Detail` ComfyUI node from https://github.com/spacepxl/ComfyUI-Image-Filters
+
+**Node Link:** https://github.com/skunkworxdark/enhance-detail-node
+
+**Example Usage:**
+
+
+
+---
+
+### Film Grain
+
+**Description:** This node adds a film grain effect to the input image based on the weights, seeds, and blur radii parameters. It works with RGB input images only.
+
+**Node Link:** https://github.com/JPPhoto/film-grain-node
+
+---
+
+### Flip Pose
+
+**Description:** This node will flip an openpose image horizontally, recoloring it to make sure that it isn't facing the wrong direction. Note that it does not work with openpose hands.
+
+**Node Link:** https://github.com/JPPhoto/flip-pose-node
+
+---
+
+### Flux Ideal Size
+
+**Description:** This node returns an ideal size to use for the first stage of a Flux image generation pipeline. Generating at the right size helps limit duplication and odd subject placement.
+
+**Node Link:** https://github.com/JPPhoto/flux-ideal-size
+
+---
+
+### 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 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:**
+
+
+
+---
+
+### GPT2RandomPromptMaker
+
+**Description:** A node for InvokeAI utilizes the GPT-2 language model to generate random prompts based on a provided seed and context.
+
+**Node Link:** https://github.com/mickr777/GPT2RandomPromptMaker
+
+**Output Examples**
+
+Generated Prompt: An enchanted weapon will be usable by any character regardless of their alignment.
+
+
+
+---
+
+### 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**
+
+
+
+
+---
+
+### 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:
+
+
+
+Halftone Output:
+
+
+
+CMYK Halftone Output:
+
+
+
+---
+
+### Hand Refiner with MeshGraphormer
+
+**Description**: Hand Refiner takes in your image and automatically generates a fixed depth map for the hands along with a mask of the hands region that will conveniently allow you to use them along with ControlNet to fix the wonky hands generated by Stable Diffusion
+
+**Node Link:** https://github.com/blessedcoolant/invoke_meshgraphormer
+
+**View**
+
+
+---
+
+### 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
+
+
+
+---
+
+### Image Dominant Color
+
+Description: Identifies and extracts the dominant color from an image using k-means clustering.
+
+Node Link: https://github.com/VeyDlin/image-dominant-color-node
+
+View:
+
+
+
+---
+
+### Image Export
+
+**Description:** Export images in multiple formats (AVIF, JPEG, PNG, TIFF, WebP) with format-specific compression and quality options.
+
+**Node Link:** https://github.com/veeliks/invoke_image_export
+
+**Nodes:**
+
+
+
+---
+
+### 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**
+
+
+
+
+
+
+
+
+
+---
+
+### 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
+
+---
+
+### Image Resize Plus
+
+Description: Provides various image resizing options such as fill, stretch, fit, center, and crop.
+
+Node Link: https://github.com/VeyDlin/image-resize-plus-node
+
+View:
+
+
+
+---
+
+### Latent Upscale
+
+**Description:** This node uses a small (~2.4mb) model to upscale the latents used in a Stable Diffusion 1.5 or Stable Diffusion XL image generation, rather than the typical interpolation method, avoiding the traditional downsides of the latent upscale technique.
+
+**Node Link:** [https://github.com/gogurtenjoyer/latent-upscale](https://github.com/gogurtenjoyer/latent-upscale)
+
+---
+
+### 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.
+
+**Node Link:** https://github.com/helix4u/load_video_frame
+
+**Output Example:**
+
+
+
+---
+### 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**
+
+
+
+
+---
+
+### Mask Operations
+
+Description: Offers logical operations (OR, SUB, AND) for combining and manipulating image masks.
+
+Node Link: https://github.com/VeyDlin/mask-operations-node
+
+View:
+
+
+
+---
+
+### Match Histogram
+
+**Description:** An InvokeAI node to match a histogram from one image to another. This is a bit like the `color correct` node in the main InvokeAI but this works in the YCbCr colourspace and can handle images of different sizes. Also does not require a mask input.
+- Option to only transfer luminance channel.
+- Option to save output as grayscale
+
+A good use case for this node is to normalize the colors of an image that has been through the tiled scaling workflow of my XYGrid Nodes.
+
+See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/main/README.md
+
+**Node Link:** https://github.com/skunkworxdark/match_histogram
+
+**Output Examples**
+
+
+
+---
+
+### Metadata Linked Nodes
+
+**Description:** A set of nodes for Metadata. Collect Metadata from within an `iterate` node & extract metadata from an image.
+
+- `Metadata Item Linked` - Allows collecting of metadata while within an iterate node with no need for a collect node or conversion to metadata node
+- `Metadata From Image` - Provides Metadata from an image
+- `Metadata To String` - Extracts a String value of a label from metadata
+- `Metadata To Integer` - Extracts an Integer value of a label from metadata
+- `Metadata To Float` - Extracts a Float value of a label from metadata
+- `Metadata To Scheduler` - Extracts a Scheduler value of a label from metadata
+- `Metadata To Bool` - Extracts Bool types from metadata
+- `Metadata To Model` - Extracts model types from metadata
+- `Metadata To SDXL Model` - Extracts SDXL model types from metadata
+- `Metadata To LoRAs` - Extracts Loras from metadata.
+- `Metadata To SDXL LoRAs` - Extracts SDXL Loras from metadata
+- `Metadata To ControlNets` - Extracts ControNets from metadata
+- `Metadata To IP-Adapters` - Extracts IP-Adapters from metadata
+- `Metadata To T2I-Adapters` - Extracts T2I-Adapters from metadata
+- `Denoise Latents + Metadata` - This is an inherited version of the existing `Denoise Latents` node but with a metadata input and output.
+
+**Node Link:** https://github.com/skunkworxdark/metadata-linked-nodes
+
+---
+
+### Negative Image
+
+Description: Creates a negative version of an image, effective for visual effects and mask inversion.
+
+Node Link: https://github.com/VeyDlin/negative-image-node
+
+View:
+
+
+
+---
+
+### Nightmare Promptgen
+
+**Description:** Nightmare Prompt Generator - Uses a local text generation model to create unique imaginative (but usually nightmarish) prompts for InvokeAI. By default, it allows you to choose from some gpt-neo models I finetuned on over 2500 of my own InvokeAI prompts in Compel format, but you're able to add your own, as well. Offers support for replacing any troublesome words with a random choice from list you can also define.
+
+**Node Link:** [https://github.com/gogurtenjoyer/nightmare-promptgen](https://github.com/gogurtenjoyer/nightmare-promptgen)
+
+---
+
+### Ollama Node
+
+**Description:** Uses Ollama API to expand text prompts for text-to-image generation using local LLMs. Works great for expanding basic prompts into detailed natural language prompts for Flux. Also provides a toggle to unload the LLM model immediately after expanding, to free up VRAM for Invoke to continue the image generation workflow.
+
+**Node Link:** https://github.com/Jonseed/Ollama-Node
+
+**Example Node Graph:** https://github.com/Jonseed/Ollama-Node/blob/main/Ollama-Node-Flux-example.json
+
+**View:**
+
+
+
+---
+
+### One Button Prompt
+
+
+
+**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
+
+The main node generates interesting prompts based on a set of parameters. There are also some additional nodes such as Auto Negative Prompt, One Button Artify, Create Prompt Variant and other cool prompt toys to play around with.
+
+**Node Link:** [https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI](https://github.com/AIrjen/OneButtonPrompt_X_InvokeAI)
+
+**Nodes:**
+
+
+
+---
+
+### Oobabooga
+
+**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
+
+**Example:**
+
+"describe a new mystical creature in its natural environment"
+
+*can return*
+
+"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."
+
+
+
+**Requirement**
+
+a Text-Generation-Webui instance (might work remotely too, but I never tried it) and obviously InvokeAI 3.x
+
+**Note**
+
+This node works best with SDXL models, especially as the style can be described independently of the LLM's output.
+
+---
+
+### Prompt Tools
+
+**Description:** A set of InvokeAI nodes that add general prompt (string) manipulation tools. Designed to accompany the `Prompts From File` node and other prompt generation nodes.
+
+1. `Prompt To File` - saves a prompt or collection of prompts to a file. one per line. There is an append/overwrite option.
+2. `PTFields Collect` - Converts image generation fields into a Json format string that can be passed to Prompt to file.
+3. `PTFields Expand` - Takes Json string and converts it to individual generation parameters. This can be fed from the Prompts to file node.
+4. `Prompt Strength` - Formats prompt with strength like the weighted format of compel
+5. `Prompt Strength Combine` - Combines weighted prompts for .and()/.blend()
+6. `CSV To Index String` - Gets a string from a CSV by index. Includes a Random index option
+
+The following Nodes are now included in v3.2 of Invoke and are no longer in this set of tools.
+
+- `Prompt Join` -> `String Join`
+- `Prompt Join Three` -> `String Join Three`
+- `Prompt Replace` -> `String Replace`
+- `Prompt Split Neg` -> `String Split Neg`
+
+
+See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/main/README.md
+
+**Node Link:** https://github.com/skunkworxdark/Prompt-tools-nodes
+
+**Workflow Examples**
+
+
+
+---
+
+### Remote Image
+
+**Description:** This is a pack of nodes to interoperate with other services, be they public websites or bespoke local servers. The pack consists of these nodes:
+
+- *Load Remote Image* - Lets you load remote images such as a realtime webcam image, an image of the day, or dynamically created images.
+- *Post Image to Remote Server* - Lets you upload an image to a remote server using an HTTP POST request, eg for storage, display or further processing.
+
+**Node Link:** https://github.com/fieldOfView/InvokeAI-remote_image
+
+---
+
+### BriaAI Remove Background
+
+**Description**: Implements one click background removal with BriaAI's new version 1.4 model which seems to be producing better results than any other previous background removal tool.
+
+**Node Link:** https://github.com/blessedcoolant/invoke_bria_rmbg
+
+**View**
+
+
+---
+
+### Remove Background
+
+Description: An integration of the rembg package to remove backgrounds from images using multiple U2NET models.
+
+Node Link: https://github.com/VeyDlin/remove-background-node
+
+View:
+
+
+
+---
+
+### Retroize
+
+**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.
+
+**Node Link:** https://github.com/Ar7ific1al/invokeai-retroizeinode/
+
+**Retroize Output Examples**
+
+
+
+---
+
+### Stereogram Nodes
+
+**Description:** A set of custom nodes for InvokeAI to create cross-view or parallel-view stereograms. Stereograms are 2D images that, when viewed properly, reveal a 3D scene. Check out [r/crossview](https://www.reddit.com/r/CrossView/) for tutorials.
+
+**Node Link:** https://github.com/simonfuhrmann/invokeai-stereo
+
+**Example Workflow and Output**
+
+
+
+---
+
+### Simple Skin Detection
+
+Description: Detects skin in images based on predefined color thresholds.
+
+Node Link: https://github.com/VeyDlin/simple-skin-detection-node
+
+View:
+
+
+
+---
+
+### Size Stepper Nodes
+
+**Description:** This is a set of nodes for calculating the necessary size increments for doing upscaling workflows. Use the *Final Size & Orientation* node to enter your full size dimensions and orientation (portrait/landscape/random), then plug that and your initial generation dimensions into the *Ideal Size Stepper* and get 1, 2, or 3 intermediate pairs of dimensions for upscaling. Note this does not output the initial size or full size dimensions: the 1, 2, or 3 outputs of this node are only the intermediate sizes.
+
+A third node is included, *Random Switch (Integers)*, which is just a generic version of Final Size with no orientation selection.
+
+**Node Link:** https://github.com/dwringer/size-stepper-nodes
+
+**Example Usage:**
+
+
+
+---
+
+### 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
+
+**Node Link:** https://github.com/mickr777/textfontimage
+
+**Output Examples**
+
+
+
+Results after using the depth controlnet
+
+
+
+
+
+---
+
+### 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:
+
+
+
+Highlights/Midtones/Shadows:
+
+
+
+
+
+Highlights/Midtones/Shadows (with LUT blur enabled):
+
+
+
+
+
+---
+
+### Unsharp Mask
+
+**Description:** Applies an unsharp mask filter to an image, preserving its alpha channel in the process.
+
+**Node Link:** https://github.com/JPPhoto/unsharp-mask-node
+
+---
+
+### XY Image to Grid and Images to Grids nodes
+
+**Description:** These nodes add the following to InvokeAI:
+- Generate grids of images from multiple input images
+- Create XY grid images with labels from parameters
+- Split images into overlapping tiles for processing (for super-resolution workflows)
+- Recombine image tiles into a single output image blending the seams
+
+The nodes include:
+1. `Images To Grids` - Combine multiple images into a grid of images
+2. `XYImage To Grid` - Take X & Y params and creates a labeled image grid.
+3. `XYImage Tiles` - Super-resolution (embiggen) style tiled resizing
+4. `Image Tot XYImages` - Takes an image and cuts it up into a number of columns and rows.
+5. Multiple supporting nodes - Helper nodes for data wrangling and building `XYImage` collections
+
+See full docs here: https://github.com/skunkworxdark/XYGrid_nodes/edit/main/README.md
+
+**Node Link:** https://github.com/skunkworxdark/XYGrid_nodes
+
+**Output Examples**
+
+
+
+---
+
+### Example Node Template
+
+**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
+
+**Example Workflow:** https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Prompt_from_File.json
+
+**Output Examples**
+
+
+
+
+## Disclaimer
+
+The nodes linked have been developed and contributed by members of the Invoke AI community. While we strive to ensure the quality and safety of these contributions, we do not guarantee the reliability or security of the nodes. If you have issues or concerns with any of the nodes below, please raise it on GitHub or in the Discord.
+
+
+## Help
+If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).
diff --git a/docs/src/content/docs/features/Workflows/custom-node-manager.mdx b/docs/src/content/docs/features/Workflows/custom-node-manager.mdx
new file mode 100644
index 00000000000..ab42b73e5a6
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/custom-node-manager.mdx
@@ -0,0 +1,76 @@
+---
+title: Custom Node Manager
+sidebar:
+ order: 5
+---
+
+import { Steps } from '@astrojs/starlight/components';
+
+The Custom Node Manager installs, updates, and removes community node packs directly from the InvokeAI UI — no manual file copying, no restart required.
+
+## Opening the Custom Node Manager
+
+Click the **Nodes** tab (circuit icon) in the left sidebar, between **Models** and **Queue**.
+
+The page is split into two panels:
+
+- **Left:** the list of installed node packs, with each pack's node count, type badges, and on-disk path.
+- **Right:** the install UI, with tabs for **Git Repository URL** and **Scan Folder**, plus an install log at the bottom.
+
+## Installing a node pack
+
+
+1. On the right panel, choose the **Git Repository URL** tab.
+2. Paste the Git URL of the pack, e.g. `https://github.com/user/my-node-pack.git`.
+3. Click **Install**.
+
+
+What happens during install:
+
+- The repo is cloned into your `nodes` directory.
+- The nodes are loaded into the running InvokeAI process immediately — **no restart needed**.
+- Any workflow `.json` files found in the repo are imported into your workflow library and tagged with `node-pack:` so you can filter for them.
+- The install log at the bottom of the panel shows the result for each step.
+
+:::caution[Security]
+Custom nodes execute arbitrary Python on your machine. **Only install node packs from authors you trust.** A malicious pack could harm your system or exfiltrate data.
+:::
+
+### Python dependencies
+
+The Custom Node Manager **does not** automatically run `pip install` for a pack's `requirements.txt` or `pyproject.toml`. Auto-installing into the running InvokeAI environment risks pulling in incompatible package versions and breaking the application.
+
+If a pack ships extra dependencies, you'll see a warning toast after installation. Install them yourself — typically `pip install -r requirements.txt` from inside an activated InvokeAI environment, but check the pack's README first. After installing, click **Reload** so the new dependencies take effect.
+
+## Managing installed packs
+
+Each entry in the left panel has actions for managing the pack:
+
+- **Reload** — re-scans the `nodes` directory. Use this after manually adding a pack via `git clone`, or after installing extra Python dependencies.
+- **Uninstall** — removes the pack from disk, unregisters its nodes from the running process, and removes any workflows that were imported from the pack. No restart needed.
+
+## Scan Folder tab
+
+The **Scan Folder** tab shows the path of your `nodes` directory. Anything placed there manually (for example, by `git clone`-ing a pack directly) is detected automatically at startup. Use **Reload** to pick up packs added at runtime.
+
+## Troubleshooting
+
+### Install fails
+
+- Confirm the Git URL is correct and reachable.
+- The repo must contain an `__init__.py` at its root.
+- Read the install log — it surfaces the underlying error.
+
+### Nodes don't appear after install
+
+- Click **Reload**.
+- Check that the pack's `__init__.py` imports the node classes.
+- Check the server console for import errors.
+
+### Workflows show errors after uninstalling
+
+User-created workflows that reference nodes from an uninstalled pack will show errors for the missing node types. Either reinstall the pack or remove the affected nodes from the workflow.
+
+## Authoring a node pack
+
+If you want to publish your own pack so it can be installed by URL, see the [Creating a Node Pack](/development/guides/creating-nodes/) developer guide for the required repository layout, `__init__.py` requirements, and conventions for shipping workflows alongside your nodes.
diff --git a/docs/src/content/docs/features/Workflows/editor-interface.mdx b/docs/src/content/docs/features/Workflows/editor-interface.mdx
new file mode 100644
index 00000000000..bce33485ad6
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/editor-interface.mdx
@@ -0,0 +1,141 @@
+---
+title: Editor Interface
+description: Learn how to use the Workflow Editor in InvokeAI.
+sidebar:
+ order: 2
+lastUpdated: 2026-02-20
+---
+
+import { Card, CardGrid, Steps, Tabs, TabItem } from '@astrojs/starlight/components';
+
+The workflow editor is a blank canvas allowing for the use of individual functions and image transformations to control the image generation workflow. Nodes take in inputs on the left side of the node, and return an output on the right side of the node.
+
+A node graph is composed of multiple nodes that are connected together to create a workflow. Nodes' inputs and outputs are connected by dragging connectors from node to node. Inputs and outputs are color-coded for ease of use.
+
+:::tip[New to Diffusion?]
+If you're not familiar with Diffusion, take a look at our [Diffusion Overview](../../concepts/diffusion). Understanding how diffusion works will enable you to more easily use the Workflow Editor and build workflows to suit your needs.
+:::
+
+## Features
+
+
+ Save workflows to the Invoke database, allowing you to easily create, modify, and share workflows as needed. A curated set of default workflows is provided to help explain important node usage.
+
+ 
+
+ The library has two views:
+
+ - **Browse Workflows** lists curated default workflows, filterable by a fixed set of category tags.
+ - **Your Workflows** lists workflows you have saved. The tag filter here is **dynamic** — it shows every unique tag found across your own workflows, with a count per tag.
+
+ Add comma-separated tags to a workflow (e.g. `portrait, SDXL, upscaling`) when saving it. The tags appear at the bottom of each workflow tile in the library and become selectable filters in the sidebar. Click one or more tags to narrow the list; click **Your Workflows** to clear the filter and show everything again. Tag counts update automatically when you create, edit, or delete a workflow.
+
+
+ Create a custom UI for your workflow, making it easier to iterate on your generations. The Linear UI View is saved alongside the workflow, allowing you to share workflows and enable others to use them.
+
+
+ 1. Right-click on any **input label** on a node.
+ 2. Select **"Add to Linear View"**.
+ 3. The input will now appear in your Linear View panel!
+
+
+ 
+
+
+ Any node or input field can be renamed in the workflow editor. If the input field you have renamed has been added to the Linear View, the changed name will be reflected in both places.
+
+
+ Nodes have a **"Use Cache"** option in their footer. This allows for performance improvements by reusing previously cached values during workflow processing.
+
+
+### Managing Nodes
+
+Use these quick keyboard shortcuts to navigate and manage your workflow efficiently:
+
+
+
+ Ctrl + C (or Cmd + C )
+
+
+ Ctrl + V (or Cmd + V )
+
+
+ Shift + Click & Drag
+
+
+ Backspace / Delete
+
+
+
+## Important Nodes & Concepts
+
+There are several node grouping concepts that can be examined with a narrow focus. These (and other) groupings can be pieced together to make up functional graph setups, and are important to understanding how groups of nodes work together as part of a whole.
+
+:::note
+The screenshots below aren't examples of complete functioning node graphs, but rather snippets demonstrating specific concepts.
+:::
+
+
+
+ ### Create Latent Noise
+ An initial noise tensor is necessary for the latent diffusion process. As a result, the Denoising node requires a noise node input.
+
+ The standard **Create Latent Noise** node now includes a **Noise Type** selector for architecture-specific latent
+ shapes. Leave it at **SD** for classic 4-channel Stable Diffusion workflows, or switch it to the architecture that
+ matches the downstream denoiser when working with models like FLUX, FLUX.2, SD3, CogView4, Z-Image, or Anima.
+
+ 
+
+ ### Text Prompt Conditioning
+ Conditioning is necessary for the latent diffusion process, whether empty or not. As a result, the Denoising node requires positive and negative conditioning inputs. Conditioning is reliant on a CLIP text encoder provided by the Model Loader node.
+
+ 
+
+
+
+ ### Image to Latents & VAE
+ The **ImageToLatents** node takes in a pixel image and a VAE and outputs latents. The **LatentsToImage** node does the opposite, taking in latents and a VAE and outputs a pixel image.
+
+ 
+
+ ### Scaling
+ Use the **ImageScale**, **ScaleLatents**, and **Upscale** nodes to upscale images and/or latent images. Upscaling is the process of enlarging an image and adding more detail.
+
+ The chosen method differs across contexts. However, be aware that latents are already noisy and compressed at their original resolution; scaling an image could produce more detailed results.
+
+ 
+
+
+
+ ### ControlNet
+ The **ControlNet** node outputs a Control, which can be provided as input to a Denoise Latents node. Depending on the type of ControlNet desired, ControlNet nodes usually require an image processor node, such as a Canny Processor or Depth Processor, which prepares an input image for use with ControlNet.
+
+ 
+
+ ### LoRA
+ The **Lora Loader** node lets you load a LoRA and pass it as output. A LoRA provides fine-tunes to the UNet and text encoder weights that augment the base model’s image and text vocabularies.
+
+ 
+
+
+
+ ### Defined & Random Seeds
+ It is common to want to use both the same seed (for continuity) and random seeds (for variety). To define a seed, simply enter it into the **'Seed'** field on a noise node. Conversely, the **RandomInt** node generates a random integer between 'Low' and 'High', and can be used as input to the 'Seed' edge point on a noise node to randomize your seed.
+
+ 
+
+ ### Iteration + Multiple Images as Input
+ Iteration is a common concept in any processing, and means to repeat a process with given input. In nodes, you're able to use the **Iterate** node to iterate through collections usually gathered by the **Collect** node.
+
+ The Iterate node has many potential uses, from processing a collection of images one after another, to varying seeds across multiple image generations and more. This screenshot demonstrates how to collect several images and use them in an image generation workflow.
+
+ 
+
+ ### Batch / Multiple Image Generation
+ Batch or multiple image generation in the workflow editor is done using the **RandomRange** node. In this case, the 'Size' field represents the number of images to generate, meaning this example will generate 4 images.
+
+ As RandomRange produces a collection of integers, we need to add the Iterate node to iterate through the collection. This noise can then be fed to the Denoise Latents node for it to iterate through the denoising process with the different seeds provided.
+
+ 
+
+
diff --git a/docs/src/content/docs/features/Workflows/face-tools.mdx b/docs/src/content/docs/features/Workflows/face-tools.mdx
new file mode 100644
index 00000000000..95959d158fd
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/face-tools.mdx
@@ -0,0 +1,94 @@
+---
+title: Face Tools Nodes
+---
+
+The Face Tools nodes detect faces with MediaPipe and provide utilities for identifying, masking, and extracting faces in workflows. The current nodes are in the `segmentation` category and use version `1.2.2`.
+
+## FaceIdentifier
+
+**FaceIdentifier** outputs a copy of the input image with detected face IDs printed on each face. Use it first when you need to target a specific face with FaceMask or FaceOff.
+
+Face IDs are numbered from `0`. Detection order can change if the image changes, so run FaceIdentifier again after editing an image.
+
+### Inputs
+
+| Input | Description |
+| --- | --- |
+| Image | Image to face detect. |
+| Minimum Confidence | Minimum confidence for face detection. Lower this if detection is failing. |
+| Chunk | Bypass full-image face detection and use chunking. Chunking is also used automatically if no faces are found in the full image. |
+
+### Outputs
+
+| Output | Description |
+| --- | --- |
+| Image | The input image with face ID numbers drawn on detected faces. |
+| Width | Output image width in pixels. |
+| Height | Output image height in pixels. |
+
+## FaceMask
+
+**FaceMask** creates a mask for detected faces on the input image.
+
+Leave **Face IDs** empty to mask all detected faces, or provide a comma-separated list such as `0,2,7` to target specific faces. Use FaceIdentifier to find the IDs.
+
+The mask can be adjusted with X and Y offsets if detection is slightly too large or small. Enable **Invert Mask** to affect everything except the detected faces.
+
+### Inputs
+
+| Input | Description |
+| --- | --- |
+| Image | Image to face detect. |
+| Face IDs | Comma-separated list of face IDs to mask. Leave empty to mask all detected faces. |
+| Minimum Confidence | Minimum confidence for face detection. Lower this if detection is failing. |
+| X Offset | Offset for the X-axis of the face mask. |
+| Y Offset | Offset for the Y-axis of the face mask. |
+| Chunk | Bypass full-image face detection and use chunking. Chunking is also used automatically if no faces are found in the full image. |
+| Invert Mask | Toggle to invert the mask. |
+
+### Outputs
+
+| Output | Description |
+| --- | --- |
+| Image | The original image, converted to RGBA. |
+| Width | Output image width in pixels. |
+| Height | Output image height in pixels. |
+| Mask | The generated face mask. |
+
+## FaceOff
+
+**FaceOff** extracts a single detected face into a bounded image and returns a matching mask plus paste coordinates. Use FaceIdentifier to find the face ID before targeting a specific face.
+
+Padding expands the bounding box around the detected face. This gives downstream processing more context and increases the bounded image size while keeping the face in place within the crop.
+
+### Inputs
+
+| Input | Description |
+| --- | --- |
+| Image | Image for face detection. |
+| Face ID | The face ID to process, numbered from `0`. Multiple faces are not supported. |
+| Minimum Confidence | Minimum confidence for face detection. Lower this 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 | Bypass full-image face detection and use chunking. Chunking is also used automatically if no faces are found in the full image. |
+
+### Outputs
+
+| Output | Description |
+| --- | --- |
+| Image | The bounded face image. If no face is found, the original image passes through. |
+| Width | Output image width in pixels. |
+| Height | Output image height in pixels. |
+| Mask | Mask matching the bounded image. |
+| X | X coordinate of the bounding box's left side. |
+| Y | Y coordinate of the bounding box's top side. |
+
+## Tips
+
+- Use the same **Minimum Confidence** value in FaceIdentifier and the FaceMask or FaceOff node that consumes the IDs.
+- Enable **Chunk** if not all target faces are detected. Full-image detection and chunked detection can produce different results.
+- Lower **Minimum Confidence** when detection fails, but watch for false positives.
+- Adjust X and Y offsets if the mask is too large, too small, or shifted.
+- Add FaceOff padding when the extracted face needs more surrounding context.
+- Face detection can fail on heavy face paint, hair covering the face, extreme angles, or other obstructions.
diff --git a/docs/src/content/docs/features/Workflows/index.mdx b/docs/src/content/docs/features/Workflows/index.mdx
new file mode 100644
index 00000000000..0ce8b49de82
--- /dev/null
+++ b/docs/src/content/docs/features/Workflows/index.mdx
@@ -0,0 +1,31 @@
+---
+title: Using Workflows
+sidebar:
+ order: 1
+---
+
+import { LinkCard, CardGrid } from '@astrojs/starlight/components';
+
+Workflows allow you to link multiple **Nodes** together to create custom, repeatable image generation processes. By connecting the outputs of some nodes to the inputs of others, you can build complex functionality tailored to your specific needs.
+
+## The Node Editor
+
+With nodes, you can easily extend the image generation capabilities of InvokeAI. All InvokeAI features are added through nodes.
+
+You can read more about nodes and how to use the node editor by checking out the detailed node documentation:
+
+
+
+## Downloading New Nodes
+
+To download a new node and enhance your workflows with new features, visit our list of Community Nodes. These are nodes that have been created by the community, for the community.
+
+
diff --git a/docs/src/content/docs/features/assets/board_settings.png b/docs/src/content/docs/features/assets/board_settings.png
new file mode 100644
index 00000000000..44c4ef240bd
Binary files /dev/null and b/docs/src/content/docs/features/assets/board_settings.png differ
diff --git a/docs/src/content/docs/features/assets/board_tabs.png b/docs/src/content/docs/features/assets/board_tabs.png
new file mode 100644
index 00000000000..23e5f8a91cf
Binary files /dev/null and b/docs/src/content/docs/features/assets/board_tabs.png differ
diff --git a/docs/src/content/docs/features/assets/board_thumbnails.png b/docs/src/content/docs/features/assets/board_thumbnails.png
new file mode 100644
index 00000000000..1c739d48546
Binary files /dev/null and b/docs/src/content/docs/features/assets/board_thumbnails.png differ
diff --git a/docs/src/content/docs/features/assets/gallery.png b/docs/src/content/docs/features/assets/gallery.png
new file mode 100644
index 00000000000..89f2dd1b463
Binary files /dev/null and b/docs/src/content/docs/features/assets/gallery.png differ
diff --git a/docs/src/content/docs/features/assets/image_menu.png b/docs/src/content/docs/features/assets/image_menu.png
new file mode 100644
index 00000000000..2f10f280acf
Binary files /dev/null and b/docs/src/content/docs/features/assets/image_menu.png differ
diff --git a/docs/src/content/docs/features/assets/info_button.png b/docs/src/content/docs/features/assets/info_button.png
new file mode 100644
index 00000000000..539cd6252e0
Binary files /dev/null and b/docs/src/content/docs/features/assets/info_button.png differ
diff --git a/docs/src/content/docs/features/assets/thumbnail_menu.png b/docs/src/content/docs/features/assets/thumbnail_menu.png
new file mode 100644
index 00000000000..a56caadbd8e
Binary files /dev/null and b/docs/src/content/docs/features/assets/thumbnail_menu.png differ
diff --git a/docs/src/content/docs/features/assets/top_controls.png b/docs/src/content/docs/features/assets/top_controls.png
new file mode 100644
index 00000000000..c5d3cdc854b
Binary files /dev/null and b/docs/src/content/docs/features/assets/top_controls.png differ
diff --git a/docs/src/content/docs/features/custom-node-manager.mdx b/docs/src/content/docs/features/custom-node-manager.mdx
new file mode 100644
index 00000000000..d91f4d82641
--- /dev/null
+++ b/docs/src/content/docs/features/custom-node-manager.mdx
@@ -0,0 +1,91 @@
+---
+title: Custom Node Manager
+lastUpdated: 2026-05-23
+sidebar:
+ order: 4
+---
+
+import { Steps } from '@astrojs/starlight/components'
+
+The Custom Node Manager allows you to install, manage, and remove community node packs directly from the InvokeAI UI — no manual file copying required.
+
+## Accessing the Node Manager
+
+Click the **Nodes** tab (circuit icon) in the left sidebar, between Models and Queue.
+
+## Installing a Node Pack
+
+
+ 1. Navigate to the **Nodes** tab
+ 2. On the right panel, select the **Git Repository URL** tab
+ 3. Paste the Git URL of the node pack (e.g. `https://github.com/user/my-node-pack.git`)
+ 4. Click **Install**
+
+
+The installer will:
+
+- Clone the repository into your `nodes` directory
+- Load the nodes immediately — no restart needed
+- Import any workflow `.json` files found in the repository into your workflow library (tagged with `node-pack:` for easy filtering)
+
+The install progress and results are shown in the **Install Log** at the bottom of the panel.
+
+### Installing Python Dependencies
+
+The installer does **not** automatically run `pip install` for `requirements.txt` or `pyproject.toml`. Auto-installing dependencies into the running InvokeAI environment can pull in incompatible package versions and break the application.
+
+If a node pack ships a `requirements.txt` or `pyproject.toml`, you'll see a warning toast after installation. Install the dependencies yourself by following the instructions in the node pack's documentation (typically `pip install -r requirements.txt` from inside an activated InvokeAI environment, but check the pack's README first). After installing, click the **Reload** button so the new dependencies take effect.
+
+### Security Warning
+
+Custom nodes execute arbitrary Python code on your system. **Only install node packs from authors you trust.** Malicious nodes could harm your system or compromise your data.
+
+## Managing Installed Nodes
+
+The left panel shows all installed node packs with:
+
+- **Pack name**
+- **Number of nodes** provided
+- **Individual node types** as badges
+- **File path** on disk
+
+### Reloading Nodes
+
+Click the **Reload** button to re-scan the nodes directory. This picks up any node packs that were manually added to the directory without using the installer.
+
+### Uninstalling a Node Pack
+
+Click the **Uninstall** button on any node pack. This will:
+
+- Remove the node pack directory
+- Unregister the nodes from the system immediately
+- Remove any workflows that were imported from the pack
+- Update the workflow editor so the nodes are no longer available
+
+No restart is required.
+
+## Scan Folder Tab
+
+The **Scan Folder** tab shows the location of your nodes directory. Node packs placed there manually (e.g. via `git clone`) are automatically detected at startup. Use the **Reload** button to detect newly added packs without restarting.
+
+## Troubleshooting
+
+### Node pack fails to install
+
+
+ 1. Verify the Git URL is correct and accessible
+ 2. Check that the repository contains an `__init__.py` file at the top level
+ 3. Review the Install Log for error details
+
+
+### Nodes don't appear after install
+
+
+ 1. Click the **Reload** button
+ 2. Check that the node pack's `__init__.py` imports its node classes
+ 3. Check the server console for error messages
+
+
+### Workflows show errors after uninstalling
+
+If you have user-created workflows that reference nodes from an uninstalled pack, those workflows will show errors for the missing node types. Reinstall the pack or remove the affected nodes from the workflow.
diff --git a/docs/src/content/docs/features/gallery.mdx b/docs/src/content/docs/features/gallery.mdx
new file mode 100644
index 00000000000..b48f3c19176
--- /dev/null
+++ b/docs/src/content/docs/features/gallery.mdx
@@ -0,0 +1,139 @@
+---
+title: Gallery Panel
+description: Learn how to manage, organize, and use your generated images and assets with the Gallery Panel in InvokeAI.
+lastUpdated: 2026-02-19
+sidebar:
+ order: 1
+---
+
+import { Card, CardGrid, Steps } from '@astrojs/starlight/components';
+
+The Gallery Panel is a fast way to review, find, and make use of images you've generated and loaded. The Gallery is divided into **Boards**. The *Uncategorized* board is always present, but you can create your own for better organization.
+
+
+
+---
+
+## Board Display and Settings
+
+At the very top of the Gallery Panel, you will find the board disclosure and settings buttons.
+
+
+
+The **disclosure button** shows the name of the currently selected board and allows you to toggle the visibility of the board thumbnails.
+
+
+
+The **settings button** opens a list of customization options:
+
+
+
+- **Image Size:** A slider that lets you control the size of the image previews in the gallery.
+- **Auto-Switch to New Images:** When enabled, newly generated images will automatically load into the current image panel (on the Text to Image tab) or the result panel (on the Image to Image tab). This happens invisibly even if you are on a different tab during generation.
+- **Auto-Assign Board on Click:** Whenever an image is generated or saved, it is placed into a board. The destination board is marked with an `AUTO` badge.
+ - *When enabled:* The board selected at the moment you click **Invoke** becomes the destination. This allows you to queue multiple generations into different boards without waiting for them to finish.
+ - *When disabled:* An **Auto-Add Board** dropdown appears, allowing you to set one specific board as the permanent destination for all new images.
+- **Always Show Image Size Badge:** Toggles whether the resolution (e.g., 512x512) is displayed on each image preview thumbnail.
+
+Below these buttons is the **Search Boards** text entry area, allowing you to quickly find specific boards by name. Next to it is the **Add Board (+)** button for creating new boards.
+
+:::tip
+You can rename any board by simply clicking on its name under the thumbnail and typing the new name.
+:::
+
+---
+
+## Board Management
+
+Each board has a context menu accessible via right-click (or Ctrl+click).
+
+
+
+- **Auto-add to this Board:** If *Auto-Assign Board on Click* is disabled in settings, use this option to quickly set the selected board as the default destination for new images.
+- **Download Board:** Packages all images within the board into a `.zip` file. A notification link will be provided when the download is ready.
+- **Delete Board:** Permanently removes the board and all of its contents.
+
+:::danger
+Deleting a board will **permanently delete all images** contained within it. Proceed with caution!
+:::
+
+### Board Contents
+
+Every board is organized into two distinct tabs:
+
+
+
+1. **Images:** Images generated directly within InvokeAI.
+2. **Assets:** External images you have uploaded to use as an [Image Prompt](https://support.invoke.ai/support/solutions/articles/151000159340-using-the-image-prompt-adapter-ip-adapter-) or within the Image to Image tab.
+
+---
+
+## Virtual Boards
+
+Virtual boards are read-only board groupings that Invoke computes on-the-fly from your image metadata rather than storing in the database. The first available type groups images **By Date**, creating one sub-board per day on which you generated images.
+
+Virtual boards are **off by default**. To enable them:
+
+1. Open the **board settings** (gear icon at the top of the Gallery).
+2. Toggle **Virtual Boards** on.
+3. A collapsible **By Date** section appears in the board list, with a sub-board for each day that has images. Each sub-board shows the date, image / asset counts, and a cover thumbnail.
+
+Selecting a date sub-board filters the gallery to just the images from that day. The collapse state of the By Date section persists across reloads.
+
+### Limits
+
+Because virtual boards are derived, not stored:
+
+- They are **read-only**: no drag-and-drop, no context menu, no auto-add destination.
+- You cannot rename or delete them.
+- Generating a new image updates the counts immediately, but the image is still saved to your regular auto-add board — virtual boards are a *view*, not a destination.
+- Disabling the **Virtual Boards** toggle hides the section and resets the selection to *Uncategorized* if you were viewing a virtual sub-board.
+
+---
+
+## Image Interaction
+
+Every image generated by InvokeAI stores its generation metadata (prompt, seed, models, etc.) directly inside the file. You can read this data by selecting the image and clicking the **Info button**  in any result panel.
+
+Additionally, each image has a context menu (right-click or Ctrl+click) with powerful workflow actions:
+
+
+
+*Options marked with an asterisk (\*) require the image to have generation metadata.*
+
+
+
+ - **Open in New Tab:** Opens the image in a separate browser tab.
+ - **Download Image:** Saves the image to your local device.
+ - **Star Image:** Pins the image to the top of the gallery. *(Also available by clicking the star icon on hover).*
+
+
+ - **Load Workflow*:** Loads the saved workflow settings into the Workflow tab and opens it.
+ - **Remix Image*:** Loads all generation settings (**excluding** the Seed) into the control panel.
+ - **Use Prompt*:** Loads only the text prompts.
+ - **Use Seed*:** Loads only the Seed.
+ - **Use All*:** Loads all generation settings into the control panel.
+
+
+ - **Send to Image to Image:** Moves the image to the left-hand panel of the Image to Image tab.
+ - **Send to Unified Canvas:** **Replaces** the current Unified Canvas contents with this image.
+
+
+ - **Change Board:** Opens a prompt to move the image. *(You can also drag and drop images onto board thumbnails).*
+ - **Delete Image:** Permanently deletes the image from InvokeAI.
+
+
+
+:::caution
+ Selecting **Delete Image** will remove the image entirely from your InvokeAI installation. This action cannot be undone.
+:::
+
+---
+
+## Summary
+
+This walkthrough covers the Gallery interface and Boards. For guidance on prompting and generation workflows, please refer to the [Prompting Guide](/concepts/prompting-guide/) and [AI Image Generation](/concepts/image-generation/).
+
+## Acknowledgements
+
+A huge shout-out to the core team working to make the Web GUI a reality, including [psychedelicious](https://github.com/psychedelicious), [Kyle0654](https://github.com/Kyle0654), and [blessedcoolant](https://github.com/blessedcoolant). [hipsterusername](https://github.com/hipsterusername) was the team's unofficial cheerleader and added tooltips/docs.
diff --git a/docs/src/content/docs/features/hotkeys.mdx b/docs/src/content/docs/features/hotkeys.mdx
new file mode 100644
index 00000000000..a4b99fca16d
--- /dev/null
+++ b/docs/src/content/docs/features/hotkeys.mdx
@@ -0,0 +1,202 @@
+---
+title: Hotkeys
+description: Learn how to use and customize hotkeys in InvokeAI, and how developers can interact with the hotkey system.
+lastUpdated: 2026-02-19
+sidebar:
+ order: 2
+---
+
+import { Tabs, TabItem, Steps, Card, CardGrid, Icon } from '@astrojs/starlight/components';
+
+InvokeAI allows you to customize all keyboard shortcuts (hotkeys) to match your workflow preferences. This guide covers how to use and customize hotkeys as a user, as well as providing technical documentation for developers.
+
+## User Guide
+
+
+
+ See all available keyboard shortcuts organized by category in one place.
+
+
+ Change any shortcut to your preference, or assign multiple key combinations to the same action.
+
+
+ Built-in validation prevents invalid combinations.
+
+
+ Your custom hotkeys are safely stored and restored across sessions.
+
+
+
+### Opening the Hotkeys Modal
+
+Press Shift + ? or click the **keyboard icon** in the application to open the Hotkeys Modal.
+
+### Managing Hotkeys
+
+
+
+ - Browse all available hotkeys organized by category (App, Canvas, Gallery, Workflows, etc.).
+ - Search for specific hotkeys using the search bar.
+ - See the current key combination for each action.
+
+
+
+ 1. Click the **pencil** button by the hotkey you want to change, or click the **plus** button to add a new one.
+ 2. Enter your new hotkey combination in the editor.
+ - Use modifier buttons for quick-insert (Mod , Ctrl , Shift , Alt ).
+ - Check the live preview to see how your hotkey will look.
+ 3. Click the **checkmark** or press Enter to save.
+
+
+
+ - **Reset a single hotkey:** Click the counter-clockwise arrow icon next to customized hotkeys.
+ - **Reset all hotkeys:** In Edit Mode, click the **Reset All to Default** button at the bottom.
+
+
+
+### Hotkey Format Reference
+
+When customizing hotkeys, use the following formats:
+
+- **Valid Modifiers:** `mod` (Ctrl on Windows/Linux, Cmd on Mac), `ctrl`, `shift`, `alt`
+- **Valid Keys:** Letters (`a-z`), Numbers (`0-9`), Function keys (`f1-f12`), Special keys (`enter`, `space`, `tab`, `backspace`, `delete`, `escape`), Arrow keys (`up`, `down`, `left`, `right`)
+- **Multiple alternatives:** Separate with commas (e.g., `mod+enter, ctrl+enter`)
+
+:::tip
+ **Valid Hotkeys:** `mod+s`, `ctrl+shift+p`, `f5, mod+r`
+ **Invalid Hotkeys:** `mod+` (no key after modifier), `shift+ctrl+` (ends with modifier)
+:::
+
+---
+
+## Developer Guide
+
+The hotkeys system allows developers to centrally define, manage, and validate hotkeys throughout the application. It is built on top of `react-hotkeys-hook`.
+
+### Architecture
+
+The customizable hotkeys feature comprises the following components:
+
+
+
+ **Hotkeys State Slice (`hotkeysSlice.ts`)**
+ - Stores custom hotkey mappings in Redux state.
+ - Persisted to IndexedDB using `redux-remember`.
+ - Provides actions to change, reset individual, or reset all hotkeys.
+
+
+ **`useHotkeyData` Hook (`useHotkeyData.ts`)**
+ - Defines all default hotkeys and merges them with custom hotkeys from the store.
+ - Returns the effective hotkeys to be used.
+ - Provides platform-specific key translations.
+
+
+ - **`HotkeyEditor.tsx`**: Inline editor with live preview, validation, and modifier insertion.
+ - **`HotkeysModal.tsx`**: The modal interface supporting View/Edit modes, searching, and categories.
+
+
+
+### Adding New Hotkeys
+
+To add a new hotkey to the system, follow these steps:
+
+
+1. **Add Translation Strings**
+ In `invokeai/frontend/web/public/locales/en.json`:
+ ```json
+ {
+ "hotkeys": {
+ "app": {
+ "myAction": {
+ "title": "My Action",
+ "desc": "Description of what this hotkey does"
+ }
+ }
+ }
+ }
+ ```
+
+2. **Register the Hotkey**
+ In `invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts`:
+ ```typescript
+ // Inside the appropriate category builder function
+ addHotkey('app', 'myAction', ['mod+k']); // Default binding
+ ```
+
+3. **Use the Hotkey in Components**
+ ```tsx
+ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
+
+ const MyComponent = () => {
+ const handleAction = useCallback(() => {
+ // Your action here
+ }, []);
+
+ // Automatically uses custom hotkeys if configured
+ useRegisteredHotkeys({
+ id: 'myAction',
+ category: 'app', // 'app', 'canvas', 'viewer', 'gallery', 'workflows'
+ callback: handleAction,
+ options: { enabled: true, preventDefault: true },
+ dependencies: [handleAction]
+ });
+
+ // ...
+ };
+ ```
+
+
+### Common Patterns
+
+
+
+ Only enable hotkeys when certain conditions are met:
+ ```typescript
+ useRegisteredHotkeys({
+ id: 'save',
+ category: 'app',
+ callback: handleSave,
+ options: {
+ enabled: hasUnsavedChanges && !isLoading, // Only when valid
+ preventDefault: true
+ },
+ dependencies: [hasUnsavedChanges, isLoading, handleSave]
+ });
+ ```
+
+
+ Ensure hotkeys are only active when a specific region is focused:
+ ```tsx
+ import { useFocusRegion } from 'common/hooks/focus';
+
+ const MyComponent = () => {
+ const focusRegionRef = useFocusRegion('myRegion');
+
+ // Hotkey only works when this region has focus
+ useRegisteredHotkeys({
+ id: 'myAction',
+ category: 'app',
+ callback: handleAction,
+ options: { enabled: true }
+ });
+
+ return ...
;
+ };
+ ```
+
+
+ Provide multiple alternatives for the same action:
+ ```typescript
+ // In useHotkeyData.ts
+ addHotkey('canvas', 'redo', ['mod+shift+z', 'mod+y']); // Two alternatives
+ ```
+
+
+
+:::caution[Best Practices]
+- **Use `mod` instead of `ctrl`**: Automatically maps to Cmd on Mac, Ctrl elsewhere.
+- **Provide descriptive translations**: Help users understand what each hotkey does.
+- **Avoid conflicts**: Check existing hotkeys before adding new ones.
+- **Check enabled state**: Only activate hotkeys when the action is available.
+- **Use dependencies correctly**: Ensure callbacks are stable with `useCallback`.
+:::
diff --git a/docs/src/content/docs/features/prompt-tools.md b/docs/src/content/docs/features/prompt-tools.md
new file mode 100644
index 00000000000..45bfd96170a
--- /dev/null
+++ b/docs/src/content/docs/features/prompt-tools.md
@@ -0,0 +1,55 @@
+---
+title: LLM Prompt Tools
+sidebar:
+ order: 3
+lastUpdated: 2026-05-23
+---
+
+InvokeAI includes two built-in tools that use local language models to help you write better prompts. Both tools appear as small buttons in the top-right corner of the positive prompt area and are only visible when you have a compatible model installed.
+
+## Expand Prompt
+
+Takes your short prompt and expands it into a detailed, vivid description suitable for image generation.
+
+**How to use:**
+
+1. Type a brief prompt (e.g. "a cat in a garden")
+2. Click the sparkle button in the prompt area
+3. Select a Text LLM model from the dropdown
+4. Click **Expand**
+5. Your prompt is replaced with the expanded version
+
+**Compatible models:** Any HuggingFace model with a `ForCausalLM` architecture. Recommended options:
+
+| Model | Size | HuggingFace ID |
+|-------|------|----------------|
+| Qwen2.5 1.5B Instruct | ~3 GB | `Qwen/Qwen2.5-1.5B-Instruct` |
+| Phi-3 Mini Instruct | ~7.5 GB | `microsoft/Phi-3-mini-4k-instruct` |
+| TinyLlama Chat | ~2 GB | `TinyLlama/TinyLlama-1.1B-Chat-v1.0` |
+
+Install by pasting the HuggingFace ID into the Model Manager. The model is automatically detected as a **Text LLM** type.
+
+## Image to Prompt
+
+Upload an image and generate a descriptive prompt from it using a vision-language model.
+
+**How to use:**
+
+1. Click the image button in the prompt area
+2. Select a LLaVA OneVision model from the dropdown
+3. Click **Upload Image** and select an image
+4. Click **Generate Prompt**
+5. The generated description is set as your prompt
+
+**Compatible models:** LLaVA OneVision models (already supported by InvokeAI).
+
+## Undo
+
+Both tools overwrite your current prompt. You can undo this change:
+
+- Press **Ctrl+Z** (or **Cmd+Z** on macOS) in the prompt textarea within 30 seconds
+- The undo state is cleared when you start typing manually
+
+## Workflow Node
+
+A **Text LLM** node is also available in the workflow editor for use in automated pipelines. It accepts a prompt string and model selection as inputs and outputs the expanded text as a string.
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
new file mode 100644
index 00000000000..52de38f6faf
--- /dev/null
+++ b/docs/src/content/docs/index.mdx
@@ -0,0 +1,127 @@
+---
+title: AI Image Generation for Creatives
+description: A leading creative engine built to empower professionals and enthusiasts alike.
+template: splash
+hero:
+ title: AI Image Generation for Creatives
+ tagline: Invoke is a free and open-source creative engine for AI-powered image generation. Built by creatives, for creatives. Self-hosted, fully customizable, and Apache 2.0 licensed.
+ actions:
+ - text: Get Started
+ link: start-here/installation
+ icon: right-arrow
+ variant: primary
+ - text: View on GitHub
+ link: https://github.com/invoke-ai/InvokeAI
+ icon: github
+ variant: minimal
+---
+
+import { Image } from 'astro:assets'
+import { Card, CardGrid, LinkButton } from '@astrojs/starlight/components';
+import DownloadOptions from '@components/DownloadOptions.astro';
+
+import splashImage from './assets/invoke-webui-canvas.png';
+
+
+
+
+
+## The Creative Engine
+
+Invoke provides the most feature-complete and professional toolkit for AI image generation, built with production workflows in mind.
+
+
+
+ Experience true **Layer-based Canvas Editing**. Invoke's powerful canvas allows you to draw, paint, sketch, and edit your creations with unhindered precision. Each layer can be independently manipulated—giving you the freedom to compose intricate scenes seamlessly without breaking a sweat.
+
+
+ Unlock limitless possibilities with **Advanced Node-based Workflows**. Build your own complex, reproducible pipelines via a completely visual graph backend. Expose custom UI parameters to share and update values easily without diving deep into the graph.
+
+
+ Stay on the cutting edge with out-of-the-box support for the latest foundational models, including **Flux, SDXL, SD 1.5**, and more. Manage checkpoints, LoRAs, Textual Inversions, and ControlNets with an intuitive Model Manager.
+
+
+ **Completely Local & Self-hosted**. Invoke runs locally on your own hardware. Your data, prompts, and creations belong entirely to you. Say goodbye to restrictive cloud services and privacy concerns—maintain absolute control over your art.
+
+
+
+---
+
+## Built for Production
+
+Invoke is designed to keep your creative flow moving. Unlike other tools that feel like engineering experiments, Invoke is a polished, professional-grade application.
+
+
+
+ A beautiful, clean interface that prioritizes your artwork. No cluttered menus—just the tools you need right where you expect them.
+
+
+ Extensive ControlNet implementation allows you to guide generations with depth maps, edges, poses, and more for exact composition control.
+
+
+ Rapidly iterate on concepts with batch generation, prompt wildcards, and high-resolution upscaling, all without leaving the app.
+
+
+ Actively developed by a passionate open-source community. Jump into the conversation, report bugs, or request features directly.
+
+
+
+---
+
+## Join the Ecosystem
+
+Whether you are looking to install the app, get support, train your own models, or contribute to the project, the Invoke community has you covered.
+
+
+
+ Ready to dive in? The [Invoke Launcher](/start-here/installation/) is the fastest way to get up and running on Windows, macOS, and Linux. For advanced setups, try [Docker](/configuration/docker/) or a [manual Python installation](/start-here/manual/).
+
+
+ Get Invoke
+
+
+
+
+ Want to train models on your own style? Invoke Training provides a dedicated UI for **Textual Inversion** and **LoRA training**.
+
+
+ Explore Invoke Training
+
+
+
+
+ Stuck? Check out our comprehensive [FAQ](/troubleshooting/faq/) for quick answers. If you still need a hand, our community is incredibly active and helpful.
+
+ Join our Discord
+
+
+
+ Invoke is open-source software made possible by [people across the world](/contributing/contributors/). We welcome code, documentation, and design contributions of any size! Read our [contributing guide](/contributing/) to start.
+
+
+ Read Contribution Guide
+
+
+
+
+---
+
+## Download Invoke
+
+Ready to unleash your creativity? Invoke is available for Windows, macOS, and Linux. Self-hosted, fully customizable, and Apache 2.0 licensed.
+
+
+
+---
+
+:::note[About the Hosted Version]
+The Invoke hosted platform has been shut down as the founding team joined Adobe. However, Invoke lives on as a thriving open-source project maintained by the community.
+
+The open-source version offers the same powerful features you may have used in the hosted service, with the added benefit of complete control and privacy through self-hosting.
+
+Stewardship of the project has been passed to Lincoln Stein (lstein) and Vic (Blessedcoolant), who have been core maintainers since the project's inception and continue to drive development forward with the community.
+:::
diff --git a/docs/src/content/docs/start-here/installation.mdx b/docs/src/content/docs/start-here/installation.mdx
new file mode 100644
index 00000000000..02e2680db66
--- /dev/null
+++ b/docs/src/content/docs/start-here/installation.mdx
@@ -0,0 +1,89 @@
+---
+title: Simple Installation
+lastUpdated: 2026-02-18
+---
+
+import { LinkCard, Tabs, TabItem, Steps } from '@astrojs/starlight/components'
+import SystemRequirementsLink from '@components/SystemRequirmentsLink.astro'
+
+export const alternateLaunchers = [
+ {
+ title: 'Stability Matrix',
+ description: 'Get the latest version of Stability Matrix for your platform.',
+ href: 'https://github.com/LykosAI/StabilityMatrix'
+ },
+ {
+ title: 'LynxHub',
+ description: 'Get the latest version of LynxHub for your platform.',
+ href: 'https://github.com/KindaBrazy/LynxHub'
+ },
+]
+
+
+
+## Invoke Launcher
+
+The Invoke launcher is the official launcher to install, update and manage your invoke installation.
+
+### Download and Set Up the Launcher
+
+The Launcher manages your Invoke install. Follow these instructions to download and set up the Launcher.
+
+
+
+
+ 1. [Download for Windows]
+ 2. Run the `EXE` to install the Launcher and start it.
+ 3. A desktop shortcut will be created; use this to run the Launcher in the future.
+ 4. You can delete the `EXE` file you downloaded.
+
+
+
+
+ 1. [Download for MacOS]
+ 2. Open the `DMG` and drag the app into `Applications`.
+ 3. Run the launcher from `Applications`.
+ 4. You can delete the `DMG` file you downloaded.
+
+
+
+
+ 1. [Download for Linux]
+ 2. You may need to edit the `AppImage` file properties and make it executable.
+ 3. Optionally move the file to a location that does not require admin privileges and add a desktop shortcut for it.
+ 4. Run the Launcher by double-clicking the `AppImage` or the shortcut you made.
+
+
+
+
+### Install Invoke
+
+Run the Launcher you just set up if you haven't already. Click **Install** and follow the instructions to install (or update) Invoke.
+
+If you have an existing Invoke installation, you can select it and let the launcher manage the install. You'll be able to update or launch the installation.
+
+### Updating
+
+The Launcher will check for updates for itself _and_ Invoke.
+
+When the Launcher detects an update is available for itself, you'll get a small popup window. Click through this and the Launcher will update itself.
+
+When the Launcher detects an update for Invoke, you'll see a small green alert in the Launcher. Click that and follow the instructions to update Invoke.
+
+## Alternative Launchers
+
+:::caution
+ Installations from alternate launchers are not managed by Invoke, so we cannot guarantee it will work correctly. If you want a more stable experience, we recommend using the [official Invoke Launcher](#invoke-launcher).
+:::
+
+{alternateLaunchers.map(({title, description, href}) => (
+
+))}
+
+[Download for Windows]: https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe
+[Download for MacOS]: https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg
+[Download for Linux]: https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage
diff --git a/docs/src/content/docs/start-here/manual.mdx b/docs/src/content/docs/start-here/manual.mdx
new file mode 100644
index 00000000000..fff6ee58774
--- /dev/null
+++ b/docs/src/content/docs/start-here/manual.mdx
@@ -0,0 +1,193 @@
+---
+title: Manual Installation
+lastUpdated: 2026-02-18
+---
+
+import { LinkCard, Tabs, TabItem, Steps, LinkButton } from '@astrojs/starlight/components'
+import SystemRequirementsLink from '@components/SystemRequirmentsLink.astro'
+
+
+
+## Are you in the right place?
+
+
+
+
+
+## Walkthrough
+
+We'll use [`uv`](https://github.com/astral-sh/uv) to install python and create a virtual environment, then install the `invokeai` package. `uv` is a modern, very fast alternative to `pip`.
+
+The following commands vary depending on the version of Invoke being installed and the system onto which it is being installed.
+
+
+
+ 1. Install `uv` as described in its [docs](https://docs.astral.sh/uv/getting-started/installation/#standalone-installer). We suggest using the standalone installer method.
+
+ Run `uv --version` to confirm that `uv` is installed and working. After installation, you may need to restart your terminal to get access to `uv`.
+
+ 2. Create a directory for your installation, typically in your home directory (e.g. `~/invokeai` or `$Home/invokeai`):
+
+
+
+ ```ps
+ mkdir $Home/invokeai
+ cd $Home/invokeai
+ ```
+
+
+ ```bash
+ mkdir ~/invokeai
+ cd ~/invokeai
+ ```
+
+
+
+ 3. Create a virtual environment in that directory:
+
+ ```sh
+ uv venv --relocatable --prompt invoke --python 3.12 --python-preference only-managed .venv
+ ```
+
+ This command creates a portable virtual environment at `.venv` complete with a portable python 3.12. It doesn't matter if your system has no python installed, or has a different version - `uv` will handle everything.
+
+ 4. Activate the virtual environment:
+
+
+
+ ```ps
+ .venv\Scripts\activate
+ ```
+
+
+ ```bash
+ source .venv/bin/activate
+ ```
+
+
+
+ 5. Choose a version to install.
+
+
+ View Releases
+
+
+ 6. Determine the package specifier to use when installing. This is a performance optimization.
+
+ - If you have an Nvidia 20xx series GPU or older, use `invokeai[xformers]`.
+ - If you have an Nvidia 30xx series GPU or newer, or do not have an Nvidia GPU, use `invokeai`.
+
+ 7. Determine the torch backend to use for installation, if any. This is necessary to get the right version of torch installed. This is acheived by using [UV's built in torch support.](https://docs.astral.sh/uv/guides/integration/pytorch/#automatic-backend-selection)
+
+ :::note[Torch Backend Selection]
+ Pick a torch backend only when it applies to your system. In all other cases, do not use a torch backend.
+ :::
+
+
+
+
+
+ Use:
+ ```sh
+ --torch-backend=cu128
+ ```
+
+
+ Do not use a torch backend.
+
+
+
+
+
+
+
+ Use:
+ ```sh
+ --torch-backend=cu128
+ ```
+
+
+ Use:
+ ```sh
+ --torch-backend=cpu
+ ```
+
+
+ Use:
+ ```sh
+ --torch-backend=rocm6.3
+ ```
+
+
+ Do not use a torch backend.
+
+
+
+
+
+ 8. Install the `invokeai` package. Substitute the package specifier and version.
+
+
+
+ ```sh
+ uv pip install == --python 3.12 --python-preference only-managed --force-reinstall
+ ```
+
+
+ ```sh
+ uv pip install == --python 3.12 --python-preference only-managed --torch-backend= --force-reinstall
+ ```
+
+
+
+
+ 9. Deactivate and reactivate your venv so that the invokeai-specific commands become available in the environment:
+
+
+
+ ```ps
+ deactivate
+ .venv\Scripts\activate
+ ```
+
+
+ ```bash
+ deactivate && source .venv/bin/activate
+ ```
+
+
+
+ 10. Run the application, specifying the directory you created earlier as the root directory:
+
+
+
+ ```ps
+ invokeai-web --root ~/invokeai
+ ```
+
+
+ ```bash
+ invokeai-web --root $Home/invokeai
+ ```
+
+
+
+
+If you run Invoke on a headless server, you might want to install and run Invoke on the command line.
+
+We do not plan to maintain scripts to do this moving forward, instead focusing our dev resources on the GUI [launcher](../installation).
+
+You can create your own scripts for this by copying the handful of commands in this guide. `uv`'s [`pip` interface docs](https://docs.astral.sh/uv/reference/cli/#uv-pip-install) may be useful.
diff --git a/docs/src/content/docs/start-here/system-requirements.mdx b/docs/src/content/docs/start-here/system-requirements.mdx
new file mode 100644
index 00000000000..114698ce158
--- /dev/null
+++ b/docs/src/content/docs/start-here/system-requirements.mdx
@@ -0,0 +1,139 @@
+---
+title: Hardware Requirements
+sidebar:
+ order: 1
+lastUpdated: 2026-02-18
+---
+
+import { Tabs, TabItem, Steps } from '@astrojs/starlight/components'
+
+Invoke runs on Windows 10+, macOS 14+ and Linux (Ubuntu 20.04+ is well-tested).
+
+## Hardware
+
+Hardware requirements vary significantly depending on model and image output size.
+
+The requirements below are rough guidelines for best performance. GPUs with less VRAM typically still work, if a bit slower. Follow the [Low VRAM Guide] to optimize performance.
+
+- All Apple Silicon (M1, M2, etc) Macs work, but 16GB+ memory is recommended.
+- AMD GPUs are supported on Linux only. The VRAM requirements are the same as Nvidia GPUs.
+
+### Windows/Linux
+
+| Model Family | Best resolution | GPU (series) | VRAM (min) | RAM (min) | Notes |
+|---|---:|---|---:|---:|---|
+| SD1.5 | 512x512 | Nvidia 10xx+ | 4GB | 8GB | |
+| SDXL | 1024x1024 | Nvidia 20xx+ | 8GB | 16GB | |
+| FLUX.1 | 1024x1024 | Nvidia 20xx+ | 10GB | 32GB | |
+| FLUX.2 Klein 4B | 1024x1024 | Nvidia 30xx+ | 12GB | 16GB | FP8 works with 8GB+; Diffusers + encoder |
+| FLUX.2 Klein 9B | 1024x1024 | Nvidia 40xx | 24GB | 32GB | FP8 works with 12GB+; Diffusers + encoder |
+| Z-Image Turbo | 1024x1024 | Nvidia 20xx+ | 8GB | 16GB | Q4_K 8GB; Q8/BF16 16GB+ |
+
+:::tip[`tmpfs` on Linux]
+ If your temporary directory is mounted as a `tmpfs`, ensure it has sufficient space.
+:::
+
+## Python
+
+:::tip[The launcher installs python for you]
+ You don't need to do this if you are installing with the [Invoke Launcher](../installation).
+:::
+
+Invoke requires python `3.11` through `3.12`. If you don't already have one of these versions installed, we suggest installing `3.12`, as it will be supported for longer.
+
+Check that your system has an up-to-date Python installed by running `python3 --version` in the terminal (Linux, macOS) or cmd/powershell (Windows).
+
+:::tip[Installing Python]{icon="seti:python"}
+
+
+
+ 1. Install python with [an official installer].
+ 2. The installer includes an option to add python to your PATH. Be sure to enable this. If you missed it, re-run the installer, choose to modify an existing installation, and tick that checkbox.
+ 3. You may need to install [Microsoft Visual C++ Redistributable].
+
+
+
+
+ 1. Install python with [an official installer].
+ 2. If model installs fail with a certificate error, you may need to run this command (changing the python version to match what you have installed): `/Applications/Python\ 3.11/Install\ Certificates.command`
+ 3. If you haven't already, you will need to install the XCode CLI Tools by running `xcode-select --install` in a terminal.
+
+
+
+
+ 1. Installing python varies depending on your system. We recommend [using `uv` to manage your python installation].
+ 2. You'll need to install `libglib2.0-0` and `libgl1-mesa-glx` for OpenCV to work. For example, on a Debian system: `sudo apt update && sudo apt install -y libglib2.0-0 libgl1-mesa-glx`
+
+
+
+:::
+
+## Drivers
+
+If you have an Nvidia or AMD GPU, you may need to manually install drivers or other support packages for things to work well or at all.
+
+### Nvidia
+
+Run `nvidia-smi` on your system's command line to verify that drivers and CUDA are installed. If this command fails, or doesn't report versions, you will need to install drivers.
+
+Go to the [CUDA Toolkit Downloads] and carefully follow the instructions for your system to get everything installed.
+
+Confirm that `nvidia-smi` displays driver and CUDA versions after installation.
+
+#### Linux - via Nvidia Container Runtime
+
+An alternative to installing CUDA locally is to use the [Nvidia Container Runtime] to run the application in a container.
+
+#### Windows - Nvidia cuDNN DLLs
+
+An out-of-date cuDNN library can greatly hamper performance on 30-series and 40-series cards. Check with the community on discord to compare your `it/s` if you think you may need this fix.
+
+First, locate the destination for the DLL files and make a quick back up:
+
+1. Find your InvokeAI installation folder, e.g. `C:\Users\Username\InvokeAI\`.
+1. Open the `.venv` folder, e.g. `C:\Users\Username\InvokeAI\.venv` (you may need to show hidden files to see it).
+1. Navigate deeper to the `torch` package, e.g. `C:\Users\Username\InvokeAI\.venv\Lib\site-packages\torch`.
+1. Copy the `lib` folder inside `torch` and back it up somewhere.
+
+Next, download and copy the updated cuDNN DLLs:
+
+1. Go to the [Cuda Docs].
+1. Create an account if needed and log in.
+1. Choose the newest version of cuDNN that works with your GPU architecture. Consult the [cuDNN support matrix] to determine the correct version for your GPU.
+1. Download the latest version and extract it.
+1. Find the `bin` folder, e.g. `cudnn-windows-x86_64-SOME_VERSION\bin`.
+1. Copy and paste the `.dll` files into the `lib` folder you located earlier. Replace files when prompted.
+
+If, after restarting the app, this doesn't improve your performance, either restore your back up or re-run the installer to reset `torch` back to its original state.
+
+### AMD
+
+:::tip[Linux Only]{icon="linux"}
+ AMD GPUs are supported on Linux only, due to ROCm (the AMD equivalent of CUDA) support being Linux only.
+:::
+
+:::caution[Bumps Ahead]
+ While the application does run on AMD GPUs, there are occasional bumps related to spotty torch support.
+:::
+
+Run `rocm-smi` on your system's command line verify that drivers and ROCm are installed. If this command fails, or doesn't report versions, you will need to install them.
+
+Go to the [ROCm Documentation] and carefully follow the instructions for your system to get everything installed.
+
+Confirm that `rocm-smi` displays driver and CUDA versions after installation.
+
+#### Linux - via Docker Container
+
+An alternative to installing ROCm locally is to use a [ROCm docker container] to run the application in a container.
+
+[Low VRAM Guide]: ../../configuration/low-vram-mode
+[Nvidia Container Runtime]: https://developer.nvidia.com/container-runtime
+[an official installer]: https://www.python.org/downloads/
+[using `uv` to manage your python installation]: https://docs.astral.sh/uv/concepts/python-versions/#installing-a-python-version
+[Microsoft Visual C++ Redistributable]: https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170
+[Invoke Launcher]: ../installation
+[CUDA Toolkit Downloads]: https://developer.nvidia.com/cuda-downloads
+[Cuda Docs]: https://developer.nvidia.com/cudnn
+[cuDNN support matrix]: https://docs.nvidia.com/deeplearning/cudnn/support-matrix/index.html
+[ROCm Documentation]: https://rocmdocs.amd.com
+[ROCm docker container]: https://rocmdocs.amd.com/en/latest/Deep_learning/Deep_learning.html#docker-containers
diff --git a/docs/src/content/docs/troubleshooting/faq.mdx b/docs/src/content/docs/troubleshooting/faq.mdx
new file mode 100644
index 00000000000..3c33845f995
--- /dev/null
+++ b/docs/src/content/docs/troubleshooting/faq.mdx
@@ -0,0 +1,117 @@
+---
+title: FAQ
+lastUpdated: 2026-02-19
+---
+
+import { LinkCard } from '@astrojs/starlight/components';
+
+If the troubleshooting steps on this page don't get you up and running, please either [create an issue] or hop on [discord] for help.
+
+## How to Install
+
+
+
+## Downloading models and using existing models
+
+The Model Manager tab in the UI provides a few ways to install models, including using your already-downloaded models. You'll see a popup directing you there on first startup. For more information, see the [model install docs].
+
+## Missing models after updating from v3
+
+If you find some models are missing after updating from v3, it's likely they weren't correctly registered before the update and didn't get picked up in the migration.
+
+You can use the `Scan Folder` tab in the Model Manager UI to fix this. The models will either be in the old, now-unused `autoimport` folder, or your `models` folder.
+
+- Find and copy your install's old `autoimport` folder path, install the main install folder.
+- Go to the Model Manager and click `Scan Folder`.
+- Paste the path and scan.
+- IMPORTANT: Uncheck `Inplace install`.
+- Click `Install All` to install all found models, or just install the models you want.
+
+Next, find and copy your install's `models` folder path (this could be your custom models folder path, or the `models` folder inside the main install folder).
+
+Follow the same steps to scan and import the missing models.
+
+## Slow generation
+
+- Check the [system requirements] to ensure that your system is capable of generating images.
+- Follow the [Low VRAM mode guide] to optimize performance.
+- Check that your generations are happening on your GPU (if you have one). Invoke will log what is being used for generation upon startup. If your GPU isn't used, re-install to and ensure you select the appropriate GPU option.
+- If you are on Windows with an Nvidia GPU, you may have exceeded your GPU's VRAM capacity and are triggering Nvidia's "sysmem fallback". There's a guide to opt out of this behaviour in the [Low VRAM mode guide].
+
+## Triton error on startup
+
+This can be safely ignored. Invoke doesn't use Triton, but if you are on Linux and wish to dismiss the error, you can install Triton.
+
+## Unable to Copy on Firefox
+
+Firefox does not allow Invoke to directly access the clipboard by default. As a result, you may be unable to use certain copy functions. You can fix this by configuring Firefox to allow access to write to the clipboard:
+
+- Go to `about:config` and click the Accept button
+- Search for `dom.events.asyncClipboard.clipboardItem`
+- Set it to `true` by clicking the toggle button
+- Restart Firefox
+
+## Replicate image found online
+
+Most example images with prompts that you'll find on the internet have been generated using different software, so you can't expect to get identical results. In order to reproduce an image, you need to replicate the exact settings and processing steps, including (but not limited to) the model, the positive and negative prompts, the seed, the sampler, the exact image size, any upscaling steps, etc.
+
+## Invalid configuration file
+
+Everything seems to install ok, you get a `ValidationError` when starting up the app.
+
+This is caused by an invalid setting in the `invokeai.yaml` configuration file. The error message should tell you what is wrong.
+
+Check the [configuration docs] for more detail about the settings and how to specify them.
+
+## Out of Memory Errors
+
+The models are large, VRAM is expensive, and you may find yourself faced with Out of Memory errors when generating images. Follow our [Low VRAM mode guide] to configure Invoke to prevent these.
+
+## Memory Leak (Linux)
+
+If you notice a memory leak, it could be caused to memory fragmentation as models are loaded and/or moved from CPU to GPU.
+
+A workaround is to tune memory allocation with an environment variable:
+
+```bash
+# Force blocks >1MB to be allocated with `mmap` so that they are released to the system immediately when they are freed.
+MALLOC_MMAP_THRESHOLD_=1048576
+```
+
+:::caution[Speed vs Memory Tradeoff]
+ Your generations may be slower overall when setting this environment variable.
+:::
+
+:::note[Possibly dependent on `libc` implementation]
+ It's not known if this issue occurs with other `libc` implementations such as `musl`.
+
+ If you encounter this issue and your system uses a different implementation, please try this environment variable and let us know if it fixes the issue.
+:::
+
+Detailed Discussion
+
+Python (and PyTorch) relies on the memory allocator from the C Standard Library (`libc`). On linux, with the GNU C Standard Library implementation (`glibc`), our memory access patterns have been observed to cause severe memory fragmentation.
+
+This fragmentation results in large amounts of memory that has been freed but can't be released back to the OS. Loading models from disk and moving them between CPU/CUDA seem to be the operations that contribute most to the fragmentation.
+
+This memory fragmentation issue can result in OOM crashes during frequent model switching, even if `ram` (the max RAM cache size) is set to a reasonable value (e.g. a OOM crash with `ram=16` on a system with 32GB of RAM).
+
+This problem may also exist on other OSes, and other `libc` implementations. But, at the time of writing, it has only been investigated on linux with `glibc`.
+
+To better understand how the `glibc` memory allocator works, see these references:
+
+- Basics: [The GNU Allocator](https://www.gnu.org/software/libc/manual/html_node/The-GNU-Allocator.html)
+- Details: [Malloc Internals](https://sourceware.org/glibc/wiki/MallocInternals)
+
+Note the differences between memory allocated as chunks in an arena vs. memory allocated with `mmap`. Under `glibc`'s default configuration, most model tensors get allocated as chunks in an arena making them vulnerable to the problem of fragmentation.
+
+[model install docs]: ../../concepts/models
+[system requirements]: ../../start-here/system-requirements
+[Low VRAM mode guide]: ../../configuration/low-vram-mode
+[create an issue]: https://github.com/invoke-ai/InvokeAI/issues
+[discord]: https://discord.gg/ZmtBAhwWhy
+[configuration docs]: ../../configuration/invokeai-yaml
diff --git a/docs/src/content/i18n/en.json b/docs/src/content/i18n/en.json
new file mode 100644
index 00000000000..69333e3a0b2
--- /dev/null
+++ b/docs/src/content/i18n/en.json
@@ -0,0 +1,45 @@
+{
+ "skipLink.label": "Skip to content",
+ "search.label": "Search",
+ "search.ctrlKey": "Ctrl",
+ "search.cancelLabel": "Cancel",
+ "search.devWarning": "Search is only available in production builds. \nTry building and previewing the site to test it out locally.",
+ "themeSelect.accessibleLabel": "Select theme",
+ "themeSelect.dark": "Dark",
+ "themeSelect.light": "Light",
+ "themeSelect.auto": "Auto",
+ "languageSelect.accessibleLabel": "Select language",
+ "menuButton.accessibleLabel": "Menu",
+ "sidebarNav.accessibleLabel": "Main",
+ "tableOfContents.onThisPage": "On this page",
+ "tableOfContents.overview": "Overview",
+ "i18n.untranslatedContent": "This content is not available in your language yet.",
+ "page.editLink": "Edit page",
+ "page.lastUpdated": "Last updated:",
+ "page.previousLink": "Previous",
+ "page.nextLink": "Next",
+ "page.draft": "This content is a draft and will not be included in production builds.",
+ "404.text": "Page not found. Check the URL or try using the search bar.",
+ "aside.note": "Note",
+ "aside.tip": "Tip",
+ "aside.caution": "Caution",
+ "aside.danger": "Danger",
+ "fileTree.directory": "Directory",
+ "builtWithStarlight.label": "Built with Starlight",
+ "heading.anchorLabel": "Section titled “{{title}}”",
+
+ "expressiveCode.copyButtonCopied": "Copied!",
+ "expressiveCode.copyButtonTooltip": "Copy to clipboard",
+ "expressiveCode.terminalWindowFallbackTitle": "Terminal window",
+
+ "pagefind.clear_search": "Clear",
+ "pagefind.load_more": "Load more results",
+ "pagefind.search_label": "Search this site",
+ "pagefind.filters_label": "Filters",
+ "pagefind.zero_results": "No results for [SEARCH_TERM]",
+ "pagefind.many_results": "[COUNT] results for [SEARCH_TERM]",
+ "pagefind.one_result": "[COUNT] result for [SEARCH_TERM]",
+ "pagefind.alt_search": "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM] instead",
+ "pagefind.search_suggestion": "No results for [SEARCH_TERM]. Try one of the following searches:",
+ "pagefind.searching": "Searching for [SEARCH_TERM]..."
+}
diff --git a/docs/src/generated/invocation-context.json b/docs/src/generated/invocation-context.json
new file mode 100644
index 00000000000..55116ec69dd
--- /dev/null
+++ b/docs/src/generated/invocation-context.json
@@ -0,0 +1,657 @@
+{
+ "description": "Provides access to various services and data for the current invocation.\n\nAttributes:\n images (ImagesInterface): Methods to save, get and update images and their metadata.\n tensors (TensorsInterface): Methods to save and get tensors, including image, noise, masks, and masked images.\n conditioning (ConditioningInterface): Methods to save and get conditioning data.\n models (ModelsInterface): Methods to check if a model exists, get a model, and get a model's info.\n logger (LoggerInterface): The app logger.\n config (ConfigInterface): The app config.\n util (UtilInterface): Utility methods, including a method to check if an invocation was canceled and step callbacks.\n boards (BoardsInterface): Methods to interact with boards.",
+ "interfaces": [
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Gets an image as an ImageDTO object.",
+ "name": "get_dto",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the image to get.",
+ "name": "image_name",
+ "type": "str"
+ }
+ ],
+ "return_type": "ImageDTO",
+ "returns": "The image as an ImageDTO object.",
+ "signature": "(image_name: str) -> ImageDTO"
+ },
+ {
+ "description": "Gets an image's metadata, if it has any.",
+ "name": "get_metadata",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the image to get the metadata for.",
+ "name": "image_name",
+ "type": "str"
+ }
+ ],
+ "return_type": "Optional[MetadataField]",
+ "returns": "The image's metadata, if it has any.",
+ "signature": "(image_name: str) -> Optional[MetadataField]"
+ },
+ {
+ "description": "Gets the internal path to an image or thumbnail.",
+ "name": "get_path",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the image to get the path of.",
+ "name": "image_name",
+ "type": "str"
+ },
+ {
+ "default": "False",
+ "description": "Get the path of the thumbnail instead of the full image",
+ "name": "thumbnail",
+ "type": "bool"
+ }
+ ],
+ "return_type": "Path",
+ "returns": "The local path of the image or thumbnail.",
+ "signature": "(image_name: str, thumbnail: bool = False) -> Path"
+ },
+ {
+ "description": "Gets an image as a PIL Image object. This method returns a copy of the image.",
+ "name": "get_pil",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the image to get.",
+ "name": "image_name",
+ "type": "str"
+ },
+ {
+ "default": "None",
+ "description": "The color mode to convert the image to. If None, the original mode is used.",
+ "name": "mode",
+ "type": "Optional[Literal['L', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F']]"
+ }
+ ],
+ "return_type": "Image",
+ "returns": "The image as a PIL Image object.",
+ "signature": "(image_name: str, mode: Optional[Literal['L', 'RGB', 'RGBA', 'CMYK', 'YCbCr', 'LAB', 'HSV', 'I', 'F']] = None) -> Image"
+ },
+ {
+ "description": "Saves an image, returning its DTO.\nIf the current queue item has a workflow or metadata, it is automatically saved with the image.",
+ "name": "save",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The image to save, as a PIL image.",
+ "name": "image",
+ "type": "Image"
+ },
+ {
+ "default": "None",
+ "description": "The board ID to add the image to, if it should be added. It the invocation inherits from `WithBoard`, that board will be used automatically. **Use this only if you want to override or provide a board manually!**",
+ "name": "board_id",
+ "type": "Optional[str]"
+ },
+ {
+ "default": "ImageCategory.GENERAL",
+ "description": "The category of the image. Only the GENERAL category is added to the gallery.",
+ "name": "image_category",
+ "type": "ImageCategory"
+ },
+ {
+ "default": "None",
+ "description": "The metadata to save with the image, if it should have any. If the invocation inherits from `WithMetadata`, that metadata will be used automatically. **Use this only if you want to override or provide metadata manually!**",
+ "name": "metadata",
+ "type": "Optional[MetadataField]"
+ }
+ ],
+ "return_type": "ImageDTO",
+ "returns": "The saved image DTO.",
+ "signature": "(image: Image, board_id: Optional[str] = None, image_category: ImageCategory = ImageCategory.GENERAL, metadata: Optional[MetadataField] = None) -> ImageDTO"
+ }
+ ],
+ "name": "ImagesInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Loads a tensor by name. This method returns a copy of the tensor.",
+ "name": "load",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the tensor to load.",
+ "name": "name",
+ "type": "str"
+ }
+ ],
+ "return_type": "Tensor",
+ "returns": "The tensor.",
+ "signature": "(name: str) -> Tensor"
+ },
+ {
+ "description": "Saves a tensor, returning its name.",
+ "name": "save",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The tensor to save.",
+ "name": "tensor",
+ "type": "Tensor"
+ }
+ ],
+ "return_type": "str",
+ "returns": "The name of the saved tensor.",
+ "signature": "(tensor: Tensor) -> str"
+ }
+ ],
+ "name": "TensorsInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Loads conditioning data by name. This method returns a copy of the conditioning data.",
+ "name": "load",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the conditioning data to load.",
+ "name": "name",
+ "type": "str"
+ }
+ ],
+ "return_type": "ConditioningFieldData",
+ "returns": "The conditioning data.",
+ "signature": "(name: str) -> ConditioningFieldData"
+ },
+ {
+ "description": "Saves a conditioning data object, returning its name.",
+ "name": "save",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The conditioning data to save.",
+ "name": "conditioning_data",
+ "type": "ConditioningFieldData"
+ }
+ ],
+ "return_type": "str",
+ "returns": "The name of the saved conditioning data.",
+ "signature": "(conditioning_data: ConditioningFieldData) -> str"
+ }
+ ],
+ "name": "ConditioningInterface"
+ },
+ {
+ "description": "Common API for loading, downloading and managing models.",
+ "methods": [
+ {
+ "description": "Download the model file located at source to the models cache and return its Path.\nThis can be used to single-file install models and other resources of arbitrary types\nwhich should not get registered with the database. If the model is already\ninstalled, the cached path will be returned. Otherwise it will be downloaded.",
+ "name": "download_and_cache_model",
+ "parameters": [
+ {
+ "default": "",
+ "description": "A URL that points to the model, or a huggingface repo_id.",
+ "name": "source",
+ "type": "str | AnyHttpUrl"
+ }
+ ],
+ "return_type": "Path",
+ "returns": "Path to the downloaded model",
+ "signature": "(source: str | AnyHttpUrl) -> Path"
+ },
+ {
+ "description": "Check if a model exists.",
+ "name": "exists",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The key or ModelField representing the model.",
+ "name": "identifier",
+ "type": "Union[str, ModelIdentifierField]"
+ }
+ ],
+ "return_type": "bool",
+ "returns": "True if the model exists, False if not.",
+ "signature": "(identifier: Union[str, ModelIdentifierField]) -> bool"
+ },
+ {
+ "description": "Gets the absolute path for a given model config or path.\nFor example, if the model's path is `flux/main/FLUX Dev.safetensors`, and the models path is\n`/home/username/InvokeAI/models`, this method will return\n`/home/username/InvokeAI/models/flux/main/FLUX Dev.safetensors`.",
+ "name": "get_absolute_path",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The model config or path.",
+ "name": "config_or_path",
+ "type": "Union[AnyModelConfig, Path, str]"
+ }
+ ],
+ "return_type": "Path",
+ "returns": "The absolute path to the model.",
+ "signature": "(config_or_path: Union[AnyModelConfig, Path, str]) -> Path"
+ },
+ {
+ "description": "Get a model's config.",
+ "name": "get_config",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The key or ModelField representing the model.",
+ "name": "identifier",
+ "type": "Union[str, ModelIdentifierField]"
+ }
+ ],
+ "return_type": "AnyModelConfig",
+ "returns": "The model's config.",
+ "signature": "(identifier: Union[str, ModelIdentifierField]) -> AnyModelConfig"
+ },
+ {
+ "description": "Load a model.",
+ "name": "load",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The key or ModelField representing the model.",
+ "name": "identifier",
+ "type": "Union[str, ModelIdentifierField]"
+ },
+ {
+ "default": "None",
+ "description": "The submodel of the model to get.",
+ "name": "submodel_type",
+ "type": "Optional[SubModelType]"
+ }
+ ],
+ "return_type": "LoadedModel",
+ "returns": "An object representing the loaded model.",
+ "signature": "(identifier: Union[str, ModelIdentifierField], submodel_type: Optional[SubModelType] = None) -> LoadedModel"
+ },
+ {
+ "description": "Load a model by its attributes.",
+ "name": "load_by_attrs",
+ "parameters": [
+ {
+ "default": "",
+ "description": "Name of the model.",
+ "name": "name",
+ "type": "str"
+ },
+ {
+ "default": "",
+ "description": "The models' base type, e.g. `BaseModelType.StableDiffusion1`, `BaseModelType.StableDiffusionXL`, etc.",
+ "name": "base",
+ "type": "BaseModelType"
+ },
+ {
+ "default": "",
+ "description": "Type of the model, e.g. `ModelType.Main`, `ModelType.Vae`, etc.",
+ "name": "type",
+ "type": "ModelType"
+ },
+ {
+ "default": "None",
+ "description": "The type of submodel to load, e.g. `SubModelType.UNet`, `SubModelType.TextEncoder`, etc. Only main models have submodels.",
+ "name": "submodel_type",
+ "type": "Optional[SubModelType]"
+ }
+ ],
+ "return_type": "LoadedModel",
+ "returns": "An object representing the loaded model.",
+ "signature": "(name: str, base: BaseModelType, type: ModelType, submodel_type: Optional[SubModelType] = None) -> LoadedModel"
+ },
+ {
+ "description": "Load the model file located at the indicated path\nIf a loader callable is provided, it will be invoked to load the model. Otherwise,\n`safetensors.torch.load_file()` or `torch.load()` will be called to load the model.\nBe aware that the LoadedModelWithoutConfig object has no `config` attribute",
+ "name": "load_local_model",
+ "parameters": [
+ {
+ "default": "",
+ "description": "",
+ "name": "model_path",
+ "type": "Path"
+ },
+ {
+ "default": "None",
+ "description": "A Callable that expects a Path and returns a dict[str|int, Any]",
+ "name": "loader",
+ "type": "Optional[Callable[[Path], AnyModel]]"
+ }
+ ],
+ "return_type": "LoadedModelWithoutConfig",
+ "returns": "A LoadedModelWithoutConfig object.",
+ "signature": "(model_path: Path, loader: Optional[Callable[[Path], AnyModel]] = None) -> LoadedModelWithoutConfig"
+ },
+ {
+ "description": "Download, cache, and load the model file located at the indicated URL or repo_id.\nIf the model is already downloaded, it will be loaded from the cache.\nIf the a loader callable is provided, it will be invoked to load the model. Otherwise,\n`safetensors.torch.load_file()` or `torch.load()` will be called to load the model.\nBe aware that the LoadedModelWithoutConfig object has no `config` attribute",
+ "name": "load_remote_model",
+ "parameters": [
+ {
+ "default": "",
+ "description": "A URL or huggingface repoid.",
+ "name": "source",
+ "type": "str | AnyHttpUrl"
+ },
+ {
+ "default": "None",
+ "description": "A Callable that expects a Path and returns a dict[str|int, Any]",
+ "name": "loader",
+ "type": "Optional[Callable[[Path], AnyModel]]"
+ }
+ ],
+ "return_type": "LoadedModelWithoutConfig",
+ "returns": "A LoadedModelWithoutConfig object.",
+ "signature": "(source: str | AnyHttpUrl, loader: Optional[Callable[[Path], AnyModel]] = None) -> LoadedModelWithoutConfig"
+ },
+ {
+ "description": "Search for models by attributes.",
+ "name": "search_by_attrs",
+ "parameters": [
+ {
+ "default": "None",
+ "description": "The name to search for (exact match).",
+ "name": "name",
+ "type": "Optional[str]"
+ },
+ {
+ "default": "None",
+ "description": "The base to search for, e.g. `BaseModelType.StableDiffusion1`, `BaseModelType.StableDiffusionXL`, etc.",
+ "name": "base",
+ "type": "Optional[BaseModelType]"
+ },
+ {
+ "default": "None",
+ "description": "Type type of model to search for, e.g. `ModelType.Main`, `ModelType.Vae`, etc.",
+ "name": "type",
+ "type": "Optional[ModelType]"
+ },
+ {
+ "default": "None",
+ "description": "The format of model to search for, e.g. `ModelFormat.Checkpoint`, `ModelFormat.Diffusers`, etc.",
+ "name": "format",
+ "type": "Optional[ModelFormat]"
+ }
+ ],
+ "return_type": "list[AnyModelConfig]",
+ "returns": "A list of models that match the attributes.",
+ "signature": "(name: Optional[str] = None, base: Optional[BaseModelType] = None, type: Optional[ModelType] = None, format: Optional[ModelFormat] = None) -> list[AnyModelConfig]"
+ },
+ {
+ "description": "Search for models by path.",
+ "name": "search_by_path",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The path to search for.",
+ "name": "path",
+ "type": "Path"
+ }
+ ],
+ "return_type": "list[AnyModelConfig]",
+ "returns": "A list of models that match the path.",
+ "signature": "(path: Path) -> list[AnyModelConfig]"
+ }
+ ],
+ "name": "ModelsInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Logs a debug message.",
+ "name": "debug",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The message to log.",
+ "name": "message",
+ "type": "str"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(message: str) -> None"
+ },
+ {
+ "description": "Logs an error message.",
+ "name": "error",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The message to log.",
+ "name": "message",
+ "type": "str"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(message: str) -> None"
+ },
+ {
+ "description": "Logs an info message.",
+ "name": "info",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The message to log.",
+ "name": "message",
+ "type": "str"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(message: str) -> None"
+ },
+ {
+ "description": "Logs a warning message.",
+ "name": "warning",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The message to log.",
+ "name": "message",
+ "type": "str"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(message: str) -> None"
+ }
+ ],
+ "name": "LoggerInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Gets the app's config.",
+ "name": "get",
+ "parameters": [],
+ "return_type": "InvokeAIAppConfig",
+ "returns": "The app's config.",
+ "signature": "() -> InvokeAIAppConfig"
+ }
+ ],
+ "name": "ConfigInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "The step callback for FLUX.2 Klein models (32-channel VAE).",
+ "name": "flux2_step_callback",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The intermediate state of the diffusion pipeline.",
+ "name": "intermediate_state",
+ "type": "PipelineIntermediateState"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(intermediate_state: PipelineIntermediateState) -> None"
+ },
+ {
+ "description": "The step callback emits a progress event with the current step, the total number of\nsteps, a preview image, and some other internal metadata.\nThis should be called after each denoising step.",
+ "name": "flux_step_callback",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The intermediate state of the diffusion pipeline.",
+ "name": "intermediate_state",
+ "type": "PipelineIntermediateState"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(intermediate_state: PipelineIntermediateState) -> None"
+ },
+ {
+ "description": "Checks if the current session has been canceled.",
+ "name": "is_canceled",
+ "parameters": [],
+ "return_type": "bool",
+ "returns": "True if the current session has been canceled, False if not.",
+ "signature": "() -> bool"
+ },
+ {
+ "description": "The step callback emits a progress event with the current step, the total number of\nsteps, a preview image, and some other internal metadata.\nThis should be called after each denoising step.",
+ "name": "sd_step_callback",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The intermediate state of the diffusion pipeline.",
+ "name": "intermediate_state",
+ "type": "PipelineIntermediateState"
+ },
+ {
+ "default": "",
+ "description": "The base model for the current denoising step.",
+ "name": "base_model",
+ "type": "BaseModelType"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None"
+ },
+ {
+ "description": "Signals the progress of some long-running invocation. The progress is displayed in the UI.\nIf a percentage is provided, the UI will display a progress bar and automatically append the percentage to the\nmessage. You should not include the percentage in the message.\nExample:\n```py\ntotal_steps = 10\nfor i in range(total_steps):\npercentage = i / (total_steps - 1)\ncontext.util.signal_progress(\"Doing something cool\", percentage)\n```\nIf an image is provided, the UI will display it. If your image should be displayed at a different size, provide\na tuple of `(width, height)` for the `image_size` parameter. The image will be displayed at the specified size\nin the UI.\nFor example, SD denoising progress images are 1/8 the size of the original image, so you'd do this to ensure the\nimage is displayed at the correct size:\n```py\n# Calculate the output size of the image (8x the progress image's size)\nwidth = progress_image.width * 8\nheight = progress_image.height * 8\n# Signal the progress with the image and output size\nsignal_progress(\"Denoising\", percentage, progress_image, (width, height))\n```\nIf your progress image is very large, consider downscaling it to reduce the payload size and provide the original\nsize to the `image_size` parameter. The PIL `thumbnail` method is useful for this, as it maintains the aspect\nratio of the image:\n```py\n# `thumbnail` modifies the image in-place, so we need to first make a copy\nthumbnail_image = progress_image.copy()\n# Resize the image to a maximum of 256x256 pixels, maintaining the aspect ratio\nthumbnail_image.thumbnail((256, 256))\n# Signal the progress with the thumbnail, passing the original size\nsignal_progress(\"Denoising\", percentage, thumbnail, progress_image.size)\n```",
+ "name": "signal_progress",
+ "parameters": [
+ {
+ "default": "",
+ "description": "A message describing the current status. Do not include the percentage in this message.",
+ "name": "message",
+ "type": "str"
+ },
+ {
+ "default": "None",
+ "description": "The current percentage completion for the process. Omit for indeterminate progress.",
+ "name": "percentage",
+ "type": "float | None"
+ },
+ {
+ "default": "None",
+ "description": "An optional image to display.",
+ "name": "image",
+ "type": "Image | None"
+ },
+ {
+ "default": "None",
+ "description": "The optional size of the image to display. If omitted, the image will be displayed at its original size.",
+ "name": "image_size",
+ "type": "tuple[int, int] | None"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(message: str, percentage: float | None = None, image: Image | None = None, image_size: tuple[int, int] | None = None) -> None"
+ }
+ ],
+ "name": "UtilInterface"
+ },
+ {
+ "description": "",
+ "methods": [
+ {
+ "description": "Adds an image to a board.",
+ "name": "add_image_to_board",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The ID of the board to add the image to.",
+ "name": "board_id",
+ "type": "str"
+ },
+ {
+ "default": "",
+ "description": "The name of the image to add to the board.",
+ "name": "image_name",
+ "type": "str"
+ }
+ ],
+ "return_type": "None",
+ "returns": "",
+ "signature": "(board_id: str, image_name: str) -> None"
+ },
+ {
+ "description": "Creates a board for the current user.",
+ "name": "create",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The name of the board to create.",
+ "name": "board_name",
+ "type": "str"
+ }
+ ],
+ "return_type": "BoardDTO",
+ "returns": "The created board DTO.",
+ "signature": "(board_name: str) -> BoardDTO"
+ },
+ {
+ "description": "Gets all boards accessible to the current user.",
+ "name": "get_all",
+ "parameters": [],
+ "return_type": "list[BoardDTO]",
+ "returns": "A list of all boards accessible to the current user.",
+ "signature": "() -> list[BoardDTO]"
+ },
+ {
+ "description": "Gets all image names for a board.",
+ "name": "get_all_image_names_for_board",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The ID of the board to get the image names for.",
+ "name": "board_id",
+ "type": "str"
+ }
+ ],
+ "return_type": "list[str]",
+ "returns": "A list of all image names for the board.",
+ "signature": "(board_id: str) -> list[str]"
+ },
+ {
+ "description": "Gets a board DTO.",
+ "name": "get_dto",
+ "parameters": [
+ {
+ "default": "",
+ "description": "The ID of the board to get.",
+ "name": "board_id",
+ "type": "str"
+ }
+ ],
+ "return_type": "BoardDTO",
+ "returns": "The board DTO.",
+ "signature": "(board_id: str) -> BoardDTO"
+ }
+ ],
+ "name": "BoardsInterface"
+ }
+ ],
+ "name": "InvocationContext"
+}
diff --git a/docs/src/generated/settings.json b/docs/src/generated/settings.json
new file mode 100644
index 00000000000..fcb47dbfb23
--- /dev/null
+++ b/docs/src/generated/settings.json
@@ -0,0 +1,832 @@
+{
+ "settings": [
+ {
+ "category": "WEB",
+ "default": "127.0.0.1",
+ "description": "IP address to bind to. Use `0.0.0.0` to serve to your local network.",
+ "env_var": "INVOKEAI_HOST",
+ "literal_values": [],
+ "name": "host",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": 9090,
+ "description": "Port to bind to.",
+ "env_var": "INVOKEAI_PORT",
+ "literal_values": [],
+ "name": "port",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": [],
+ "description": "Allowed CORS origins.",
+ "env_var": "INVOKEAI_ALLOW_ORIGINS",
+ "literal_values": [],
+ "name": "allow_origins",
+ "required": false,
+ "type": "list[str]",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": true,
+ "description": "Allow CORS credentials.",
+ "env_var": "INVOKEAI_ALLOW_CREDENTIALS",
+ "literal_values": [],
+ "name": "allow_credentials",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": [
+ "*"
+ ],
+ "description": "Methods allowed for CORS.",
+ "env_var": "INVOKEAI_ALLOW_METHODS",
+ "literal_values": [],
+ "name": "allow_methods",
+ "required": false,
+ "type": "list[str]",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": [
+ "*"
+ ],
+ "description": "Headers allowed for CORS.",
+ "env_var": "INVOKEAI_ALLOW_HEADERS",
+ "literal_values": [],
+ "name": "allow_headers",
+ "required": false,
+ "type": "list[str]",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": null,
+ "description": "SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.",
+ "env_var": "INVOKEAI_SSL_CERTFILE",
+ "literal_values": [],
+ "name": "ssl_certfile",
+ "required": false,
+ "type": "typing.Optional[pathlib.Path]",
+ "validation": {}
+ },
+ {
+ "category": "WEB",
+ "default": null,
+ "description": "SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.",
+ "env_var": "INVOKEAI_SSL_KEYFILE",
+ "literal_values": [],
+ "name": "ssl_keyfile",
+ "required": false,
+ "type": "typing.Optional[pathlib.Path]",
+ "validation": {}
+ },
+ {
+ "category": "MISC FEATURES",
+ "default": false,
+ "description": "Enable logging of parsed prompt tokens.",
+ "env_var": "INVOKEAI_LOG_TOKENIZATION",
+ "literal_values": [],
+ "name": "log_tokenization",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MISC FEATURES",
+ "default": true,
+ "description": "Enable patchmatch inpaint code.",
+ "env_var": "INVOKEAI_PATCHMATCH",
+ "literal_values": [],
+ "name": "patchmatch",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "models",
+ "description": "Path to the models directory.",
+ "env_var": "INVOKEAI_MODELS_DIR",
+ "literal_values": [],
+ "name": "models_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "models/.convert_cache",
+ "description": "Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).",
+ "env_var": "INVOKEAI_CONVERT_CACHE_DIR",
+ "literal_values": [],
+ "name": "convert_cache_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "models/.download_cache",
+ "description": "Path to the directory that contains dynamically downloaded models.",
+ "env_var": "INVOKEAI_DOWNLOAD_CACHE_DIR",
+ "literal_values": [],
+ "name": "download_cache_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "configs",
+ "description": "Path to directory of legacy checkpoint config files.",
+ "env_var": "INVOKEAI_LEGACY_CONF_DIR",
+ "literal_values": [],
+ "name": "legacy_conf_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "databases",
+ "description": "Path to InvokeAI databases directory.",
+ "env_var": "INVOKEAI_DB_DIR",
+ "literal_values": [],
+ "name": "db_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "outputs",
+ "description": "Path to directory for outputs.",
+ "env_var": "INVOKEAI_OUTPUTS_DIR",
+ "literal_values": [],
+ "name": "outputs_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "flat",
+ "description": "Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.",
+ "env_var": "INVOKEAI_IMAGE_SUBFOLDER_STRATEGY",
+ "literal_values": [
+ "flat",
+ "date",
+ "type",
+ "hash"
+ ],
+ "name": "image_subfolder_strategy",
+ "required": false,
+ "type": "typing.Literal['flat', 'date', 'type', 'hash']",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "nodes",
+ "description": "Path to directory for custom nodes.",
+ "env_var": "INVOKEAI_CUSTOM_NODES_DIR",
+ "literal_values": [],
+ "name": "custom_nodes_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "style_presets",
+ "description": "Path to directory for style presets.",
+ "env_var": "INVOKEAI_STYLE_PRESETS_DIR",
+ "literal_values": [],
+ "name": "style_presets_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "PATHS",
+ "default": "workflow_thumbnails",
+ "description": "Path to directory for workflow thumbnails.",
+ "env_var": "INVOKEAI_WORKFLOW_THUMBNAILS_DIR",
+ "literal_values": [],
+ "name": "workflow_thumbnails_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": [
+ "console"
+ ],
+ "description": "Log handler. Valid options are \"console\", \"file=\", \"syslog=path|address:host:port\", \"http=\".",
+ "env_var": "INVOKEAI_LOG_HANDLERS",
+ "literal_values": [],
+ "name": "log_handlers",
+ "required": false,
+ "type": "list[str]",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": "color",
+ "description": "Log format. Use \"plain\" for text-only, \"color\" for colorized output, \"legacy\" for 2.3-style logging and \"syslog\" for syslog-style.",
+ "env_var": "INVOKEAI_LOG_FORMAT",
+ "literal_values": [
+ "plain",
+ "color",
+ "syslog",
+ "legacy"
+ ],
+ "name": "log_format",
+ "required": false,
+ "type": "typing.Literal['plain', 'color', 'syslog', 'legacy']",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": "info",
+ "description": "Emit logging messages at this level or higher.",
+ "env_var": "INVOKEAI_LOG_LEVEL",
+ "literal_values": [
+ "debug",
+ "info",
+ "warning",
+ "error",
+ "critical"
+ ],
+ "name": "log_level",
+ "required": false,
+ "type": "typing.Literal['debug', 'info', 'warning', 'error', 'critical']",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": false,
+ "description": "Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.",
+ "env_var": "INVOKEAI_LOG_SQL",
+ "literal_values": [],
+ "name": "log_sql",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": "warning",
+ "description": "Log level for network-related messages. 'info' and 'debug' are very verbose.",
+ "env_var": "INVOKEAI_LOG_LEVEL_NETWORK",
+ "literal_values": [
+ "debug",
+ "info",
+ "warning",
+ "error",
+ "critical"
+ ],
+ "name": "log_level_network",
+ "required": false,
+ "type": "typing.Literal['debug', 'info', 'warning', 'error', 'critical']",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": false,
+ "description": "Use in-memory database. Useful for development.",
+ "env_var": "INVOKEAI_USE_MEMORY_DB",
+ "literal_values": [],
+ "name": "use_memory_db",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": false,
+ "description": "Automatically reload when Python sources are changed. Does not reload node definitions.",
+ "env_var": "INVOKEAI_DEV_RELOAD",
+ "literal_values": [],
+ "name": "dev_reload",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": false,
+ "description": "Enable graph profiling using `cProfile`.",
+ "env_var": "INVOKEAI_PROFILE_GRAPHS",
+ "literal_values": [],
+ "name": "profile_graphs",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": null,
+ "description": "An optional prefix for profile output files.",
+ "env_var": "INVOKEAI_PROFILE_PREFIX",
+ "literal_values": [],
+ "name": "profile_prefix",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "LOGGING",
+ "default": "profiles",
+ "description": "Path to profiles output directory.",
+ "env_var": "INVOKEAI_PROFILES_DIR",
+ "literal_values": [],
+ "name": "profiles_dir",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": null,
+ "description": "The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.",
+ "env_var": "INVOKEAI_MAX_CACHE_RAM_GB",
+ "literal_values": [],
+ "name": "max_cache_ram_gb",
+ "required": false,
+ "type": "typing.Optional[float]",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": null,
+ "description": "The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.",
+ "env_var": "INVOKEAI_MAX_CACHE_VRAM_GB",
+ "literal_values": [],
+ "name": "max_cache_vram_gb",
+ "required": false,
+ "type": "typing.Optional[float]",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": false,
+ "description": "If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.",
+ "env_var": "INVOKEAI_LOG_MEMORY_USAGE",
+ "literal_values": [],
+ "name": "log_memory_usage",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": 0,
+ "description": "How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.",
+ "env_var": "INVOKEAI_MODEL_CACHE_KEEP_ALIVE_MIN",
+ "literal_values": [],
+ "name": "model_cache_keep_alive_min",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": 3,
+ "description": "The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.",
+ "env_var": "INVOKEAI_DEVICE_WORKING_MEM_GB",
+ "literal_values": [],
+ "name": "device_working_mem_gb",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": true,
+ "description": "Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.",
+ "env_var": "INVOKEAI_ENABLE_PARTIAL_LOADING",
+ "literal_values": [],
+ "name": "enable_partial_loading",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": true,
+ "description": "Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.",
+ "env_var": "INVOKEAI_KEEP_RAM_COPY_OF_WEIGHTS",
+ "literal_values": [],
+ "name": "keep_ram_copy_of_weights",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": null,
+ "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.",
+ "env_var": "INVOKEAI_RAM",
+ "literal_values": [],
+ "name": "ram",
+ "required": false,
+ "type": "typing.Optional[float]",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": null,
+ "description": "DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.",
+ "env_var": "INVOKEAI_VRAM",
+ "literal_values": [],
+ "name": "vram",
+ "required": false,
+ "type": "typing.Optional[float]",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": true,
+ "description": "DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.",
+ "env_var": "INVOKEAI_LAZY_OFFLOAD",
+ "literal_values": [],
+ "name": "lazy_offload",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "CACHE",
+ "default": null,
+ "description": "Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.",
+ "env_var": "INVOKEAI_PYTORCH_CUDA_ALLOC_CONF",
+ "literal_values": [],
+ "name": "pytorch_cuda_alloc_conf",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "DEVICE",
+ "default": "auto",
+ "description": "Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)",
+ "env_var": "INVOKEAI_DEVICE",
+ "literal_values": [],
+ "name": "device",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "DEVICE",
+ "default": "auto",
+ "description": "Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.",
+ "env_var": "INVOKEAI_PRECISION",
+ "literal_values": [
+ "auto",
+ "float16",
+ "bfloat16",
+ "float32"
+ ],
+ "name": "precision",
+ "required": false,
+ "type": "typing.Literal['auto', 'float16', 'bfloat16', 'float32']",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": false,
+ "description": "Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.",
+ "env_var": "INVOKEAI_SEQUENTIAL_GUIDANCE",
+ "literal_values": [],
+ "name": "sequential_guidance",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": "auto",
+ "description": "Attention type.",
+ "env_var": "INVOKEAI_ATTENTION_TYPE",
+ "literal_values": [
+ "auto",
+ "normal",
+ "xformers",
+ "sliced",
+ "torch-sdp"
+ ],
+ "name": "attention_type",
+ "required": false,
+ "type": "typing.Literal['auto', 'normal', 'xformers', 'sliced', 'torch-sdp']",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": "auto",
+ "description": "Slice size, valid when attention_type==\"sliced\".",
+ "env_var": "INVOKEAI_ATTENTION_SLICE_SIZE",
+ "literal_values": [
+ "auto",
+ "balanced",
+ "max",
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
+ ],
+ "name": "attention_slice_size",
+ "required": false,
+ "type": "typing.Literal['auto', 'balanced', 'max', 1, 2, 3, 4, 5, 6, 7, 8]",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": false,
+ "description": "Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).",
+ "env_var": "INVOKEAI_FORCE_TILED_DECODE",
+ "literal_values": [],
+ "name": "force_tiled_decode",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": 1,
+ "description": "The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.",
+ "env_var": "INVOKEAI_PIL_COMPRESS_LEVEL",
+ "literal_values": [],
+ "name": "pil_compress_level",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": 10000,
+ "description": "Maximum number of items in the session queue.",
+ "env_var": "INVOKEAI_MAX_QUEUE_SIZE",
+ "literal_values": [],
+ "name": "max_queue_size",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": false,
+ "description": "Empties session queue on startup. If true, disables `max_queue_history`.",
+ "env_var": "INVOKEAI_CLEAR_QUEUE_ON_STARTUP",
+ "literal_values": [],
+ "name": "clear_queue_on_startup",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "GENERATION",
+ "default": null,
+ "description": "Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.",
+ "env_var": "INVOKEAI_MAX_QUEUE_HISTORY",
+ "literal_values": [],
+ "name": "max_queue_history",
+ "required": false,
+ "type": "typing.Optional[int]",
+ "validation": {}
+ },
+ {
+ "category": "NODES",
+ "default": null,
+ "description": "List of nodes to allow. Omit to allow all.",
+ "env_var": "INVOKEAI_ALLOW_NODES",
+ "literal_values": [],
+ "name": "allow_nodes",
+ "required": false,
+ "type": "typing.Optional[list[str]]",
+ "validation": {}
+ },
+ {
+ "category": "NODES",
+ "default": null,
+ "description": "List of nodes to deny. Omit to deny none.",
+ "env_var": "INVOKEAI_DENY_NODES",
+ "literal_values": [],
+ "name": "deny_nodes",
+ "required": false,
+ "type": "typing.Optional[list[str]]",
+ "validation": {}
+ },
+ {
+ "category": "NODES",
+ "default": 512,
+ "description": "How many cached nodes to keep in memory.",
+ "env_var": "INVOKEAI_NODE_CACHE_SIZE",
+ "literal_values": [],
+ "name": "node_cache_size",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MODEL INSTALL",
+ "default": "blake3_single",
+ "description": "Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.",
+ "env_var": "INVOKEAI_HASHING_ALGORITHM",
+ "literal_values": [
+ "blake3_multi",
+ "blake3_single",
+ "random",
+ "md5",
+ "sha1",
+ "sha224",
+ "sha256",
+ "sha384",
+ "sha512",
+ "blake2b",
+ "blake2s",
+ "sha3_224",
+ "sha3_256",
+ "sha3_384",
+ "sha3_512",
+ "shake_128",
+ "shake_256"
+ ],
+ "name": "hashing_algorithm",
+ "required": false,
+ "type": "typing.Literal['blake3_multi', 'blake3_single', 'random', 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'blake2b', 'blake2s', 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', 'shake_128', 'shake_256']",
+ "validation": {}
+ },
+ {
+ "category": "MODEL INSTALL",
+ "default": null,
+ "description": "List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.",
+ "env_var": "INVOKEAI_REMOTE_API_TOKENS",
+ "literal_values": [],
+ "name": "remote_api_tokens",
+ "required": false,
+ "type": "typing.Optional[list[invokeai.app.services.config.config_default.URLRegexTokenPair]]",
+ "validation": {}
+ },
+ {
+ "category": "MODEL INSTALL",
+ "default": false,
+ "description": "Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.",
+ "env_var": "INVOKEAI_SCAN_MODELS_ON_STARTUP",
+ "literal_values": [],
+ "name": "scan_models_on_startup",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MODEL INSTALL",
+ "default": false,
+ "description": "UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.",
+ "env_var": "INVOKEAI_UNSAFE_DISABLE_PICKLESCAN",
+ "literal_values": [],
+ "name": "unsafe_disable_picklescan",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MODEL INSTALL",
+ "default": true,
+ "description": "Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.",
+ "env_var": "INVOKEAI_ALLOW_UNKNOWN_MODELS",
+ "literal_values": [],
+ "name": "allow_unknown_models",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MULTIUSER",
+ "default": false,
+ "description": "Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.",
+ "env_var": "INVOKEAI_MULTIUSER",
+ "literal_values": [],
+ "name": "multiuser",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "MULTIUSER",
+ "default": false,
+ "description": "Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.",
+ "env_var": "INVOKEAI_STRICT_PASSWORD_CHECKING",
+ "literal_values": [],
+ "name": "strict_password_checking",
+ "required": false,
+ "type": "",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "API key for Alibaba Cloud DashScope image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_ALIBABACLOUD_API_KEY",
+ "literal_values": [],
+ "name": "external_alibabacloud_api_key",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "Base URL override for Alibaba Cloud DashScope image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_ALIBABACLOUD_BASE_URL",
+ "literal_values": [],
+ "name": "external_alibabacloud_base_url",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "API key for Gemini image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_GEMINI_API_KEY",
+ "literal_values": [],
+ "name": "external_gemini_api_key",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "API key for OpenAI image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_OPENAI_API_KEY",
+ "literal_values": [],
+ "name": "external_openai_api_key",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "Base URL override for Gemini image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_GEMINI_BASE_URL",
+ "literal_values": [],
+ "name": "external_gemini_base_url",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "Base URL override for OpenAI image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_OPENAI_BASE_URL",
+ "literal_values": [],
+ "name": "external_openai_base_url",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "API key for Seedream image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_SEEDREAM_API_KEY",
+ "literal_values": [],
+ "name": "external_seedream_api_key",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ },
+ {
+ "category": "EXTERNAL PROVIDERS",
+ "default": null,
+ "description": "Base URL override for Seedream image generation.",
+ "env_var": "INVOKEAI_EXTERNAL_SEEDREAM_BASE_URL",
+ "literal_values": [],
+ "name": "external_seedream_base_url",
+ "required": false,
+ "type": "typing.Optional[str]",
+ "validation": {}
+ }
+ ]
+}
diff --git a/docs/src/layouts/PageFrameExtended.astro b/docs/src/layouts/PageFrameExtended.astro
new file mode 100644
index 00000000000..9287376a4b9
--- /dev/null
+++ b/docs/src/layouts/PageFrameExtended.astro
@@ -0,0 +1,9 @@
+---
+import PageFrame from '@astrojs/starlight/components/PageFrame.astro';
+---
+
+
+
+
+
+
diff --git a/docs/src/lib/base-path.ts b/docs/src/lib/base-path.ts
new file mode 100644
index 00000000000..23042d899a5
--- /dev/null
+++ b/docs/src/lib/base-path.ts
@@ -0,0 +1,6 @@
+export const withBase = (path: string, baseUrl: string) => {
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
+ const normalizedPath = path.replace(/^\//, '');
+
+ return `${normalizedBase}${normalizedPath}`;
+};
diff --git a/docs/src/lib/components/DownloadOptions.astro b/docs/src/lib/components/DownloadOptions.astro
new file mode 100644
index 00000000000..dfae32a4915
--- /dev/null
+++ b/docs/src/lib/components/DownloadOptions.astro
@@ -0,0 +1,197 @@
+---
+import { LinkCard, Icon, LinkButton } from '@astrojs/starlight/components';
+import { type StarlightIcon } from '@astrojs/starlight/types';
+import { withBase } from '../base-path';
+
+type LauncherDownloadOption = {
+ icon: StarlightIcon;
+ headline: string;
+ note: string;
+ launcherDownloadLink: string;
+ launcherDownloadLabel?: string;
+};
+const launcherDownloadOptions: Record = {
+ windows: {
+ icon: 'seti:windows',
+ headline: 'Download for Windows',
+ note: 'Requires Windows 10 or later, and NVIDIA or AMD GPU.',
+ launcherDownloadLink:
+ 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe',
+ launcherDownloadLabel: 'Download EXE',
+ },
+ macos: {
+ icon: 'apple',
+ headline: 'Download for MacOS',
+ note: 'Requires Apple Silicon (M-Series). Not compatible with Intel.',
+ launcherDownloadLink:
+ 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg',
+ launcherDownloadLabel: 'Download DMG',
+ },
+ linux: {
+ icon: 'linux',
+ headline: 'Download for Linux',
+ note: 'Requires NVIDIA or AMD GPU. Compatible with most distributions.',
+ launcherDownloadLink:
+ 'https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage',
+ launcherDownloadLabel: 'Download AppImage',
+ },
+};
+
+const manualDownloadOptions = {
+ github: {
+ headline: 'Download from GitHub',
+ description: 'For advanced users who want to set up Invoke manually or contribute to the project.',
+ href: 'https://github.com/invoke-ai/InvokeAI/releases',
+ },
+ docker: {
+ headline: 'Run with Docker',
+ description: 'For users who want to run Invoke without installing dependencies directly on their system.',
+ href: withBase('/configuration/docker/', import.meta.env.BASE_URL),
+ },
+};
+---
+
+
+
+
+ {
+ Object.entries(launcherDownloadOptions).map(
+ ([key, { icon, headline, note, launcherDownloadLink, launcherDownloadLabel }]) => (
+
+
+
{headline}
+
{note}
+
+
+
+ {launcherDownloadLabel}
+
+
+
+ ),
+ )
+ }
+
+
+
+
+ OR
+
+
+
+
+ {
+ Object.entries(manualDownloadOptions).map(([key, { headline, href, description }]) => (
+
+ ))
+ }
+
+
+
+
+
+
diff --git a/docs/src/lib/components/EmptyComponent.astro b/docs/src/lib/components/EmptyComponent.astro
new file mode 100644
index 00000000000..a04846e64d7
--- /dev/null
+++ b/docs/src/lib/components/EmptyComponent.astro
@@ -0,0 +1,3 @@
+---
+// This is used to override starlight components we don't want to use
+---
diff --git a/docs/src/lib/components/Footer.astro b/docs/src/lib/components/Footer.astro
new file mode 100644
index 00000000000..65137dec3b9
--- /dev/null
+++ b/docs/src/lib/components/Footer.astro
@@ -0,0 +1,28 @@
+---
+import PageFooter from '@astrojs/starlight/components/Footer.astro';
+---
+
+
+
+
+
+
diff --git a/docs/src/lib/components/ForceDarkTheme.astro b/docs/src/lib/components/ForceDarkTheme.astro
new file mode 100644
index 00000000000..f9c102a1ce8
--- /dev/null
+++ b/docs/src/lib/components/ForceDarkTheme.astro
@@ -0,0 +1,12 @@
+---
+
+---
+
+
+
diff --git a/docs/src/lib/components/InvocationContextDocs.astro b/docs/src/lib/components/InvocationContextDocs.astro
new file mode 100644
index 00000000000..851ac8a25b1
--- /dev/null
+++ b/docs/src/lib/components/InvocationContextDocs.astro
@@ -0,0 +1,324 @@
+---
+import invocationContext from '../../generated/invocation-context.json';
+
+/** Strip "Interface" suffix for the access path hint, e.g. "ImagesInterface" -> "context.images" */
+const accessPath = (name: string) => {
+ const stripped = name.replace(/Interface$/, '').toLowerCase();
+ return `context.${stripped}`;
+};
+
+/** Build a URL-friendly anchor id from an interface name, e.g. "ImagesInterface" -> "imagesinterface" */
+const ifaceId = (name: string) => name.toLowerCase();
+
+/** Build a URL-friendly anchor id for a method, e.g. ("ImagesInterface","get_dto") -> "imagesinterface--get_dto" */
+const methodId = (ifaceName: string, methodName: string) =>
+ `${ifaceName.toLowerCase()}--${methodName}`;
+
+/** Lightweight markdown-to-HTML for docstring content.
+ * Handles: fenced code blocks (```lang ... ```), inline `code`, **bold**, and paragraphs. */
+const miniMarkdown = (text: string): string => {
+ // HTML-escape first
+ let s = text.replace(/&/g, '&').replace(//g, '>');
+ // Fenced code blocks: ```lang\n...\n```
+ s = s.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) => {
+ return `${code.replace(/^\n|\n$/g, '')} `;
+ });
+ // Inline code: `...`
+ s = s.replace(/`([^`]+)`/g, '$1');
+ // Bold: **...**
+ s = s.replace(/\*\*([^*]+)\*\*/g, '$1 ');
+ // Convert double newlines to paragraph breaks
+ s = s.replace(/\n\n+/g, '
');
+ return `
${s}
`;
+};
+
+/** Inline-only markdown: just backtick `code` and **bold**, no block elements. */
+const inlineMarkdown = (text: string): string => {
+ let s = text.replace(/&/g, '&').replace(//g, '>');
+ s = s.replace(/`([^`]+)`/g, '$1');
+ s = s.replace(/\*\*([^*]+)\*\*/g, '$1 ');
+ return s;
+};
+
+/** Get the first sentence/line of a description for the summary table. */
+const summaryDesc = (desc: string) => {
+ if (!desc) return '';
+ // Take up to first newline or period+space
+ const nl = desc.indexOf('\n');
+ const trimmed = nl !== -1 ? desc.substring(0, nl) : desc;
+ return trimmed;
+};
+---
+
+{invocationContext.interfaces.map((iface) => (
+
+
+ {iface.name}
+ {accessPath(iface.name)}
+
+
+ {iface.description &&
}
+
+ {/* ── Summary table ── */}
+
+
+
+ Method
+ Description
+
+
+
+ {iface.methods.map((method) => (
+
+ {method.name}
+
+
+ ))}
+
+
+
+ {/* ── Per-method details ── */}
+ {iface.methods.map((method) => (
+
+
{method.name}
+
+
{method.signature}
+
+ {method.description && (
+
+ )}
+
+ {method.parameters.length > 0 && (
+ <>
+
Parameters
+
+
+
+ Name
+ Type
+ Description
+ Default
+
+
+
+ {method.parameters.map((param) => (
+
+ {param.name}
+ {param.type || '—'}
+
+ {param.default ? {param.default} : required }
+
+ ))}
+
+
+ >
+ )}
+
+ {(method.returns || method.return_type) && (
+ <>
+
Returns
+
+
+
+ Type
+ Description
+
+
+
+
+ {method.return_type || '—'}
+
+
+
+
+ >
+ )}
+
+ ))}
+
+))}
+
+
diff --git a/docs/src/lib/components/Link.astro b/docs/src/lib/components/Link.astro
new file mode 100644
index 00000000000..cca88478340
--- /dev/null
+++ b/docs/src/lib/components/Link.astro
@@ -0,0 +1,23 @@
+---
+type Props = {
+ href: string;
+ label?: string;
+ [key: string]: any;
+};
+
+const { href, label, ...rest } = Astro.props as Props;
+
+const useSlot = !!Astro.slots.has('default');
+const isExternal = /^https?:\/\//.test(href);
+---
+
+
+ {useSlot ? : label}
+
diff --git a/docs/src/lib/components/Mermaid.astro b/docs/src/lib/components/Mermaid.astro
new file mode 100644
index 00000000000..18a59eba203
--- /dev/null
+++ b/docs/src/lib/components/Mermaid.astro
@@ -0,0 +1,58 @@
+---
+type Props = {
+ title?: string;
+};
+
+const { title = '' } = Astro.props as Props;
+---
+
+
+
+
+ {title}
+
+ Loading diagram...
+
+
+ Source
+
+
+
diff --git a/docs/src/lib/components/SettingsDocs.astro b/docs/src/lib/components/SettingsDocs.astro
new file mode 100644
index 00000000000..c539b27cd46
--- /dev/null
+++ b/docs/src/lib/components/SettingsDocs.astro
@@ -0,0 +1,231 @@
+---
+import settingsData from '../../generated/settings.json';
+
+const groupedSettings = Object.entries(
+ settingsData.settings.reduce((groups, setting) => {
+ const category = setting.category || 'OTHER';
+ if (!groups[category]) {
+ groups[category] = [];
+ }
+ groups[category].push(setting);
+ return groups;
+ }, {}),
+);
+
+const formatValue = (value) => {
+ if (value === null) {
+ return 'null';
+ }
+ if (Array.isArray(value) || typeof value === 'object') {
+ return JSON.stringify(value);
+ }
+ return String(value);
+};
+
+/** Clean up Python type representations for display */
+const formatType = (typeStr) => {
+ // Strip wrapper
+ const classMatch = typeStr.match(/^$/);
+ if (classMatch) {
+ const inner = classMatch[1];
+ // Strip module paths (e.g. pathlib.Path -> Path)
+ return inner.split('.').pop();
+ }
+ // Strip typing. prefix
+ let cleaned = typeStr.replace(/typing\./g, '');
+ // Strip module paths inside brackets
+ cleaned = cleaned.replace(/[a-z_][a-z0-9_.]*\.([A-Z][A-Za-z]*)/g, '$1');
+ return cleaned;
+};
+
+/** Format category name: "MODEL INSTALL" -> "Model Install" */
+const formatCategoryName = (category) => {
+ return category
+ .split(' ')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(' ');
+};
+---
+
+{groupedSettings.map(([category, settings]) => (
+
+
+ {formatCategoryName(category)}
+ {settings.length}
+
+
+ {settings.map((setting) => (
+
+
+
+ {setting.name}
+
+
+
+ Type
+ {formatType(setting.type)}
+
+
+ Default
+ {formatValue(setting.default)}
+
+
+ Env
+ {setting.env_var}
+
+
+
+ {setting.description}
+ {setting.literal_values.length > 0 && (
+
+ Values: {setting.literal_values.map((v) => {formatValue(v)}).reduce((prev, curr) => [prev, ' ', curr])}
+
+ )}
+ {Object.keys(setting.validation).length > 0 && (
+
+ {Object.entries(setting.validation).map(([key, value]) => (
+ {key}={formatValue(value)}
+ )).reduce((prev, curr) => [prev, ' ', curr])}
+
+ )}
+
+
+ ))}
+
+
+))}
+
+
diff --git a/docs/src/lib/components/SplashGallery.astro b/docs/src/lib/components/SplashGallery.astro
new file mode 100644
index 00000000000..a0293e48ff6
--- /dev/null
+++ b/docs/src/lib/components/SplashGallery.astro
@@ -0,0 +1,263 @@
+---
+// Community Gallery Marquee
+
+import {Image} from 'astro:assets';
+
+import linearview from '../../content/docs/workflows/assets/linearview.png';
+import workflowLibrary from '../../content/docs/workflows/assets/workflow_library.png';
+import groupsMultigenSeeding from '../../content/docs/workflows/assets/groupsmultigenseeding.png';
+import groupsImgVae from '../../content/docs/workflows/assets/groupsimgvae.png';
+import groupsConditioning from '../../content/docs/workflows/assets/groupsconditioning.png';
+import groupsControl from '../../content/docs/workflows/assets/groupscontrol.png';
+import groupsLora from '../../content/docs/workflows/assets/groupslora.png';
+import groupsNoise from '../../content/docs/workflows/assets/groupsnoise.png';
+import groupsIterate from '../../content/docs/workflows/assets/groupsiterate.png';
+
+const placeholderSocials = [
+ { label: 'Instagram', href: 'https://example.com/instagram' },
+ { label: 'X', href: 'https://example.com/x' },
+ { label: 'ArtStation', href: 'https://example.com/artstation' },
+];
+
+const rows = [
+ [
+ {
+ id: 'linearview',
+ image: linearview,
+ alt: 'Linear workflow editor view',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'workflow-library',
+ image: workflowLibrary,
+ alt: 'Workflow library browser view',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'conditioning',
+ image: groupsConditioning,
+ alt: 'Workflow conditioning group',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'img-vae',
+ image: groupsImgVae,
+ alt: 'Workflow image and VAE group',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'multigen',
+ image: groupsMultigenSeeding,
+ alt: 'Workflow multigen seeding group',
+ socials: placeholderSocials,
+ },
+ ],
+ [
+ {
+ id: 'control',
+ image: groupsControl,
+ alt: 'Workflow control group',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'lora',
+ image: groupsLora,
+ alt: 'Workflow LoRA group',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'noise',
+ image: groupsNoise,
+ alt: 'Workflow noise group',
+ socials: placeholderSocials,
+ },
+ {
+ id: 'iterate',
+ image: groupsIterate,
+ alt: 'Workflow iterate group',
+ socials: placeholderSocials,
+ },
+ ],
+];
+---
+
+
+ {rows.map((row, rowIndex) => (
+
+
+
+ {[...Array(2)].map((_, segmentIndex) => (
+
+ {row.map((tile) => (
+
+
+
+ Submitted by
+
+
+
+ ))}
+
+ ))}
+
+
+ ))}
+
+
+
diff --git a/docs/src/lib/components/SystemRequirmentsLink.astro b/docs/src/lib/components/SystemRequirmentsLink.astro
new file mode 100644
index 00000000000..16455b8e749
--- /dev/null
+++ b/docs/src/lib/components/SystemRequirmentsLink.astro
@@ -0,0 +1,10 @@
+---
+import { LinkCard } from '@astrojs/starlight/components';
+import { withBase } from '../base-path';
+---
+
+
diff --git a/docs/src/pages/download.astro b/docs/src/pages/download.astro
new file mode 100644
index 00000000000..1160566dadc
--- /dev/null
+++ b/docs/src/pages/download.astro
@@ -0,0 +1,17 @@
+---
+import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
+import DownloadOptions from '@components/DownloadOptions.astro';
+---
+
+
+
+
diff --git a/docs/src/styles/custom.css b/docs/src/styles/custom.css
new file mode 100644
index 00000000000..abb93263edd
--- /dev/null
+++ b/docs/src/styles/custom.css
@@ -0,0 +1,397 @@
+:root {
+ /* Page Layout */
+ --sl-content-width: 84ch;
+
+ /* Typography */
+ --__sl-font: 'Inter', sans-serif;
+ --__sl-font-mono:
+ 'Roboto Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+
+ --radius: 0.35rem;
+
+ /* Colors */
+ --sl-color-bg: #1c1f23;
+ --sl-color-bg-nav: #31343b;
+ --sl-color-bg-sidebar: #272a2f;
+
+ --sl-color-gray-7: #272a2f;
+
+ --sl-color-hairline: rgba(255, 255, 255, 0.08);
+ --sl-color-hairline-light: rgba(255, 255, 255, 0.16);
+
+ --sl-color-text-accent: #97d2ee;
+ --sl-color-text-accent-2: #e4fd1d;
+
+ --sl-color-accent-2-rgb: 228, 253, 29;
+}
+
+html,
+body {
+ scroll-behavior: smooth;
+}
+
+.text-xs {
+ font-size: var(--sl-text-xs);
+}
+
+[data-has-hero] {
+ header {
+ background-color: var(--sl-color-bg);
+ border-color: transparent;
+
+ .header {
+ max-width: calc(var(--sl-content-width) + 8rem);
+ margin-inline: auto;
+ }
+ }
+}
+
+.site-title {
+ transition: transform 100ms ease-in-out;
+
+ &:hover {
+ transform: scale(1.02);
+ }
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+.hero {
+ padding-top: clamp(2.5rem, calc(1rem + 10vmin), 5rem);
+ padding-bottom: clamp(2.5rem, calc(1rem + 10vmin), 10rem);
+
+ &:has(> :only-child) {
+ grid-template-columns: 1fr;
+ gap: 0;
+
+ .sl-flex {
+ align-items: center;
+ text-align: center;
+ }
+ }
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ .title--wrapper {
+ flex-shrink: 0;
+ }
+
+ /* Site Search Container */
+ .sl-flex:has(site-search) {
+ order: 2;
+ justify-content: end;
+ width: 100%;
+ max-width: 22rem;
+
+ @media (max-width: 799px) {
+ width: auto;
+ }
+ }
+
+ /* Social Items */
+ > :last-child:has(> .social-icons) {
+ order: 1;
+ margin-left: auto;
+ justify-content: end;
+ }
+}
+
+.page {
+ background-image: radial-gradient(circle, var(--sl-color-hairline) 1px, transparent 1px);
+ background-size: 20px 20px;
+}
+
+.right-sidebar-container {
+ background: var(--sl-color-bg);
+}
+
+#starlight__sidebar .sidebar-content {
+ a {
+ transition:
+ background 50ms ease-in-out,
+ color 50ms ease-in-out;
+
+ &:not([aria-current='page']):hover {
+ background: var(--sl-color-hairline);
+ }
+ &:not([aria-current='page']):active {
+ background: var(--sl-color-hairline-light);
+ }
+
+ &[aria-current='page'] span,
+ span {
+ font-weight: normal;
+ }
+ }
+}
+
+site-search > button {
+ transition: border-color 100ms ease-in-out;
+}
+
+.sl-link-button {
+ border-radius: 0.5rem;
+}
+
+.sl-link-card {
+ background: var(--sl-color-bg);
+ transition: border-color 100ms ease-in-out;
+
+ &:active {
+ border-color: var(--sl-color-hairline-light);
+ }
+
+ svg {
+ transition: color 100ms ease-in-out;
+ }
+}
+
+.expressive-code .frame pre {
+ background: var(--sl-color-bg-sidebar);
+}
+
+.expressive-code .has-title {
+ .header {
+ border-bottom: var(--ec-brdWd) solid var(--ec-brdCol);
+ }
+ .header .title {
+ border-inline: var(--ec-brdWd) solid var(--ec-brdCol);
+ border-top: var(--ec-brdWd) solid var(--ec-brdCol);
+ background: var(--sl-color-bg-sidebar);
+ font-family: var(--__sl-font-mono);
+ font-size: var(--sl-text-xs);
+ padding: calc(var(--ec-uiPadBlk) + var(--ec-frm-edActTabIndHt)) var(--ec-uiPadInl);
+ cursor: pointer;
+
+ &::after {
+ display: none;
+ }
+
+ &::before {
+ position: absolute;
+ content: '';
+ inset: 0;
+ background: var(--sl-color-hairline);
+ opacity: 0;
+ transition: opacity 75ms ease-in-out;
+ }
+
+ &:hover::before {
+ opacity: 1;
+ }
+ }
+}
+
+ul[role='tablist'] {
+ border-bottom: 1px solid var(--sl-color-hairline);
+}
+
+a[role='tab'] {
+ border: none;
+ padding: 0.275rem 0.5rem;
+ transition: all 100ms ease;
+ border-radius: var(--radius);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-width: 2px;
+ border-bottom-style: solid;
+ box-shadow: none;
+
+ &:not([aria-selected='true']) {
+ border-color: transparent;
+ color: var(--sl-color-text);
+ }
+
+ &:not([aria-selected='true']):hover {
+ background: var(--sl-color-hairline);
+ }
+
+ &:not([aria-selected='true']):active {
+ background: var(--sl-color-hairline-light);
+ }
+
+ &[aria-selected='true'] {
+ font-weight: normal;
+ --sl-tab-color-border: var(--sl-color-text-accent);
+ color: var(--sl-color-text-accent);
+ }
+}
+
+/* Decorate tabs with parent aside colors */
+aside a[role='tab'] {
+ &[aria-selected='true'] {
+ --sl-tab-color-border: var(--sl-color-asides-border);
+ color: var(--sl-color-asides-text-accent);
+ }
+}
+
+a[rel='next'],
+a[rel='prev'] {
+ background: var(--sl-color-bg);
+ transition: border-color 100ms ease-in-out;
+}
+
+.sl-steps {
+ & > li::before {
+ font-family: var(--__sl-font-mono);
+ }
+}
+
+article.card {
+ border-radius: var(--radius);
+
+ padding: clamp(1rem, calc(0.125rem + 3vw), 1.5rem);
+}
+
+.starlight-aside {
+ border-radius: var(--radius);
+ border: none;
+ position: relative;
+ padding: 0.75rem;
+ padding-left: 1.5rem;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0.35rem;
+ width: 0.25rem;
+ inset-block: 0.35rem;
+ border-radius: 999px;
+ background: var(--sl-color-asides-border);
+ }
+}
+
+.hero .actions {
+ gap: 1.5rem;
+}
+
+.card .sl-link-button {
+ margin-bottom: 0;
+}
+
+.sl-link-button {
+ position: relative;
+ overflow: hidden;
+ transition: transform 100ms ease-in-out;
+
+ &:not(.minimal) {
+ padding: 0.65rem 0.85rem;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: var(--sl-color-hairline);
+ opacity: 0;
+ transition: opacity 75ms ease-in-out;
+ pointer-events: none;
+ }
+ }
+
+ &:hover::before {
+ opacity: 1;
+ }
+
+ &:hover,
+ &:focus-visible {
+ transform: scale(1.02);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+
+ &.primary::before {
+ background: rgba(0, 0, 0, 0.12);
+ }
+
+ &.secondary {
+ border-color: var(--sl-color-gray-5);
+ }
+}
+
+/* Contextual Menu */
+#contextual-menu-container {
+
+ & > button {
+ transition: background 100ms ease-in-out;
+ }
+
+ #contextual-dropdown-menu button {
+ transition: background 75ms ease-in-out;
+ }
+}
+
+/* TODO: Custom markdown content styles */
+.sl-markdown-content {
+ table {
+ :is(th:first-child, td:first-child):not(:where(.not-content *)) {
+ padding: 0.5rem 1rem;
+ }
+ tr:hover {
+ background-color: var(--sl-color-bg-sidebar);
+ }
+ }
+}
+
+/* Splash Page-specific styles */
+
+@keyframes splash-animate {
+ 0% { background-position: 0% 0%; }
+ 50% { background-position: 0% 100%; }
+ 100% { background-position: 0% 0%; }
+}
+
+.splash-img {
+ --thickness: 2px;
+
+ position: relative;
+
+ img {
+ position: relative;
+ z-index: 2;
+ border-radius: var(--radius);
+ }
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ display: block;
+ background-size: 100% 200%;
+ animation: splash-animate 6s ease-in-out infinite;
+ }
+
+ /* Border */
+ &::before {
+ z-index: 1;
+ inset: calc(-1 * var(--thickness));
+ border-radius: calc(var(--radius) + var(--thickness));
+ background-image: linear-gradient(
+ rgb(var(--sl-color-accent-2-rgb)),
+ rgb(var(--sl-color-accent-2-rgb), 0) 80%
+ );
+ }
+
+ /* Glow */
+ &::after {
+ z-index: 0;
+ /* Mirror the border's gradient */
+ background-image: linear-gradient(
+ rgb(var(--sl-color-accent-2-rgb)),
+ rgba(var(--sl-color-accent-2-rgb), 0) 70%
+ );
+ /* Diffuse the gradient into a glow */
+ filter: blur(24px);
+ inset: -6px;
+ opacity: 0.7;
+ border-radius: calc(var(--radius) * 3);
+ opacity: 0.65;
+ }
+}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
new file mode 100644
index 00000000000..555a061bdcb
--- /dev/null
+++ b/docs/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "paths": {
+ "@/*": ["./*"],
+ "@lib/*": ["./src/lib/*"],
+ "@utils/*": ["./src/lib/utils/*"],
+ "@components/*": ["./src/lib/components/*"],
+ },
+ },
+}
diff --git a/environment-mac.yaml b/environment-mac.yaml
deleted file mode 100644
index 44cd1efcd6f..00000000000
--- a/environment-mac.yaml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: ldm
-channels:
- - pytorch-nightly
- - conda-forge
-dependencies:
- - python==3.9.13
- - pip==22.2.2
-
- # pytorch-nightly, left unpinned
- - pytorch
- - torchmetrics
- - torchvision
-
- # I suggest to keep the other deps sorted for convenience.
- # If you wish to upgrade to 3.10, try to run this:
- #
- # ```shell
- # CONDA_CMD=conda
- # sed -E 's/python==3.9.13/python==3.10.5/;s/ldm/ldm-3.10/;21,99s/- ([^=]+)==.+/- \1/' environment-mac.yaml > /tmp/environment-mac-updated.yml
- # CONDA_SUBDIR=osx-arm64 $CONDA_CMD env create -f /tmp/environment-mac-updated.yml && $CONDA_CMD list -n ldm-3.10 | awk ' {print " - " $1 "==" $2;} '
- # ```
- #
- # Unfortunately, as of 2022-08-31, this fails at the pip stage.
- - albumentations==1.2.1
- - coloredlogs==15.0.1
- - einops==0.4.1
- - grpcio==1.46.4
- - humanfriendly
- - imageio-ffmpeg==0.4.7
- - imageio==2.21.2
- - imgaug==0.4.0
- - kornia==0.6.7
- - mpmath==1.2.1
- - nomkl
- - numpy==1.23.2
- - omegaconf==2.1.1
- - onnx==1.12.0
- - onnxruntime==1.12.1
- - opencv==4.6.0
- - pudb==2022.1
- - pytorch-lightning==1.6.5
- - scipy==1.9.1
- - streamlit==1.12.2
- - sympy==1.10.1
- - tensorboard==2.9.0
- - transformers==4.21.2
- - pip:
- - invisible-watermark
- - test-tube
- - tokenizers
- - torch-fidelity
- - -e git+https://github.com/huggingface/diffusers.git@v0.2.4#egg=diffusers
- - -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers
- - -e git+https://github.com/openai/CLIP.git@main#egg=clip
- - -e git+https://github.com/Birch-san/k-diffusion.git@mps#egg=k_diffusion
- - -e .
-variables:
- PYTORCH_ENABLE_MPS_FALLBACK: 1
diff --git a/environment.yaml b/environment.yaml
deleted file mode 100644
index 7d5b4fe9e35..00000000000
--- a/environment.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: ldm
-channels:
- - pytorch
- - defaults
-dependencies:
- - python=3.8.5
- - pip=20.3
- - cudatoolkit=11.3
- - pytorch=1.11.0
- - torchvision=0.12.0
- - numpy=1.19.2
- - pip:
- - albumentations==0.4.3
- - opencv-python==4.1.2.30
- - pudb==2019.2
- - imageio==2.9.0
- - imageio-ffmpeg==0.4.2
- - pytorch-lightning==1.4.2
- - omegaconf==2.1.1
- - test-tube>=0.7.5
- - streamlit==1.12.0
- - pillow==9.2.0
- - einops==0.3.0
- - torch-fidelity==0.3.0
- - transformers==4.19.2
- - torchmetrics==0.6.0
- - kornia==0.6.0
- - -e git+https://github.com/openai/CLIP.git@main#egg=clip
- - -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers
- - -e git+https://github.com/lstein/k-diffusion.git@master#egg=k-diffusion
- - -e .
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000000..dedd56e74f3
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,25 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1727955264,
+ "narHash": "sha256-lrd+7mmb5NauRoMa8+J1jFKYVa+rc8aq2qc9+CxPDKc=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "71cd616696bd199ef18de62524f3df3ffe8b9333",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000000..07af19e93bf
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,91 @@
+# Important note: this flake does not attempt to create a fully isolated, 'pure'
+# Python environment for InvokeAI. Instead, it depends on local invocations of
+# virtualenv/pip to install the required (binary) packages, most importantly the
+# prebuilt binary pytorch packages with CUDA support.
+# ML Python packages with CUDA support, like pytorch, are notoriously expensive
+# to compile so it's purposefuly not what this flake does.
+
+{
+ description = "An (impure) flake to develop on InvokeAI.";
+
+ outputs = { self, nixpkgs }:
+ let
+ system = "x86_64-linux";
+ pkgs = import nixpkgs {
+ inherit system;
+ config.allowUnfree = true;
+ };
+
+ python = pkgs.python310;
+
+ mkShell = { dir, install }:
+ let
+ setupScript = pkgs.writeScript "setup-invokai" ''
+ # This must be sourced using 'source', not executed.
+ ${python}/bin/python -m venv ${dir}
+ ${dir}/bin/python -m pip install ${install}
+ # ${dir}/bin/python -c 'import torch; assert(torch.cuda.is_available())'
+ source ${dir}/bin/activate
+ '';
+ in
+ pkgs.mkShell rec {
+ buildInputs = with pkgs; [
+ # Backend: graphics, CUDA.
+ cudaPackages.cudnn
+ cudaPackages.cuda_nvrtc
+ cudatoolkit
+ pkg-config
+ libconfig
+ cmake
+ blas
+ freeglut
+ glib
+ gperf
+ procps
+ libGL
+ libGLU
+ linuxPackages.nvidia_x11
+ python
+ (opencv4.override {
+ enableGtk3 = true;
+ enableFfmpeg = true;
+ enableCuda = true;
+ enableUnfree = true;
+ })
+ stdenv.cc
+ stdenv.cc.cc.lib
+ xorg.libX11
+ xorg.libXext
+ xorg.libXi
+ xorg.libXmu
+ xorg.libXrandr
+ xorg.libXv
+ zlib
+
+ # Pre-commit hooks.
+ black
+
+ # Frontend.
+ pnpm_8
+ nodejs
+ ];
+ LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
+ CUDA_PATH = pkgs.cudatoolkit;
+ EXTRA_LDFLAGS = "-L${pkgs.linuxPackages.nvidia_x11}/lib";
+ shellHook = ''
+ if [[ -f "${dir}/bin/activate" ]]; then
+ source "${dir}/bin/activate"
+ echo "Using Python: $(which python)"
+ else
+ echo "Use 'source ${setupScript}' to set up the environment."
+ fi
+ '';
+ };
+ in
+ {
+ devShells.${system} = rec {
+ develop = mkShell { dir = "venv"; install = "-e '.[xformers]' --extra-index-url https://download.pytorch.org/whl/cu118"; };
+ default = develop;
+ };
+ };
+}
diff --git a/ldm/models/diffusion/__init__.py b/invokeai/__init__.py
similarity index 100%
rename from ldm/models/diffusion/__init__.py
rename to invokeai/__init__.py
diff --git a/ldm/modules/diffusionmodules/__init__.py b/invokeai/app/__init__.py
similarity index 100%
rename from ldm/modules/diffusionmodules/__init__.py
rename to invokeai/app/__init__.py
diff --git a/invokeai/app/api/auth_dependencies.py b/invokeai/app/api/auth_dependencies.py
new file mode 100644
index 00000000000..1df1ed6e250
--- /dev/null
+++ b/invokeai/app/api/auth_dependencies.py
@@ -0,0 +1,166 @@
+"""FastAPI dependencies for authentication."""
+
+from typing import Annotated
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.auth.token_service import TokenData, verify_token
+from invokeai.backend.util.logging import logging
+
+logger = logging.getLogger(__name__)
+
+# HTTP Bearer token security scheme
+security = HTTPBearer(auto_error=False)
+
+
+async def get_current_user(
+ credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
+) -> TokenData:
+ """Get current authenticated user from Bearer token.
+
+ Note: This function accesses ApiDependencies.invoker.services.users directly,
+ which is the established pattern in this codebase. The ApiDependencies.invoker
+ is initialized in the FastAPI lifespan context before any requests are handled.
+
+ Args:
+ credentials: The HTTP authorization credentials containing the Bearer token
+
+ Returns:
+ TokenData containing user information from the token
+
+ Raises:
+ HTTPException: If token is missing, invalid, or expired (401 Unauthorized)
+ """
+ if credentials is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Missing authentication credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ token = credentials.credentials
+ token_data = verify_token(token)
+
+ if token_data is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid or expired authentication token",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ # Verify user still exists and is active
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.get(token_data.user_id)
+
+ if user is None or not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User account is inactive or does not exist",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ return token_data
+
+
+async def get_current_user_or_default(
+ credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
+) -> TokenData:
+ """Get current authenticated user from Bearer token, or return a default system user if not authenticated.
+
+ This dependency is useful for endpoints that should work in both single-user and multiuser modes.
+
+ When multiuser mode is disabled (default), this always returns a system user with admin privileges,
+ allowing unrestricted access to all operations.
+
+ When multiuser mode is enabled, authentication is required and this function validates the token,
+ returning authenticated user data or raising 401 Unauthorized if no valid credentials are provided.
+
+ Args:
+ credentials: The HTTP authorization credentials containing the Bearer token
+
+ Returns:
+ TokenData containing user information from the token, or system user in single-user mode
+
+ Raises:
+ HTTPException: 401 Unauthorized if in multiuser mode and credentials are missing, invalid, or user is inactive
+ """
+ # Get configuration to check if multiuser is enabled
+ config = ApiDependencies.invoker.services.configuration
+
+ # In single-user mode (multiuser=False), always return system user with admin privileges
+ if not config.multiuser:
+ return TokenData(user_id="system", email="system@system.invokeai", is_admin=True)
+
+ # Multiuser mode is enabled - validate credentials
+ if credentials is None:
+ # In multiuser mode, authentication is required
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
+
+ token = credentials.credentials
+ token_data = verify_token(token)
+
+ if token_data is None:
+ # Invalid token in multiuser mode - reject
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
+
+ # Verify user still exists and is active
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.get(token_data.user_id)
+
+ if user is None or not user.is_active:
+ # User doesn't exist or is inactive in multiuser mode - reject
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive")
+
+ return token_data
+
+
+async def require_admin(
+ current_user: Annotated[TokenData, Depends(get_current_user)],
+) -> TokenData:
+ """Require admin role for the current user.
+
+ Args:
+ current_user: The current authenticated user's token data
+
+ Returns:
+ The token data if user is an admin
+
+ Raises:
+ HTTPException: If user does not have admin privileges (403 Forbidden)
+ """
+ if not current_user.is_admin:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
+ return current_user
+
+
+async def require_admin_or_default(
+ current_user: Annotated[TokenData, Depends(get_current_user_or_default)],
+) -> TokenData:
+ """Require admin role for the current user, or return default system admin in single-user mode.
+
+ This dependency is useful for admin-only endpoints that should work in both single-user and multiuser modes.
+
+ When multiuser mode is disabled (default), this always returns a system user with admin privileges.
+ When multiuser mode is enabled, this validates that the authenticated user has admin privileges.
+
+ Args:
+ current_user: The current authenticated user's token data (or default system user)
+
+ Returns:
+ The token data if user is an admin (or system user in single-user mode)
+
+ Raises:
+ HTTPException: If user does not have admin privileges (403 Forbidden) in multiuser mode
+ """
+ if not current_user.is_admin:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
+ return current_user
+
+
+# Type aliases for convenient use in route dependencies
+CurrentUser = Annotated[TokenData, Depends(get_current_user)]
+CurrentUserOrDefault = Annotated[TokenData, Depends(get_current_user_or_default)]
+AdminUser = Annotated[TokenData, Depends(require_admin)]
+AdminUserOrDefault = Annotated[TokenData, Depends(require_admin_or_default)]
diff --git a/invokeai/app/api/dependencies.py b/invokeai/app/api/dependencies.py
new file mode 100644
index 00000000000..e7468c1bca4
--- /dev/null
+++ b/invokeai/app/api/dependencies.py
@@ -0,0 +1,242 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+import asyncio
+from logging import Logger
+
+import torch
+
+from invokeai.app.services.app_settings import AppSettingsService
+from invokeai.app.services.auth.token_service import set_jwt_secret
+from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
+from invokeai.app.services.board_images.board_images_default import BoardImagesService
+from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
+from invokeai.app.services.boards.boards_default import BoardService
+from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
+from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.download.download_default import DownloadQueueService
+from invokeai.app.services.events.events_fastapievents import FastAPIEventService
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+from invokeai.app.services.external_generation.providers import (
+ AlibabaCloudProvider,
+ GeminiProvider,
+ OpenAIProvider,
+ SeedreamProvider,
+)
+from invokeai.app.services.external_generation.startup import sync_configured_external_starter_models
+from invokeai.app.services.image_files.image_files_disk import DiskImageFileStorage
+from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
+from invokeai.app.services.images.images_default import ImageService
+from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
+from invokeai.app.services.invocation_services import InvocationServices
+from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_images.model_images_default import ModelImageFileStorageDisk
+from invokeai.app.services.model_manager.model_manager_default import ModelManagerService
+from invokeai.app.services.model_records.model_records_sql import ModelRecordServiceSQL
+from invokeai.app.services.model_relationship_records.model_relationship_records_sqlite import (
+ SqliteModelRelationshipRecordStorage,
+)
+from invokeai.app.services.model_relationships.model_relationships_default import ModelRelationshipsService
+from invokeai.app.services.names.names_default import SimpleNameService
+from invokeai.app.services.object_serializer.object_serializer_disk import ObjectSerializerDisk
+from invokeai.app.services.object_serializer.object_serializer_forward_cache import ObjectSerializerForwardCache
+from invokeai.app.services.session_processor.session_processor_default import (
+ DefaultSessionProcessor,
+ DefaultSessionRunner,
+)
+from invokeai.app.services.session_queue.session_queue_sqlite import SqliteSessionQueue
+from invokeai.app.services.shared.sqlite.sqlite_util import init_db
+from invokeai.app.services.style_preset_images.style_preset_images_disk import StylePresetImageFileStorageDisk
+from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage
+from invokeai.app.services.urls.urls_default import LocalUrlService
+from invokeai.app.services.users.users_default import UserService
+from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
+from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_disk import WorkflowThumbnailFileStorageDisk
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ AnimaConditioningInfo,
+ BasicConditioningInfo,
+ CogView4ConditioningInfo,
+ ConditioningFieldData,
+ FLUXConditioningInfo,
+ QwenImageConditioningInfo,
+ SD3ConditioningInfo,
+ SDXLConditioningInfo,
+ ZImageConditioningInfo,
+)
+from invokeai.backend.util.logging import InvokeAILogger
+from invokeai.version.invokeai_version import __version__
+
+
+# TODO: is there a better way to achieve this?
+def check_internet() -> bool:
+ """
+ Return true if the internet is reachable.
+ It does this by pinging huggingface.co.
+ """
+ import urllib.request
+
+ host = "http://huggingface.co"
+ try:
+ urllib.request.urlopen(host, timeout=1)
+ return True
+ except Exception:
+ return False
+
+
+logger = InvokeAILogger.get_logger()
+
+
+class ApiDependencies:
+ """Contains and initializes all dependencies for the API"""
+
+ invoker: Invoker
+
+ @staticmethod
+ def initialize(
+ config: InvokeAIAppConfig,
+ event_handler_id: int,
+ loop: asyncio.AbstractEventLoop,
+ logger: Logger = logger,
+ ) -> None:
+ logger.info(f"InvokeAI version {__version__}")
+ logger.info(f"Root directory = {str(config.root_path)}")
+
+ output_folder = config.outputs_path
+ if output_folder is None:
+ raise ValueError("Output folder is not set")
+
+ image_files = DiskImageFileStorage(f"{output_folder}/images")
+
+ model_images_folder = config.models_path
+ style_presets_folder = config.style_presets_path
+ workflow_thumbnails_folder = config.workflow_thumbnails_path
+
+ db = init_db(config=config, logger=logger, image_files=image_files)
+
+ # Initialize JWT secret from database
+ app_settings = AppSettingsService(db=db)
+ jwt_secret = app_settings.get_jwt_secret()
+ set_jwt_secret(jwt_secret)
+ logger.info("JWT secret loaded from database")
+
+ configuration = config
+ logger = logger
+
+ board_image_records = SqliteBoardImageRecordStorage(db=db)
+ board_images = BoardImagesService()
+ board_records = SqliteBoardRecordStorage(db=db)
+ boards = BoardService()
+ events = FastAPIEventService(event_handler_id, loop=loop)
+ bulk_download = BulkDownloadService()
+ image_records = SqliteImageRecordStorage(db=db)
+ images = ImageService()
+ invocation_cache = MemoryInvocationCache(max_cache_size=config.node_cache_size)
+ tensors = ObjectSerializerForwardCache(
+ ObjectSerializerDisk[torch.Tensor](
+ output_folder / "tensors",
+ safe_globals=[torch.Tensor],
+ ephemeral=True,
+ ),
+ )
+ conditioning = ObjectSerializerForwardCache(
+ ObjectSerializerDisk[ConditioningFieldData](
+ output_folder / "conditioning",
+ safe_globals=[
+ ConditioningFieldData,
+ BasicConditioningInfo,
+ SDXLConditioningInfo,
+ FLUXConditioningInfo,
+ SD3ConditioningInfo,
+ CogView4ConditioningInfo,
+ ZImageConditioningInfo,
+ QwenImageConditioningInfo,
+ AnimaConditioningInfo,
+ ],
+ ephemeral=True,
+ ),
+ )
+ download_queue_service = DownloadQueueService(app_config=configuration, event_bus=events)
+ model_record_service = ModelRecordServiceSQL(db=db, logger=logger)
+ model_manager = ModelManagerService.build_model_manager(
+ app_config=configuration,
+ model_record_service=model_record_service,
+ download_queue=download_queue_service,
+ events=events,
+ )
+ external_generation = ExternalGenerationService(
+ providers={
+ AlibabaCloudProvider.provider_id: AlibabaCloudProvider(app_config=configuration, logger=logger),
+ GeminiProvider.provider_id: GeminiProvider(app_config=configuration, logger=logger),
+ OpenAIProvider.provider_id: OpenAIProvider(app_config=configuration, logger=logger),
+ SeedreamProvider.provider_id: SeedreamProvider(app_config=configuration, logger=logger),
+ },
+ logger=logger,
+ record_store=model_record_service,
+ )
+ model_images_service = ModelImageFileStorageDisk(model_images_folder / "model_images")
+ model_relationships = ModelRelationshipsService()
+ model_relationship_records = SqliteModelRelationshipRecordStorage(db=db)
+ names = SimpleNameService()
+ performance_statistics = InvocationStatsService()
+ session_processor = DefaultSessionProcessor(session_runner=DefaultSessionRunner())
+ session_queue = SqliteSessionQueue(db=db)
+ urls = LocalUrlService()
+ workflow_records = SqliteWorkflowRecordsStorage(db=db)
+ style_preset_records = SqliteStylePresetRecordsStorage(db=db)
+ style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images")
+ workflow_thumbnails = WorkflowThumbnailFileStorageDisk(workflow_thumbnails_folder)
+ client_state_persistence = ClientStatePersistenceSqlite(db=db)
+ users = UserService(db=db)
+
+ services = InvocationServices(
+ board_image_records=board_image_records,
+ board_images=board_images,
+ board_records=board_records,
+ boards=boards,
+ bulk_download=bulk_download,
+ configuration=configuration,
+ events=events,
+ image_files=image_files,
+ image_records=image_records,
+ images=images,
+ invocation_cache=invocation_cache,
+ logger=logger,
+ model_images=model_images_service,
+ model_manager=model_manager,
+ model_relationships=model_relationships,
+ model_relationship_records=model_relationship_records,
+ download_queue=download_queue_service,
+ external_generation=external_generation,
+ names=names,
+ performance_statistics=performance_statistics,
+ session_processor=session_processor,
+ session_queue=session_queue,
+ urls=urls,
+ workflow_records=workflow_records,
+ tensors=tensors,
+ conditioning=conditioning,
+ style_preset_records=style_preset_records,
+ style_preset_image_files=style_preset_image_files,
+ workflow_thumbnails=workflow_thumbnails,
+ client_state_persistence=client_state_persistence,
+ users=users,
+ )
+
+ ApiDependencies.invoker = Invoker(services)
+ configured_external_providers = {
+ provider_id
+ for provider_id, status in external_generation.get_provider_statuses().items()
+ if status.configured
+ }
+ sync_configured_external_starter_models(
+ configured_provider_ids=configured_external_providers,
+ model_manager=model_manager,
+ logger=logger,
+ )
+ db.clean()
+
+ @staticmethod
+ def shutdown() -> None:
+ if ApiDependencies.invoker:
+ ApiDependencies.invoker.stop()
diff --git a/invokeai/app/api/extract_metadata_from_image.py b/invokeai/app/api/extract_metadata_from_image.py
new file mode 100644
index 00000000000..054b3cc38cc
--- /dev/null
+++ b/invokeai/app/api/extract_metadata_from_image.py
@@ -0,0 +1,124 @@
+import json
+import logging
+from dataclasses import dataclass
+
+from PIL import Image
+
+from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutIDValidator
+
+
+@dataclass
+class ExtractedMetadata:
+ invokeai_metadata: str | None
+ invokeai_workflow: str | None
+ invokeai_graph: str | None
+
+
+def extract_metadata_from_image(
+ pil_image: Image.Image,
+ invokeai_metadata_override: str | None,
+ invokeai_workflow_override: str | None,
+ invokeai_graph_override: str | None,
+ logger: logging.Logger,
+) -> ExtractedMetadata:
+ """
+ Extracts the "invokeai_metadata", "invokeai_workflow", and "invokeai_graph" data embedded in the PIL Image.
+
+ These items are stored as stringified JSON in the image file's metadata, so we need to do some parsing to validate
+ them. Once parsed, the values are returned as they came (as strings), or None if they are not present or invalid.
+
+ In some situations, we may prefer to override the values extracted from the image file with some other values.
+
+ For example, when uploading an image via API, the client can optionally provide the metadata directly in the request,
+ as opposed to embedding it in the image file. In this case, the client-provided metadata will be used instead of the
+ metadata embedded in the image file.
+
+ Args:
+ pil_image: The PIL Image object.
+ invokeai_metadata_override: The metadata override provided by the client.
+ invokeai_workflow_override: The workflow override provided by the client.
+ invokeai_graph_override: The graph override provided by the client.
+ logger: The logger to use for debug logging.
+
+ Returns:
+ ExtractedMetadata: The extracted metadata, workflow, and graph.
+ """
+
+ # The fallback value for metadata is None.
+ stringified_metadata: str | None = None
+
+ # Use the metadata override if provided, else attempt to extract it from the image file.
+ metadata_raw = invokeai_metadata_override or pil_image.info.get("invokeai_metadata", None)
+
+ # If the metadata is present in the image file, we will attempt to parse it as JSON. When we create images,
+ # we always store metadata as a stringified JSON dict. So, we expect it to be a string here.
+ if isinstance(metadata_raw, str):
+ try:
+ # Must be a JSON string
+ metadata_parsed = json.loads(metadata_raw)
+ # Must be a dict
+ if isinstance(metadata_parsed, dict):
+ # Looks good, overwrite the fallback value
+ stringified_metadata = metadata_raw
+ except Exception as e:
+ logger.debug(f"Failed to parse metadata for uploaded image, {e}")
+ pass
+
+ # We expect the workflow, if embedded in the image, to be a JSON-stringified WorkflowWithoutID. We will store it
+ # as a string.
+ workflow_raw: str | None = invokeai_workflow_override or pil_image.info.get("invokeai_workflow", None)
+
+ # The fallback value for workflow is None.
+ stringified_workflow: str | None = None
+
+ # If the workflow is present in the image file, we will attempt to parse it as JSON. When we create images, we
+ # always store workflows as a stringified JSON WorkflowWithoutID. So, we expect it to be a string here.
+ if isinstance(workflow_raw, str):
+ try:
+ # Validate the workflow JSON before storing it
+ WorkflowWithoutIDValidator.validate_json(workflow_raw)
+ # Looks good, overwrite the fallback value
+ stringified_workflow = workflow_raw
+ except Exception:
+ logger.debug("Failed to parse workflow for uploaded image")
+ pass
+
+ # We expect the workflow, if embedded in the image, to be a JSON-stringified Graph. We will store it as a
+ # string.
+ graph_raw: str | None = invokeai_graph_override or pil_image.info.get("invokeai_graph", None)
+
+ # The fallback value for graph is None.
+ stringified_graph: str | None = None
+
+ # If the graph is present in the image file, we will attempt to parse it as JSON. When we create images, we
+ # always store graphs as a stringified JSON Graph. So, we expect it to be a string here.
+ if isinstance(graph_raw, str):
+ try:
+ # TODO(psyche): Due to pydantic's handling of None values, it is possible for the graph to fail validation,
+ # even if it is a direct dump of a valid graph. Node fields in the graph are allowed to have be unset if
+ # they have incoming connections, but something about the ser/de process cannot adequately handle this.
+ #
+ # In lieu of fixing the graph validation, we will just do a simple check here to see if the graph is dict
+ # with the correct keys. This is not a perfect solution, but it should be good enough for now.
+
+ # FIX ME: Validate the graph JSON before storing it
+ # Graph.model_validate_json(graph_raw)
+
+ # Crappy workaround to validate JSON
+ graph_parsed = json.loads(graph_raw)
+ if not isinstance(graph_parsed, dict):
+ raise ValueError("Not a dict")
+ if not isinstance(graph_parsed.get("nodes", None), dict):
+ raise ValueError("'nodes' is not a dict")
+ if not isinstance(graph_parsed.get("edges", None), list):
+ raise ValueError("'edges' is not a list")
+
+ # Looks good, overwrite the fallback value
+ stringified_graph = graph_raw
+ except Exception as e:
+ logger.debug(f"Failed to parse graph for uploaded image, {e}")
+ pass
+
+ return ExtractedMetadata(
+ invokeai_metadata=stringified_metadata, invokeai_workflow=stringified_workflow, invokeai_graph=stringified_graph
+ )
diff --git a/invokeai/app/api/no_cache_staticfiles.py b/invokeai/app/api/no_cache_staticfiles.py
new file mode 100644
index 00000000000..cbf82d99c71
--- /dev/null
+++ b/invokeai/app/api/no_cache_staticfiles.py
@@ -0,0 +1,50 @@
+from typing import Any
+
+from starlette.exceptions import HTTPException
+from starlette.responses import Response
+from starlette.staticfiles import StaticFiles
+from starlette.types import Scope
+
+
+class NoCacheStaticFiles(StaticFiles):
+ """
+ This class is used to override the default caching behavior of starlette for static files,
+ ensuring we *never* cache static files. It modifies the file response headers to strictly
+ never cache the files.
+
+ Static files include the javascript bundles, fonts, locales, and some images. Generated
+ images are not included, as they are served by a router.
+
+ This class also implements proper SPA (Single Page Application) routing by serving index.html
+ for any routes that don't match static files, enabling client-side routing to work correctly
+ in production builds.
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any):
+ self.cachecontrol = "max-age=0, no-cache, no-store, , must-revalidate"
+ self.pragma = "no-cache"
+ self.expires = "0"
+ super().__init__(*args, **kwargs)
+
+ def file_response(self, *args: Any, **kwargs: Any) -> Response:
+ resp = super().file_response(*args, **kwargs)
+ resp.headers.setdefault("Cache-Control", self.cachecontrol)
+ resp.headers.setdefault("Pragma", self.pragma)
+ resp.headers.setdefault("Expires", self.expires)
+ return resp
+
+ async def get_response(self, path: str, scope: Scope) -> Response:
+ """
+ Override get_response to implement SPA routing.
+
+ When a file is not found and html mode is enabled, serve index.html instead of raising a 404.
+ This allows client-side routing to work correctly in SPAs.
+ """
+ try:
+ return await super().get_response(path, scope)
+ except HTTPException as exc:
+ # If the file is not found (404) and html mode is enabled, serve index.html
+ # This allows client-side routing to handle the path
+ if exc.status_code == 404 and self.html:
+ return await super().get_response("index.html", scope)
+ raise
diff --git a/invokeai/app/api/routers/_access.py b/invokeai/app/api/routers/_access.py
new file mode 100644
index 00000000000..fae3971a144
--- /dev/null
+++ b/invokeai/app/api/routers/_access.py
@@ -0,0 +1,92 @@
+"""Cross-router authorization helpers.
+
+These helpers are imported by multiple router modules. Keep them free of router
+specifics so any route can call them after resolving `current_user`.
+"""
+
+from fastapi import HTTPException
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+
+def assert_image_owner(image_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user does not own the image and is not an admin.
+
+ Ownership is satisfied when ANY of these hold:
+ - The user is an admin.
+ - The user is the image's direct owner (image_records.user_id).
+ - The user owns the board the image sits on.
+ - The image sits on a Public board (public boards grant mutation rights).
+ """
+ if current_user.is_admin:
+ return
+ owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+
+ board_id = ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name)
+ if board_id is not None:
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ if board.user_id == current_user.user_id:
+ return
+ if board.board_visibility == BoardVisibility.Public:
+ return
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=403, detail="Not authorized to modify this image")
+
+
+def assert_image_read_access(image_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user may not view the image.
+
+ Access is granted when ANY of these hold:
+ - The user is an admin.
+ - The user owns the image.
+ - The image sits on a shared or public board.
+ """
+ if current_user.is_admin:
+ return
+
+ owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+
+ board_id = ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name)
+ if board_id is not None:
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ return
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=403, detail="Not authorized to access this image")
+
+
+def assert_board_read_access(board_id: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user may not read images from this board.
+
+ Access is granted when ANY of these hold:
+ - The user is an admin.
+ - The user owns the board.
+ - The board visibility is Shared or Public.
+ """
+ if current_user.is_admin:
+ return
+
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+
+ if board.user_id == current_user.user_id:
+ return
+
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ return
+
+ raise HTTPException(status_code=403, detail="Not authorized to access this board")
diff --git a/invokeai/app/api/routers/app_info.py b/invokeai/app/api/routers/app_info.py
new file mode 100644
index 00000000000..832e58f5e24
--- /dev/null
+++ b/invokeai/app/api/routers/app_info.py
@@ -0,0 +1,399 @@
+import locale
+from enum import Enum
+from importlib.metadata import distributions
+from pathlib import Path as FilePath
+from threading import Lock
+from typing import Any
+
+import torch
+import yaml
+from fastapi import Body, HTTPException, Path
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field, model_validator
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.config.config_default import (
+ EXTERNAL_PROVIDER_CONFIG_FIELDS,
+ IMAGE_SUBFOLDER_STRATEGY,
+ DefaultInvokeAIAppConfig,
+ InvokeAIAppConfig,
+ get_config,
+ load_and_migrate_config,
+ load_external_api_keys,
+)
+from invokeai.app.services.external_generation.external_generation_common import ExternalProviderStatus
+from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
+from invokeai.app.services.model_records.model_records_base import UnknownModelException
+from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+from invokeai.backend.util.logging import logging
+from invokeai.version import __version__
+
+
+class LogLevel(int, Enum):
+ NotSet = logging.NOTSET
+ Debug = logging.DEBUG
+ Info = logging.INFO
+ Warning = logging.WARNING
+ Error = logging.ERROR
+ Critical = logging.CRITICAL
+
+
+app_router = APIRouter(prefix="/v1/app", tags=["app"])
+
+
+class AppVersion(BaseModel):
+ """App Version Response"""
+
+ version: str = Field(description="App version")
+
+
+@app_router.get("/version", operation_id="app_version", status_code=200, response_model=AppVersion)
+async def get_version() -> AppVersion:
+ return AppVersion(version=__version__)
+
+
+@app_router.get("/app_deps", operation_id="get_app_deps", status_code=200, response_model=dict[str, str])
+async def get_app_deps() -> dict[str, str]:
+ deps: dict[str, str] = {dist.metadata["Name"]: dist.version for dist in distributions()}
+ try:
+ cuda = getattr(getattr(torch, "version", None), "cuda", None) or "N/A" # pyright: ignore[reportAttributeAccessIssue]
+ except Exception:
+ cuda = "N/A"
+
+ deps["CUDA"] = cuda
+
+ sorted_deps = dict(sorted(deps.items(), key=lambda item: item[0].lower()))
+
+ return sorted_deps
+
+
+@app_router.get("/patchmatch_status", operation_id="get_patchmatch_status", status_code=200, response_model=bool)
+async def get_patchmatch_status() -> bool:
+ return PatchMatch.patchmatch_available()
+
+
+class InvokeAIAppConfigWithSetFields(BaseModel):
+ """InvokeAI App Config with model fields set"""
+
+ set_fields: set[str] = Field(description="The set fields")
+ config: InvokeAIAppConfig = Field(description="The InvokeAI App Config")
+
+
+class ExternalProviderStatusModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ configured: bool = Field(description="Whether credentials are configured for the provider")
+ message: str | None = Field(default=None, description="Optional provider status detail")
+
+
+class ExternalProviderConfigUpdate(BaseModel):
+ api_key: str | None = Field(default=None, description="API key for the external provider")
+ base_url: str | None = Field(default=None, description="Optional base URL override for the provider")
+
+
+class ExternalProviderConfigModel(BaseModel):
+ provider_id: str = Field(description="The external provider identifier")
+ api_key_configured: bool = Field(description="Whether an API key is configured")
+ base_url: str | None = Field(default=None, description="Optional base URL override")
+
+
+EXTERNAL_PROVIDER_FIELDS: dict[str, tuple[str, str]] = {
+ "alibabacloud": ("external_alibabacloud_api_key", "external_alibabacloud_base_url"),
+ "gemini": ("external_gemini_api_key", "external_gemini_base_url"),
+ "openai": ("external_openai_api_key", "external_openai_base_url"),
+ "seedream": ("external_seedream_api_key", "external_seedream_base_url"),
+}
+_EXTERNAL_PROVIDER_CONFIG_LOCK = Lock()
+
+
+def _remove_nullable_default_from_schema(schema: dict[str, Any]) -> None:
+ schema.pop("default", None)
+ any_of = schema.pop("anyOf", None)
+ if isinstance(any_of, list):
+ non_null_schemas = [
+ subschema for subschema in any_of if isinstance(subschema, dict) and subschema.get("type") != "null"
+ ]
+ if len(non_null_schemas) == 1:
+ schema.update(non_null_schemas[0])
+
+
+class UpdateAppGenerationSettingsRequest(BaseModel):
+ """Writable generation-related app settings."""
+
+ image_subfolder_strategy: IMAGE_SUBFOLDER_STRATEGY | None = Field(
+ default=None,
+ description="Strategy for organizing images into subfolders.",
+ json_schema_extra=_remove_nullable_default_from_schema,
+ )
+ max_queue_history: int | None = Field(
+ default=None,
+ ge=0,
+ description="Keep the last N completed, failed, and canceled queue items on startup. Set to 0 to prune all terminal items.",
+ )
+
+ @model_validator(mode="after")
+ def validate_explicit_nulls(self) -> "UpdateAppGenerationSettingsRequest":
+ if "image_subfolder_strategy" in self.model_fields_set and self.image_subfolder_strategy is None:
+ raise ValueError("image_subfolder_strategy may not be null")
+ return self
+
+
+@app_router.get(
+ "/runtime_config", operation_id="get_runtime_config", status_code=200, response_model=InvokeAIAppConfigWithSetFields
+)
+async def get_runtime_config() -> InvokeAIAppConfigWithSetFields:
+ config = get_config()
+ return InvokeAIAppConfigWithSetFields(set_fields=config.model_fields_set, config=config)
+
+
+@app_router.patch(
+ "/runtime_config",
+ operation_id="update_runtime_config",
+ status_code=200,
+ response_model=InvokeAIAppConfigWithSetFields,
+)
+async def update_runtime_config(
+ _: AdminUserOrDefault,
+ changes: UpdateAppGenerationSettingsRequest = Body(description="Writable runtime configuration changes"),
+) -> InvokeAIAppConfigWithSetFields:
+ with _EXTERNAL_PROVIDER_CONFIG_LOCK:
+ config = get_config()
+ update_dict = changes.model_dump(exclude_unset=True)
+ config.update_config(update_dict)
+
+ if config.config_file_path.exists():
+ persisted_config = load_and_migrate_config(config.config_file_path)
+ else:
+ persisted_config = DefaultInvokeAIAppConfig()
+
+ persisted_config.update_config(update_dict)
+ persisted_config.write_file(config.config_file_path)
+ return InvokeAIAppConfigWithSetFields(set_fields=config.model_fields_set, config=config)
+
+
+@app_router.get(
+ "/external_providers/status",
+ operation_id="get_external_provider_statuses",
+ status_code=200,
+ response_model=list[ExternalProviderStatusModel],
+)
+async def get_external_provider_statuses() -> list[ExternalProviderStatusModel]:
+ statuses = ApiDependencies.invoker.services.external_generation.get_provider_statuses()
+ return [status_to_model(status) for status in statuses.values()]
+
+
+@app_router.get(
+ "/external_providers/config",
+ operation_id="get_external_provider_configs",
+ status_code=200,
+ response_model=list[ExternalProviderConfigModel],
+)
+async def get_external_provider_configs() -> list[ExternalProviderConfigModel]:
+ config = get_config()
+ return [_build_external_provider_config(provider_id, config) for provider_id in EXTERNAL_PROVIDER_FIELDS]
+
+
+@app_router.post(
+ "/external_providers/config/{provider_id}",
+ operation_id="set_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def set_external_provider_config(
+ _: AdminUserOrDefault,
+ provider_id: str = Path(description="The external provider identifier"),
+ update: ExternalProviderConfigUpdate = Body(description="External provider configuration settings"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ updates: dict[str, str | None] = {}
+
+ if update.api_key is not None:
+ api_key = update.api_key.strip()
+ updates[api_key_field] = api_key or None
+ if update.base_url is not None:
+ base_url = update.base_url.strip()
+ updates[base_url_field] = base_url or None
+
+ if not updates:
+ raise HTTPException(status_code=400, detail="No external provider config fields provided")
+
+ api_key_removed = update.api_key is not None and updates.get(api_key_field) is None
+ _apply_external_provider_update(updates)
+ if api_key_removed:
+ _remove_external_models_for_provider(provider_id)
+ return _build_external_provider_config(provider_id, get_config())
+
+
+@app_router.delete(
+ "/external_providers/config/{provider_id}",
+ operation_id="reset_external_provider_config",
+ status_code=200,
+ response_model=ExternalProviderConfigModel,
+)
+async def reset_external_provider_config(
+ _: AdminUserOrDefault,
+ provider_id: str = Path(description="The external provider identifier"),
+) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ _apply_external_provider_update({api_key_field: None, base_url_field: None})
+ _remove_external_models_for_provider(provider_id)
+ return _build_external_provider_config(provider_id, get_config())
+
+
+def status_to_model(status: ExternalProviderStatus) -> ExternalProviderStatusModel:
+ return ExternalProviderStatusModel(
+ provider_id=status.provider_id,
+ configured=status.configured,
+ message=status.message,
+ )
+
+
+def _get_external_provider_fields(provider_id: str) -> tuple[str, str]:
+ if provider_id not in EXTERNAL_PROVIDER_FIELDS:
+ raise HTTPException(status_code=404, detail=f"Unknown external provider '{provider_id}'")
+ return EXTERNAL_PROVIDER_FIELDS[provider_id]
+
+
+def _write_external_api_keys_file(api_keys_file_path: FilePath, api_keys: dict[str, str]) -> None:
+ if not api_keys:
+ if api_keys_file_path.exists():
+ api_keys_file_path.unlink()
+ return
+
+ api_keys_file_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(api_keys_file_path, "w", encoding=locale.getpreferredencoding()) as api_keys_file:
+ yaml.safe_dump(api_keys, api_keys_file, sort_keys=False)
+
+
+def _apply_external_provider_update(updates: dict[str, str | None]) -> None:
+ with _EXTERNAL_PROVIDER_CONFIG_LOCK:
+ runtime_config = get_config()
+ config_path = runtime_config.config_file_path
+ api_keys_file_path = runtime_config.api_keys_file_path
+ if config_path.exists():
+ file_config = load_and_migrate_config(config_path)
+ else:
+ file_config = DefaultInvokeAIAppConfig()
+
+ runtime_config.update_config(updates)
+ provider_config_fields = set(EXTERNAL_PROVIDER_CONFIG_FIELDS)
+ provider_updates = {field: value for field, value in updates.items() if field in provider_config_fields}
+ non_provider_updates = {field: value for field, value in updates.items() if field not in provider_config_fields}
+
+ if non_provider_updates:
+ file_config.update_config(non_provider_updates)
+
+ persisted_api_keys = load_external_api_keys(api_keys_file_path)
+ for field_name in EXTERNAL_PROVIDER_CONFIG_FIELDS:
+ file_value = getattr(file_config, field_name, None)
+ if field_name not in persisted_api_keys and isinstance(file_value, str) and file_value.strip():
+ persisted_api_keys[field_name] = file_value
+
+ for field_name, value in provider_updates.items():
+ if value is None:
+ persisted_api_keys.pop(field_name, None)
+ else:
+ persisted_api_keys[field_name] = value
+
+ _write_external_api_keys_file(api_keys_file_path, persisted_api_keys)
+
+ for field_name in EXTERNAL_PROVIDER_CONFIG_FIELDS:
+ setattr(file_config, field_name, None)
+
+ file_config_to_write = type(file_config).model_validate(
+ file_config.model_dump(exclude_unset=True, exclude_none=True)
+ )
+ file_config_to_write.write_file(config_path, as_example=False)
+
+
+def _build_external_provider_config(provider_id: str, config: InvokeAIAppConfig) -> ExternalProviderConfigModel:
+ api_key_field, base_url_field = _get_external_provider_fields(provider_id)
+ return ExternalProviderConfigModel(
+ provider_id=provider_id,
+ api_key_configured=bool(getattr(config, api_key_field)),
+ base_url=getattr(config, base_url_field),
+ )
+
+
+def _remove_external_models_for_provider(provider_id: str) -> None:
+ model_manager = ApiDependencies.invoker.services.model_manager
+ external_models = model_manager.store.search_by_attr(
+ base_model=BaseModelType.External,
+ model_type=ModelType.ExternalImageGenerator,
+ )
+
+ for model in external_models:
+ if getattr(model, "provider_id", None) != provider_id:
+ continue
+ try:
+ model_manager.install.delete(model.key)
+ except UnknownModelException:
+ logging.warning(f"External model key '{model.key}' was already removed while resetting '{provider_id}'")
+ except Exception as error:
+ logging.warning(f"Failed removing external model key '{model.key}' for '{provider_id}': {error}")
+
+
+@app_router.get(
+ "/logging",
+ operation_id="get_log_level",
+ responses={200: {"description": "The operation was successful"}},
+ response_model=LogLevel,
+)
+async def get_log_level() -> LogLevel:
+ """Returns the log level"""
+ return LogLevel(ApiDependencies.invoker.services.logger.level)
+
+
+@app_router.post(
+ "/logging",
+ operation_id="set_log_level",
+ responses={200: {"description": "The operation was successful"}},
+ response_model=LogLevel,
+)
+async def set_log_level(
+ level: LogLevel = Body(description="New log verbosity level"),
+) -> LogLevel:
+ """Sets the log verbosity level"""
+ ApiDependencies.invoker.services.logger.setLevel(level)
+ return LogLevel(ApiDependencies.invoker.services.logger.level)
+
+
+@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()
diff --git a/invokeai/app/api/routers/auth.py b/invokeai/app/api/routers/auth.py
new file mode 100644
index 00000000000..e0b0c885cd2
--- /dev/null
+++ b/invokeai/app/api/routers/auth.py
@@ -0,0 +1,536 @@
+"""Authentication endpoints."""
+
+import secrets
+import string
+from datetime import timedelta
+from typing import Annotated
+
+from fastapi import APIRouter, Body, HTTPException, Path, status
+from pydantic import BaseModel, Field, field_validator
+
+from invokeai.app.api.auth_dependencies import AdminUser, CurrentUser
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.auth.token_service import TokenData, create_access_token
+from invokeai.app.services.users.users_common import (
+ UserCreateRequest,
+ UserDTO,
+ UserUpdateRequest,
+ validate_email_with_special_domains,
+)
+
+auth_router = APIRouter(prefix="/v1/auth", tags=["authentication"])
+
+# Token expiration constants (in days)
+TOKEN_EXPIRATION_NORMAL = 1 # 1 day for normal login
+TOKEN_EXPIRATION_REMEMBER_ME = 7 # 7 days for "remember me" login
+
+
+class LoginRequest(BaseModel):
+ """Request body for user login."""
+
+ email: str = Field(description="User email address")
+ password: str = Field(description="User password")
+ remember_me: bool = Field(default=False, description="Whether to extend session duration")
+
+ @field_validator("email")
+ @classmethod
+ def validate_email(cls, v: str) -> str:
+ """Validate email address, allowing special-use domains."""
+ return validate_email_with_special_domains(v)
+
+
+class LoginResponse(BaseModel):
+ """Response from successful login."""
+
+ token: str = Field(description="JWT access token")
+ user: UserDTO = Field(description="User information")
+ expires_in: int = Field(description="Token expiration time in seconds")
+
+
+class SetupRequest(BaseModel):
+ """Request body for initial admin setup."""
+
+ email: str = Field(description="Admin email address")
+ display_name: str | None = Field(default=None, description="Admin display name")
+ password: str = Field(description="Admin password")
+
+ @field_validator("email")
+ @classmethod
+ def validate_email(cls, v: str) -> str:
+ """Validate email address, allowing special-use domains."""
+ return validate_email_with_special_domains(v)
+
+
+class SetupResponse(BaseModel):
+ """Response from successful admin setup."""
+
+ success: bool = Field(description="Whether setup was successful")
+ user: UserDTO = Field(description="Created admin user information")
+
+
+class LogoutResponse(BaseModel):
+ """Response from logout."""
+
+ success: bool = Field(description="Whether logout was successful")
+
+
+class SetupStatusResponse(BaseModel):
+ """Response for setup status check."""
+
+ setup_required: bool = Field(description="Whether initial setup is required")
+ multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
+ strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
+ admin_email: str | None = Field(default=None, description="Email of the first active admin user, if any")
+
+
+@auth_router.get("/status", response_model=SetupStatusResponse)
+async def get_setup_status() -> SetupStatusResponse:
+ """Check if initial administrator setup is required.
+
+ Returns:
+ SetupStatusResponse indicating whether setup is needed and multiuser mode status
+ """
+ config = ApiDependencies.invoker.services.configuration
+
+ # If multiuser is disabled, setup is never required
+ if not config.multiuser:
+ return SetupStatusResponse(
+ setup_required=False,
+ multiuser_enabled=False,
+ strict_password_checking=config.strict_password_checking,
+ admin_email=None,
+ )
+
+ # In multiuser mode, check if an admin exists
+ user_service = ApiDependencies.invoker.services.users
+ setup_required = not user_service.has_admin()
+
+ # Only expose admin_email during initial setup to avoid leaking
+ # administrator identity on public deployments.
+ admin_email = user_service.get_admin_email() if setup_required else None
+
+ return SetupStatusResponse(
+ setup_required=setup_required,
+ multiuser_enabled=True,
+ strict_password_checking=config.strict_password_checking,
+ admin_email=admin_email,
+ )
+
+
+@auth_router.post("/login", response_model=LoginResponse)
+async def login(
+ request: Annotated[LoginRequest, Body(description="Login credentials")],
+) -> LoginResponse:
+ """Authenticate user and return access token.
+
+ Args:
+ request: Login credentials (email and password)
+
+ Returns:
+ LoginResponse containing JWT token and user information
+
+ Raises:
+ HTTPException: 401 if credentials are invalid or user is inactive
+ HTTPException: 403 if multiuser mode is disabled
+ """
+ config = ApiDependencies.invoker.services.configuration
+
+ # Check if multiuser is enabled
+ if not config.multiuser:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Multiuser mode is disabled. Authentication is not required in single-user mode.",
+ )
+
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.authenticate(request.email, request.password)
+
+ if user is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect email or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ if not user.is_active:
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled")
+
+ # Create token with appropriate expiration
+ expires_delta = timedelta(days=TOKEN_EXPIRATION_REMEMBER_ME if request.remember_me else TOKEN_EXPIRATION_NORMAL)
+ token_data = TokenData(
+ user_id=user.user_id,
+ email=user.email,
+ is_admin=user.is_admin,
+ remember_me=request.remember_me,
+ )
+ token = create_access_token(token_data, expires_delta)
+
+ return LoginResponse(
+ token=token,
+ user=user,
+ expires_in=int(expires_delta.total_seconds()),
+ )
+
+
+@auth_router.post("/logout", response_model=LogoutResponse)
+async def logout(
+ current_user: CurrentUser,
+) -> LogoutResponse:
+ """Logout current user.
+
+ Currently a no-op since we use stateless JWT tokens. For token invalidation in
+ future implementations, consider:
+ - Token blacklist: Store invalidated tokens in Redis/database with expiration
+ - Token versioning: Add version field to user record, increment on logout
+ - Short-lived tokens: Use refresh token pattern with token rotation
+ - Session storage: Track active sessions server-side for revocation
+
+ Args:
+ current_user: The authenticated user (validates token)
+
+ Returns:
+ LogoutResponse indicating success
+ """
+ # TODO: Implement token invalidation when server-side session management is added
+ # For now, this is a no-op since we use stateless JWT tokens
+ return LogoutResponse(success=True)
+
+
+@auth_router.get("/me", response_model=UserDTO)
+async def get_current_user_info(
+ current_user: CurrentUser,
+) -> UserDTO:
+ """Get current authenticated user's information.
+
+ Args:
+ current_user: The authenticated user's token data
+
+ Returns:
+ UserDTO containing user information
+
+ Raises:
+ HTTPException: 404 if user is not found (should not happen normally)
+ """
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.get(current_user.user_id)
+
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+
+ return user
+
+
+@auth_router.post("/setup", response_model=SetupResponse)
+async def setup_admin(
+ request: Annotated[SetupRequest, Body(description="Admin account details")],
+) -> SetupResponse:
+ """Set up initial administrator account.
+
+ This endpoint can only be called once, when no admin user exists. It creates
+ the first admin user for the system.
+
+ Args:
+ request: Admin account details (email, display_name, password)
+
+ Returns:
+ SetupResponse containing the created admin user
+
+ Raises:
+ HTTPException: 400 if admin already exists or password is weak
+ HTTPException: 403 if multiuser mode is disabled
+ """
+ config = ApiDependencies.invoker.services.configuration
+
+ # Check if multiuser is enabled
+ if not config.multiuser:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Multiuser mode is disabled. Admin setup is not required in single-user mode.",
+ )
+
+ user_service = ApiDependencies.invoker.services.users
+
+ # Check if any admin exists
+ if user_service.has_admin():
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Administrator account already configured",
+ )
+
+ # Create admin user - this will validate password strength
+ try:
+ user_data = UserCreateRequest(
+ email=request.email,
+ display_name=request.display_name,
+ password=request.password,
+ is_admin=True,
+ )
+ user = user_service.create_admin(user_data, strict_password_checking=config.strict_password_checking)
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
+
+ return SetupResponse(success=True, user=user)
+
+
+# ---------------------------------------------------------------------------
+# User management models
+# ---------------------------------------------------------------------------
+
+_PASSWORD_ALPHABET = string.ascii_letters + string.digits + string.punctuation
+
+
+class AdminUserCreateRequest(BaseModel):
+ """Request body for admin to create a new user."""
+
+ email: str = Field(description="User email address")
+ display_name: str | None = Field(default=None, description="Display name")
+ password: str = Field(description="User password")
+ is_admin: bool = Field(default=False, description="Whether user should have admin privileges")
+
+ @field_validator("email")
+ @classmethod
+ def validate_email(cls, v: str) -> str:
+ """Validate email address, allowing special-use domains."""
+ return validate_email_with_special_domains(v)
+
+
+class AdminUserUpdateRequest(BaseModel):
+ """Request body for admin to update any user."""
+
+ display_name: str | None = Field(default=None, description="Display name")
+ password: str | None = Field(default=None, description="New password")
+ is_admin: bool | None = Field(default=None, description="Whether user should have admin privileges")
+ is_active: bool | None = Field(default=None, description="Whether user account should be active")
+
+
+class UserProfileUpdateRequest(BaseModel):
+ """Request body for a user to update their own profile."""
+
+ display_name: str | None = Field(default=None, description="New display name")
+ current_password: str | None = Field(default=None, description="Current password (required when changing password)")
+ new_password: str | None = Field(default=None, description="New password")
+
+
+class GeneratePasswordResponse(BaseModel):
+ """Response containing a generated password."""
+
+ password: str = Field(description="Generated strong password")
+
+
+# ---------------------------------------------------------------------------
+# User management endpoints
+# ---------------------------------------------------------------------------
+
+
+@auth_router.get("/generate-password", response_model=GeneratePasswordResponse)
+async def generate_password(
+ current_user: CurrentUser,
+) -> GeneratePasswordResponse:
+ """Generate a strong random password.
+
+ Returns a cryptographically secure random password of 16 characters
+ containing uppercase, lowercase, digits, and punctuation.
+ """
+ # Ensure the generated password always meets strength requirements:
+ # at least one uppercase, one lowercase, one digit, one special char.
+ while True:
+ password = "".join(secrets.choice(_PASSWORD_ALPHABET) for _ in range(16))
+ if (
+ any(c.isupper() for c in password)
+ and any(c.islower() for c in password)
+ and any(c.isdigit() for c in password)
+ ):
+ return GeneratePasswordResponse(password=password)
+
+
+@auth_router.get("/users", response_model=list[UserDTO])
+async def list_users(
+ current_user: AdminUser,
+) -> list[UserDTO]:
+ """List all users. Requires admin privileges.
+
+ The internal 'system' user (created for backward compatibility) is excluded
+ from the results since it cannot be managed through this interface.
+
+ Returns:
+ List of all real users (system user excluded)
+ """
+ user_service = ApiDependencies.invoker.services.users
+ return [u for u in user_service.list_users() if u.user_id != "system"]
+
+
+@auth_router.post("/users", response_model=UserDTO, status_code=status.HTTP_201_CREATED)
+async def create_user(
+ request: Annotated[AdminUserCreateRequest, Body(description="New user details")],
+ current_user: AdminUser,
+) -> UserDTO:
+ """Create a new user. Requires admin privileges.
+
+ Args:
+ request: New user details
+
+ Returns:
+ The created user
+
+ Raises:
+ HTTPException: 400 if email already exists or password is weak
+ """
+ user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
+ try:
+ user_data = UserCreateRequest(
+ email=request.email,
+ display_name=request.display_name,
+ password=request.password,
+ is_admin=request.is_admin,
+ )
+ return user_service.create(user_data, strict_password_checking=config.strict_password_checking)
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
+
+
+@auth_router.get("/users/{user_id}", response_model=UserDTO)
+async def get_user(
+ user_id: Annotated[str, Path(description="User ID")],
+ current_user: AdminUser,
+) -> UserDTO:
+ """Get a user by ID. Requires admin privileges.
+
+ Args:
+ user_id: The user ID
+
+ Returns:
+ The user
+
+ Raises:
+ HTTPException: 404 if user not found
+ """
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.get(user_id)
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+ return user
+
+
+@auth_router.patch("/users/{user_id}", response_model=UserDTO)
+async def update_user(
+ user_id: Annotated[str, Path(description="User ID")],
+ request: Annotated[AdminUserUpdateRequest, Body(description="User fields to update")],
+ current_user: AdminUser,
+) -> UserDTO:
+ """Update a user. Requires admin privileges.
+
+ Args:
+ user_id: The user ID
+ request: Fields to update
+
+ Returns:
+ The updated user
+
+ Raises:
+ HTTPException: 400 if password is weak
+ HTTPException: 404 if user not found
+ """
+ user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
+ try:
+ changes = UserUpdateRequest(
+ display_name=request.display_name,
+ password=request.password,
+ is_admin=request.is_admin,
+ is_active=request.is_active,
+ )
+ return user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
+
+
+@auth_router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
+async def delete_user(
+ user_id: Annotated[str, Path(description="User ID")],
+ current_user: AdminUser,
+) -> None:
+ """Delete a user. Requires admin privileges.
+
+ Admins can delete any user including other admins, but cannot delete the last
+ remaining admin.
+
+ Args:
+ user_id: The user ID
+
+ Raises:
+ HTTPException: 400 if attempting to delete the last admin
+ HTTPException: 404 if user not found
+ """
+ user_service = ApiDependencies.invoker.services.users
+ user = user_service.get(user_id)
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+
+ # Prevent deleting the last active admin
+ if user.is_admin and user.is_active and user_service.count_admins() <= 1:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Cannot delete the last administrator",
+ )
+
+ try:
+ user_service.delete(user_id)
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
+
+
+@auth_router.patch("/me", response_model=UserDTO)
+async def update_current_user(
+ request: Annotated[UserProfileUpdateRequest, Body(description="Profile fields to update")],
+ current_user: CurrentUser,
+) -> UserDTO:
+ """Update the current user's own profile.
+
+ To change the password, both ``current_password`` and ``new_password`` must
+ be provided. The current password is verified before the change is applied.
+
+ Args:
+ request: Profile fields to update
+ current_user: The authenticated user
+
+ Returns:
+ The updated user
+
+ Raises:
+ HTTPException: 400 if current password is incorrect or new password is weak
+ HTTPException: 404 if user not found
+ """
+ user_service = ApiDependencies.invoker.services.users
+ config = ApiDependencies.invoker.services.configuration
+
+ # Verify current password when attempting a password change
+ if request.new_password is not None:
+ if not request.current_password:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Current password is required to set a new password",
+ )
+
+ # Re-authenticate to verify the current password
+ user = user_service.get(current_user.user_id)
+ if user is None:
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
+
+ authenticated = user_service.authenticate(user.email, request.current_password)
+ if authenticated is None:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Current password is incorrect",
+ )
+
+ try:
+ changes = UserUpdateRequest(
+ display_name=request.display_name,
+ password=request.new_password,
+ )
+ return user_service.update(
+ current_user.user_id, changes, strict_password_checking=config.strict_password_checking
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
diff --git a/invokeai/app/api/routers/board_images.py b/invokeai/app/api/routers/board_images.py
new file mode 100644
index 00000000000..f94e4f2437c
--- /dev/null
+++ b/invokeai/app/api/routers/board_images.py
@@ -0,0 +1,204 @@
+from fastapi import Body, HTTPException
+from fastapi.routing import APIRouter
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.images.images_common import AddImagesToBoardResult, RemoveImagesFromBoardResult
+
+board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
+
+
+def _assert_board_write_access(board_id: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user may not mutate the given board.
+
+ Write access is granted when ANY of these hold:
+ - The user is an admin.
+ - The user owns the board.
+ - The board visibility is Public (public boards accept contributions from any user).
+ """
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+ if current_user.is_admin:
+ return
+ if board.user_id == current_user.user_id:
+ return
+ if board.board_visibility == BoardVisibility.Public:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to modify this board")
+
+
+def _assert_image_direct_owner(image_name: str, current_user: CurrentUserOrDefault) -> None:
+ """Raise 403 if the current user is not the direct owner of the image.
+
+ This is intentionally stricter than _assert_image_owner in images.py:
+ board ownership is NOT sufficient here. Allowing a user to add someone
+ else's image to their own board would grant them mutation rights via the
+ board-ownership fallback in _assert_image_owner, escalating read access
+ into write access.
+ """
+ if current_user.is_admin:
+ return
+ owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
+ if owner is not None and owner == current_user.user_id:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to move this image")
+
+
+@board_images_router.post(
+ "/",
+ operation_id="add_image_to_board",
+ responses={
+ 201: {"description": "The image was added to a board successfully"},
+ },
+ status_code=201,
+ response_model=AddImagesToBoardResult,
+)
+async def add_image_to_board(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Body(description="The id of the board to add to"),
+ image_name: str = Body(description="The name of the image to add"),
+) -> AddImagesToBoardResult:
+ """Creates a board_image"""
+ _assert_board_write_access(board_id, current_user)
+ _assert_image_direct_owner(image_name, current_user)
+ try:
+ added_images: set[str] = set()
+ affected_boards: set[str] = set()
+ old_board_id = ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name) or "none"
+ ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
+ added_images.add(image_name)
+ affected_boards.add(board_id)
+ affected_boards.add(old_board_id)
+
+ return AddImagesToBoardResult(
+ added_images=list(added_images),
+ affected_boards=list(affected_boards),
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to add image to board")
+
+
+@board_images_router.delete(
+ "/",
+ operation_id="remove_image_from_board",
+ responses={
+ 201: {"description": "The image was removed from the board successfully"},
+ },
+ status_code=201,
+ response_model=RemoveImagesFromBoardResult,
+)
+async def remove_image_from_board(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Body(description="The name of the image to remove", embed=True),
+) -> RemoveImagesFromBoardResult:
+ """Removes an image from its board, if it had one"""
+ try:
+ old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
+ if old_board_id != "none":
+ _assert_board_write_access(old_board_id, current_user)
+ removed_images: set[str] = set()
+ affected_boards: set[str] = set()
+ ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
+ removed_images.add(image_name)
+ affected_boards.add("none")
+ affected_boards.add(old_board_id)
+ return RemoveImagesFromBoardResult(
+ removed_images=list(removed_images),
+ affected_boards=list(affected_boards),
+ )
+
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to remove image from board")
+
+
+@board_images_router.post(
+ "/batch",
+ operation_id="add_images_to_board",
+ responses={
+ 201: {"description": "Images were added to board successfully"},
+ },
+ status_code=201,
+ response_model=AddImagesToBoardResult,
+)
+async def add_images_to_board(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Body(description="The id of the board to add to"),
+ image_names: list[str] = Body(description="The names of the images to add", embed=True),
+) -> AddImagesToBoardResult:
+ """Adds a list of images to a board"""
+ _assert_board_write_access(board_id, current_user)
+ try:
+ added_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ _assert_image_direct_owner(image_name, current_user)
+ old_board_id = (
+ ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name) or "none"
+ )
+ ApiDependencies.invoker.services.board_images.add_image_to_board(
+ board_id=board_id,
+ image_name=image_name,
+ )
+ added_images.add(image_name)
+ affected_boards.add(board_id)
+ affected_boards.add(old_board_id)
+
+ except HTTPException:
+ raise
+ except Exception:
+ pass
+ return AddImagesToBoardResult(
+ added_images=list(added_images),
+ affected_boards=list(affected_boards),
+ )
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to add images to board")
+
+
+@board_images_router.post(
+ "/batch/delete",
+ operation_id="remove_images_from_board",
+ responses={
+ 201: {"description": "Images were removed from board successfully"},
+ },
+ status_code=201,
+ response_model=RemoveImagesFromBoardResult,
+)
+async def remove_images_from_board(
+ current_user: CurrentUserOrDefault,
+ image_names: list[str] = Body(description="The names of the images to remove", embed=True),
+) -> RemoveImagesFromBoardResult:
+ """Removes a list of images from their board, if they had one"""
+ try:
+ removed_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
+ if old_board_id != "none":
+ _assert_board_write_access(old_board_id, current_user)
+ ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
+ removed_images.add(image_name)
+ affected_boards.add("none")
+ affected_boards.add(old_board_id)
+ except HTTPException:
+ raise
+ except Exception:
+ pass
+ return RemoveImagesFromBoardResult(
+ removed_images=list(removed_images),
+ affected_boards=list(affected_boards),
+ )
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to remove images from board")
diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
new file mode 100644
index 00000000000..6897e90aff4
--- /dev/null
+++ b/invokeai/app/api/routers/boards.py
@@ -0,0 +1,221 @@
+from typing import Optional, Union
+
+from fastapi import Body, HTTPException, Path, Query
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy, BoardVisibility
+from invokeai.app.services.boards.boards_common import BoardDTO
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
+
+
+class DeleteBoardResult(BaseModel):
+ board_id: str = Field(description="The id of the board that was deleted.")
+ deleted_board_images: list[str] = Field(
+ description="The image names of the board-images relationships that were deleted."
+ )
+ deleted_images: list[str] = Field(description="The names of the images that were deleted.")
+
+
+@boards_router.post(
+ "/",
+ operation_id="create_board",
+ responses={
+ 201: {"description": "The board was created successfully"},
+ },
+ status_code=201,
+ response_model=BoardDTO,
+)
+async def create_board(
+ current_user: CurrentUserOrDefault,
+ board_name: str = Query(description="The name of the board to create", max_length=300),
+) -> BoardDTO:
+ """Creates a board for the current user"""
+ try:
+ result = ApiDependencies.invoker.services.boards.create(board_name=board_name, user_id=current_user.user_id)
+ return result
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to create board")
+
+
+@boards_router.get("/{board_id}", operation_id="get_board", response_model=BoardDTO)
+async def get_board(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Path(description="The id of board to get"),
+) -> BoardDTO:
+ """Gets a board (user must have access to it)"""
+
+ try:
+ result = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+
+ # Admins can access any board.
+ # Owners can access their own boards.
+ # Shared and public boards are visible to all authenticated users.
+ if (
+ not current_user.is_admin
+ and result.user_id != current_user.user_id
+ and result.board_visibility == BoardVisibility.Private
+ ):
+ raise HTTPException(status_code=403, detail="Not authorized to access this board")
+
+ return result
+
+
+@boards_router.patch(
+ "/{board_id}",
+ operation_id="update_board",
+ responses={
+ 201: {
+ "description": "The board was updated successfully",
+ },
+ },
+ status_code=201,
+ response_model=BoardDTO,
+)
+async def update_board(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Path(description="The id of board to update"),
+ changes: BoardChanges = Body(description="The changes to apply to the board"),
+) -> BoardDTO:
+ """Updates a board (user must have access to it)"""
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+
+ if not current_user.is_admin and board.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this board")
+
+ try:
+ result = ApiDependencies.invoker.services.boards.update(board_id=board_id, changes=changes)
+ return result
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to update board")
+
+
+@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteBoardResult)
+async def delete_board(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Path(description="The id of board to delete"),
+ include_images: Optional[bool] = Query(description="Permanently delete all images on the board", default=False),
+) -> DeleteBoardResult:
+ """Deletes a board (user must have access to it)"""
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+
+ if not current_user.is_admin and board.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to delete this board")
+
+ try:
+ if include_images is True:
+ deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id=board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+ ApiDependencies.invoker.services.images.delete_images_on_board(board_id=board_id)
+ ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ return DeleteBoardResult(
+ board_id=board_id,
+ deleted_board_images=[],
+ deleted_images=deleted_images,
+ )
+ else:
+ deleted_board_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id=board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+ ApiDependencies.invoker.services.boards.delete(board_id=board_id)
+ return DeleteBoardResult(
+ board_id=board_id,
+ deleted_board_images=deleted_board_images,
+ deleted_images=[],
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to delete board")
+
+
+@boards_router.get(
+ "/",
+ operation_id="list_boards",
+ response_model=Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]],
+)
+async def list_boards(
+ current_user: CurrentUserOrDefault,
+ order_by: BoardRecordOrderBy = Query(default=BoardRecordOrderBy.CreatedAt, description="The attribute to order by"),
+ direction: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The direction to order by"),
+ all: Optional[bool] = Query(default=None, description="Whether to list all boards"),
+ offset: Optional[int] = Query(default=None, description="The page offset"),
+ limit: Optional[int] = Query(default=None, description="The number of boards per page"),
+ include_archived: bool = Query(default=False, description="Whether or not to include archived boards in list"),
+) -> Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]]:
+ """Gets a list of boards for the current user, including shared boards. Admin users see all boards."""
+ if all:
+ return ApiDependencies.invoker.services.boards.get_all(
+ current_user.user_id, current_user.is_admin, order_by, direction, include_archived
+ )
+ elif offset is not None and limit is not None:
+ return ApiDependencies.invoker.services.boards.get_many(
+ current_user.user_id, current_user.is_admin, order_by, direction, offset, limit, include_archived
+ )
+ else:
+ raise HTTPException(
+ status_code=400,
+ detail="Invalid request: Must provide either 'all' or both 'offset' and 'limit'",
+ )
+
+
+@boards_router.get(
+ "/{board_id}/image_names",
+ operation_id="list_all_board_image_names",
+ response_model=list[str],
+)
+async def list_all_board_image_names(
+ current_user: CurrentUserOrDefault,
+ board_id: str = Path(description="The id of the board or 'none' for uncategorized images"),
+ categories: list[ImageCategory] | None = Query(default=None, description="The categories of image to include."),
+ is_intermediate: bool | None = Query(default=None, description="Whether to list intermediate images."),
+) -> list[str]:
+ """Gets a list of images for a board"""
+
+ if board_id != "none":
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+
+ if (
+ not current_user.is_admin
+ and board.user_id != current_user.user_id
+ and board.board_visibility == BoardVisibility.Private
+ ):
+ raise HTTPException(status_code=403, detail="Not authorized to access this board")
+
+ image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id,
+ categories,
+ is_intermediate,
+ )
+
+ # For uncategorized images (board_id="none"), filter to only the caller's
+ # images so that one user cannot enumerate another's uncategorized images.
+ # Admin users can see all uncategorized images.
+ if board_id == "none" and not current_user.is_admin:
+ image_names = [
+ name
+ for name in image_names
+ if ApiDependencies.invoker.services.image_records.get_user_id(name) == current_user.user_id
+ ]
+
+ return image_names
diff --git a/invokeai/app/api/routers/client_state.py b/invokeai/app/api/routers/client_state.py
new file mode 100644
index 00000000000..cd92263f97c
--- /dev/null
+++ b/invokeai/app/api/routers/client_state.py
@@ -0,0 +1,100 @@
+from fastapi import Body, HTTPException, Path, Query
+from fastapi.routing import APIRouter
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.backend.util.logging import logging
+
+client_state_router = APIRouter(prefix="/v1/client_state", tags=["client_state"])
+
+
+@client_state_router.get(
+ "/{queue_id}/get_by_key",
+ operation_id="get_client_state_by_key",
+ response_model=str | None,
+)
+async def get_client_state_by_key(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
+ key: str = Query(..., description="Key to get"),
+) -> str | None:
+ """Gets the client state for the current user (or system user if not authenticated)"""
+ try:
+ return ApiDependencies.invoker.services.client_state_persistence.get_by_key(current_user.user_id, key)
+ except Exception as e:
+ logging.error(f"Error getting client state: {e}")
+ raise HTTPException(status_code=500, detail="Error getting client state")
+
+
+@client_state_router.post(
+ "/{queue_id}/set_by_key",
+ operation_id="set_client_state",
+ response_model=str,
+)
+async def set_client_state(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
+ key: str = Query(..., description="Key to set"),
+ value: str = Body(..., description="Stringified value to set"),
+) -> str:
+ """Sets the client state for the current user (or system user if not authenticated)"""
+ try:
+ return ApiDependencies.invoker.services.client_state_persistence.set_by_key(current_user.user_id, key, value)
+ except Exception as e:
+ logging.error(f"Error setting client state: {e}")
+ raise HTTPException(status_code=500, detail="Error setting client state")
+
+
+@client_state_router.get(
+ "/{queue_id}/get_keys_by_prefix",
+ operation_id="get_client_state_keys_by_prefix",
+ response_model=list[str],
+)
+async def get_client_state_keys_by_prefix(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
+ prefix: str = Query(..., description="Prefix to filter keys by"),
+) -> list[str]:
+ """Gets client state keys matching a prefix for the current user"""
+ try:
+ return ApiDependencies.invoker.services.client_state_persistence.get_keys_by_prefix(
+ current_user.user_id, prefix
+ )
+ except Exception as e:
+ logging.error(f"Error getting client state keys: {e}")
+ raise HTTPException(status_code=500, detail="Error getting client state keys")
+
+
+@client_state_router.post(
+ "/{queue_id}/delete_by_key",
+ operation_id="delete_client_state_by_key",
+ responses={204: {"description": "Client state key deleted"}},
+)
+async def delete_client_state_by_key(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
+ key: str = Query(..., description="Key to delete"),
+) -> None:
+ """Deletes a specific client state key for the current user"""
+ try:
+ ApiDependencies.invoker.services.client_state_persistence.delete_by_key(current_user.user_id, key)
+ except Exception as e:
+ logging.error(f"Error deleting client state key: {e}")
+ raise HTTPException(status_code=500, detail="Error deleting client state key")
+
+
+@client_state_router.post(
+ "/{queue_id}/delete",
+ operation_id="delete_client_state",
+ responses={204: {"description": "Client state deleted"}},
+)
+async def delete_client_state(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
+) -> None:
+ """Deletes the client state for the current user (or system user if not authenticated)"""
+ try:
+ ApiDependencies.invoker.services.client_state_persistence.delete(current_user.user_id)
+ except Exception as e:
+ logging.error(f"Error deleting client state: {e}")
+ raise HTTPException(status_code=500, detail="Error deleting client state")
diff --git a/invokeai/app/api/routers/custom_nodes.py b/invokeai/app/api/routers/custom_nodes.py
new file mode 100644
index 00000000000..3ee8c0ec99c
--- /dev/null
+++ b/invokeai/app/api/routers/custom_nodes.py
@@ -0,0 +1,504 @@
+"""FastAPI routes for custom node management."""
+
+import json
+import shutil
+import subprocess
+import sys
+import traceback
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+from typing import Optional
+
+from fastapi import Body
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.invocations.baseinvocation import InvocationRegistry
+from invokeai.app.services.config.config_default import get_config
+from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutIDValidator
+from invokeai.backend.util.logging import InvokeAILogger
+
+custom_nodes_router = APIRouter(prefix="/v2/custom_nodes", tags=["custom_nodes"])
+
+logger = InvokeAILogger.get_logger()
+
+# Name of the manifest file written inside a pack directory to track which workflows
+# were imported by that pack. Used on uninstall to delete only pack-imported workflows
+# — deleting by tag alone is unsafe because users can edit tags on their own workflows.
+PACK_MANIFEST_FILENAME = ".invokeai_pack_manifest.json"
+
+
+class NodePackInfo(BaseModel):
+ """Information about an installed node pack."""
+
+ name: str = Field(description="The name of the node pack.")
+ path: str = Field(description="The path to the node pack directory.")
+ node_count: int = Field(description="The number of nodes in the pack.")
+ node_types: list[str] = Field(description="The invocation types provided by this node pack.")
+
+
+class NodePackListResponse(BaseModel):
+ """Response for listing installed node packs."""
+
+ node_packs: list[NodePackInfo] = Field(description="List of installed node packs.")
+ custom_nodes_path: str = Field(description="The configured custom nodes directory path.")
+
+
+class InstallNodePackRequest(BaseModel):
+ """Request to install a node pack from a git URL."""
+
+ source: str = Field(description="Git URL of the node pack to install.")
+
+
+class InstallNodePackResponse(BaseModel):
+ """Response after installing a node pack."""
+
+ name: str = Field(description="The name of the installed node pack.")
+ success: bool = Field(description="Whether the installation was successful.")
+ message: str = Field(description="Status message.")
+ workflows_imported: int = Field(default=0, description="Number of workflows imported from the pack.")
+ requires_dependencies: bool = Field(
+ default=False,
+ description="Whether the pack ships a dependency manifest (requirements.txt or pyproject.toml) "
+ "that the user must install manually following the pack's documentation.",
+ )
+ dependency_file: Optional[str] = Field(
+ default=None,
+ description="Name of the detected dependency manifest file, if any.",
+ )
+
+
+class UninstallNodePackResponse(BaseModel):
+ """Response after uninstalling a node pack."""
+
+ name: str = Field(description="The name of the uninstalled node pack.")
+ success: bool = Field(description="Whether the uninstall was successful.")
+ message: str = Field(description="Status message.")
+
+
+def _get_custom_nodes_path() -> Path:
+ """Returns the configured custom nodes directory path."""
+ config = get_config()
+ return config.custom_nodes_path
+
+
+def _get_installed_packs() -> list[NodePackInfo]:
+ """Scans the custom nodes directory and returns info about installed packs."""
+ custom_nodes_path = _get_custom_nodes_path()
+
+ if not custom_nodes_path.exists():
+ return []
+
+ packs: list[NodePackInfo] = []
+
+ # Get all node types grouped by node_pack
+ node_types_by_pack: dict[str, list[str]] = {}
+ for inv_class in InvocationRegistry._invocation_classes:
+ node_pack = inv_class.UIConfig.node_pack
+ inv_type = inv_class.get_type()
+ if node_pack not in node_types_by_pack:
+ node_types_by_pack[node_pack] = []
+ node_types_by_pack[node_pack].append(inv_type)
+
+ for d in sorted(custom_nodes_path.iterdir()):
+ if not d.is_dir():
+ continue
+ if d.name.startswith("_") or d.name.startswith("."):
+ continue
+ init = d / "__init__.py"
+ if not init.exists():
+ continue
+
+ pack_name = d.name
+ node_types = node_types_by_pack.get(pack_name, [])
+
+ packs.append(
+ NodePackInfo(
+ name=pack_name,
+ path=str(d),
+ node_count=len(node_types),
+ node_types=node_types,
+ )
+ )
+
+ return packs
+
+
+@custom_nodes_router.get(
+ "/",
+ operation_id="list_custom_node_packs",
+ response_model=NodePackListResponse,
+)
+async def list_custom_node_packs(current_admin: AdminUserOrDefault) -> NodePackListResponse:
+ """Lists all installed custom node packs.
+
+ Admin-only: the response includes absolute filesystem paths, and non-admins have no
+ legitimate use for pack management data (install/uninstall/reload are also admin-only).
+ """
+ packs = _get_installed_packs()
+ return NodePackListResponse(node_packs=packs, custom_nodes_path=str(_get_custom_nodes_path()))
+
+
+@custom_nodes_router.post(
+ "/install",
+ operation_id="install_custom_node_pack",
+ response_model=InstallNodePackResponse,
+)
+async def install_custom_node_pack(
+ current_admin: AdminUserOrDefault,
+ request: InstallNodePackRequest = Body(description="The source URL to install from."),
+) -> InstallNodePackResponse:
+ """Installs a custom node pack from a git URL by cloning it into the nodes directory."""
+ custom_nodes_path = _get_custom_nodes_path()
+ custom_nodes_path.mkdir(parents=True, exist_ok=True)
+
+ source = request.source.strip()
+
+ # Extract pack name from URL
+ pack_name = source.rstrip("/").split("/")[-1]
+ if pack_name.endswith(".git"):
+ pack_name = pack_name[:-4]
+
+ target_dir = custom_nodes_path / pack_name
+
+ if target_dir.exists():
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Node pack '{pack_name}' already exists. Uninstall it first to reinstall.",
+ )
+
+ try:
+ # Clone the repository
+ result = subprocess.run(
+ ["git", "clone", source, str(target_dir)],
+ capture_output=True,
+ text=True,
+ timeout=120,
+ )
+
+ if result.returncode != 0:
+ # Clean up on failure
+ if target_dir.exists():
+ shutil.rmtree(target_dir)
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Git clone failed: {result.stderr.strip()}",
+ )
+
+ # Detect dependency manifests but do NOT install them automatically.
+ # The user is responsible for installing dependencies per the pack's documentation,
+ # since arbitrary pip installs can break the InvokeAI environment.
+ dependency_file: Optional[str] = None
+ for candidate in ("requirements.txt", "pyproject.toml"):
+ if (target_dir / candidate).exists():
+ dependency_file = candidate
+ logger.info(f"Node pack '{pack_name}' ships a {candidate}; user must install dependencies manually.")
+ break
+
+ # Check for __init__.py
+ init_file = target_dir / "__init__.py"
+ if not init_file.exists():
+ shutil.rmtree(target_dir)
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Node pack '{pack_name}' does not contain an __init__.py file.",
+ )
+
+ # Load the node pack at runtime
+ _load_node_pack(pack_name, target_dir)
+
+ # Import any workflows found in the pack, owned by the installing admin and shared with all users
+ imported_workflow_ids = _import_workflows_from_pack(target_dir, pack_name, owner_user_id=current_admin.user_id)
+ _write_pack_manifest(target_dir, imported_workflow_ids)
+ workflows_imported = len(imported_workflow_ids)
+ workflow_msg = f" Imported {workflows_imported} workflow(s)." if workflows_imported > 0 else ""
+ dependency_msg = (
+ f" This pack includes a {dependency_file} — install its dependencies manually following the pack's documentation."
+ if dependency_file
+ else ""
+ )
+
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=True,
+ message=f"Successfully installed node pack '{pack_name}'.{workflow_msg}{dependency_msg}",
+ workflows_imported=workflows_imported,
+ requires_dependencies=dependency_file is not None,
+ dependency_file=dependency_file,
+ )
+
+ except subprocess.TimeoutExpired:
+ if target_dir.exists():
+ shutil.rmtree(target_dir)
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message="Installation timed out.",
+ )
+ except Exception:
+ if target_dir.exists():
+ shutil.rmtree(target_dir)
+ error = traceback.format_exc()
+ logger.error(f"Failed to install node pack {pack_name}: {error}")
+ return InstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Installation failed: {error}",
+ )
+
+
+@custom_nodes_router.delete(
+ "/{pack_name}",
+ operation_id="uninstall_custom_node_pack",
+ response_model=UninstallNodePackResponse,
+)
+async def uninstall_custom_node_pack(
+ current_admin: AdminUserOrDefault,
+ pack_name: str,
+) -> UninstallNodePackResponse:
+ """Uninstalls a custom node pack by removing its directory.
+
+ Note: A restart is required for the node removal to take full effect.
+ Installed nodes from the pack will remain registered until restart.
+ """
+ custom_nodes_path = _get_custom_nodes_path()
+ target_dir = custom_nodes_path / pack_name
+
+ if not target_dir.exists():
+ return UninstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Node pack '{pack_name}' not found.",
+ )
+
+ try:
+ # Read the manifest BEFORE removing the directory — it records exactly which
+ # workflow IDs this pack imported, so uninstall doesn't accidentally delete
+ # user workflows that happen to share the pack tag.
+ imported_workflow_ids = _read_pack_manifest(target_dir)
+
+ shutil.rmtree(target_dir)
+
+ # Unregister the nodes from the registry so they disappear immediately
+ removed_types = InvocationRegistry.unregister_pack(pack_name)
+ if removed_types:
+ # Invalidate OpenAPI schema cache so frontend gets updated node definitions
+ from invokeai.app.api_app import app
+
+ app.openapi_schema = None
+ logger.info(
+ f"Unregistered {len(removed_types)} node(s) from pack '{pack_name}': {', '.join(removed_types)}"
+ )
+
+ # Remove the pack's module subtree from sys.modules. Only dropping the
+ # root module would leave submodules cached; on reinstall the cached
+ # submodules would be reused without re-running their @invocation
+ # decorators, so the pack would show up with 0 nodes until restart.
+ _purge_pack_modules(pack_name)
+
+ # Remove only workflows this pack imported, using the manifest-recorded IDs
+ workflows_removed = _remove_workflows_by_ids(imported_workflow_ids, pack_name)
+ workflow_msg = f" Removed {workflows_removed} workflow(s)." if workflows_removed > 0 else ""
+
+ return UninstallNodePackResponse(
+ name=pack_name,
+ success=True,
+ message=f"Successfully uninstalled node pack '{pack_name}'.{workflow_msg}",
+ )
+ except Exception:
+ error = traceback.format_exc()
+ logger.error(f"Failed to uninstall node pack {pack_name}: {error}")
+ return UninstallNodePackResponse(
+ name=pack_name,
+ success=False,
+ message=f"Uninstall failed: {error}",
+ )
+
+
+@custom_nodes_router.post(
+ "/reload",
+ operation_id="reload_custom_nodes",
+)
+async def reload_custom_nodes(current_admin: AdminUserOrDefault) -> dict[str, str]:
+ """Triggers a reload of all custom nodes.
+
+ This re-scans the nodes directory and loads any new node packs.
+ Already loaded packs are skipped.
+ """
+ config = get_config()
+ custom_nodes_path = config.custom_nodes_path
+
+ if not custom_nodes_path.exists():
+ return {"status": "No custom nodes directory found."}
+
+ from invokeai.app.invocations.load_custom_nodes import load_custom_nodes
+
+ load_custom_nodes(custom_nodes_path, logger)
+
+ # Invalidate the OpenAPI schema cache so the frontend gets updated node definitions
+ from invokeai.app.api_app import app
+
+ app.openapi_schema = None
+
+ return {"status": "Custom nodes reloaded successfully."}
+
+
+def _purge_pack_modules(pack_name: str) -> list[str]:
+ """Removes the pack's root module and all of its submodules from sys.modules.
+
+ After uninstall, cached submodules (e.g. `pack_name.nodes`, `pack_name.foo.bar`)
+ must be evicted as well — otherwise a subsequent reinstall reuses the cached
+ objects, the @invocation decorators never re-run, and the pack ends up loaded
+ with zero registered nodes until a full process restart.
+ """
+ prefix = f"{pack_name}."
+ to_remove = [name for name in sys.modules if name == pack_name or name.startswith(prefix)]
+ for name in to_remove:
+ del sys.modules[name]
+ return to_remove
+
+
+def _load_node_pack(pack_name: str, pack_dir: Path) -> None:
+ """Loads a single node pack at runtime."""
+ init = pack_dir / "__init__.py"
+ if not init.exists():
+ return
+
+ if pack_name in sys.modules:
+ logger.info(f"Node pack {pack_name} already loaded, skipping.")
+ return
+
+ spec = spec_from_file_location(pack_name, init.absolute())
+ if spec is None or spec.loader is None:
+ logger.warning(f"Could not load {init}")
+ return
+
+ logger.info(f"Loading node pack {pack_name}")
+ module = module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+
+ # Invalidate OpenAPI schema cache
+ from invokeai.app.api_app import app
+
+ app.openapi_schema = None
+
+ logger.info(f"Successfully loaded node pack {pack_name}")
+
+
+def _import_workflows_from_pack(pack_dir: Path, pack_name: str, owner_user_id: str) -> list[str]:
+ """Scans a node pack directory for workflow JSON files and imports them into the workflow library.
+
+ A JSON file is considered a workflow if it contains 'nodes' and 'edges' keys at the top level.
+ Workflows are imported as user workflows owned by the installing admin and marked public so all
+ users can see them — a pack is an admin-installed shared resource, not a private asset.
+
+ Returns the list of workflow IDs successfully created, in import order.
+ """
+ imported_ids: list[str] = []
+
+ # Search for .json files recursively
+ for json_file in pack_dir.rglob("*.json"):
+ # Skip our own manifest file
+ if json_file.name == PACK_MANIFEST_FILENAME:
+ continue
+ try:
+ with open(json_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ # Check if this looks like a workflow (must have nodes and edges)
+ if not isinstance(data, dict):
+ continue
+ if "nodes" not in data or "edges" not in data:
+ continue
+
+ # Ensure the workflow has a meta section with category set to "user"
+ if "meta" not in data:
+ data["meta"] = {"version": "3.0.0", "category": "user"}
+ else:
+ data["meta"]["category"] = "user"
+
+ # Add the node pack name to tags for discoverability (display only — uninstall
+ # does not rely on this tag, since users can edit tags on their own workflows).
+ existing_tags = data.get("tags", "")
+ pack_tag = f"node-pack:{pack_name}"
+ if pack_tag not in existing_tags:
+ data["tags"] = f"{existing_tags}, {pack_tag}".strip(", ") if existing_tags else pack_tag
+
+ # Remove the 'id' field if present — the system will assign a new one
+ data.pop("id", None)
+
+ # Validate and import the workflow
+ workflow = WorkflowWithoutIDValidator.validate_python(data)
+ created = ApiDependencies.invoker.services.workflow_records.create(
+ workflow=workflow, user_id=owner_user_id, is_public=True
+ )
+ imported_ids.append(created.workflow_id)
+ logger.info(f"Imported workflow '{workflow.name}' from node pack '{pack_name}'")
+
+ except Exception:
+ logger.warning(f"Skipped non-workflow or invalid JSON file: {json_file}")
+ continue
+
+ if imported_ids:
+ logger.info(f"Imported {len(imported_ids)} workflow(s) from node pack '{pack_name}'")
+
+ return imported_ids
+
+
+def _write_pack_manifest(pack_dir: Path, workflow_ids: list[str]) -> None:
+ """Writes the pack manifest recording which workflow IDs were imported from the pack."""
+ manifest_path = pack_dir / PACK_MANIFEST_FILENAME
+ try:
+ with open(manifest_path, "w", encoding="utf-8") as f:
+ json.dump({"workflow_ids": workflow_ids}, f)
+ except Exception:
+ logger.warning(f"Failed to write pack manifest at {manifest_path}")
+
+
+def _read_pack_manifest(pack_dir: Path) -> list[str]:
+ """Reads workflow IDs that this pack's install recorded in its manifest.
+
+ Returns an empty list if the manifest is missing or malformed. We deliberately do NOT
+ fall back to tag-based lookup: workflow tags are user-editable and could collide with
+ unrelated workflows, so we only delete what we recorded ourselves at install time.
+ """
+ manifest_path = pack_dir / PACK_MANIFEST_FILENAME
+ if not manifest_path.exists():
+ return []
+ try:
+ with open(manifest_path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ ids = data.get("workflow_ids", [])
+ if not isinstance(ids, list):
+ return []
+ return [str(x) for x in ids if isinstance(x, str)]
+ except Exception:
+ logger.warning(f"Failed to read pack manifest at {manifest_path}")
+ return []
+
+
+def _remove_workflows_by_ids(workflow_ids: list[str], pack_name: str) -> int:
+ """Deletes the given workflow IDs. Used during uninstall to remove only the workflows
+ this pack's install recorded in its manifest.
+ """
+ if not workflow_ids:
+ return 0
+
+ removed_count = 0
+ for workflow_id in workflow_ids:
+ try:
+ ApiDependencies.invoker.services.workflow_records.delete(workflow_id)
+ removed_count += 1
+ except Exception:
+ logger.warning(f"Failed to remove workflow '{workflow_id}' (from node pack '{pack_name}')")
+
+ if removed_count > 0:
+ logger.info(f"Removed {removed_count} workflow(s) from node pack '{pack_name}'")
+
+ return removed_count
diff --git a/invokeai/app/api/routers/download_queue.py b/invokeai/app/api/routers/download_queue.py
new file mode 100644
index 00000000000..305eaf9273e
--- /dev/null
+++ b/invokeai/app/api/routers/download_queue.py
@@ -0,0 +1,138 @@
+# Copyright (c) 2023 Lincoln D. Stein
+"""FastAPI route for the download queue."""
+
+from pathlib import Path as FsPath
+from pathlib import PurePosixPath, PureWindowsPath
+from typing import List, Optional
+
+from fastapi import Body, Path, Response
+from fastapi.routing import APIRouter
+from pydantic.networks import AnyHttpUrl
+from starlette.exceptions import HTTPException
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.download import (
+ DownloadJob,
+ UnknownJobIDException,
+)
+
+download_queue_router = APIRouter(prefix="/v1/download_queue", tags=["download_queue"])
+
+
+def _validate_dest(dest: str) -> str:
+ """Reject absolute paths and parent-traversal segments.
+
+ Accepts a relative POSIX- or Windows-style path. Returns the original string
+ for the caller to wrap in `Path(...)`. Raises 400 on suspicious input so the
+ download service never sees it.
+ """
+ if not dest or not dest.strip():
+ raise HTTPException(status_code=400, detail="Download destination must not be empty.")
+
+ posix = PurePosixPath(dest)
+ windows = PureWindowsPath(dest)
+ if posix.is_absolute() or windows.is_absolute():
+ raise HTTPException(status_code=400, detail="Download destination must be a relative path.")
+
+ if ".." in posix.parts or ".." in windows.parts:
+ raise HTTPException(status_code=400, detail="Download destination must not contain '..' segments.")
+
+ return dest
+
+
+@download_queue_router.get(
+ "/",
+ operation_id="list_downloads",
+)
+async def list_downloads(current_user: CurrentUserOrDefault) -> List[DownloadJob]:
+ """Get a list of active and inactive jobs."""
+ queue = ApiDependencies.invoker.services.download_queue
+ return queue.list_jobs()
+
+
+@download_queue_router.patch(
+ "/",
+ operation_id="prune_downloads",
+ responses={
+ 204: {"description": "All completed jobs have been pruned"},
+ 400: {"description": "Bad request"},
+ },
+)
+async def prune_downloads(current_user: AdminUserOrDefault) -> Response:
+ """Prune completed and errored jobs."""
+ queue = ApiDependencies.invoker.services.download_queue
+ queue.prune_jobs()
+ return Response(status_code=204)
+
+
+@download_queue_router.post(
+ "/i/",
+ operation_id="download",
+)
+async def download(
+ current_user: CurrentUserOrDefault,
+ source: AnyHttpUrl = Body(description="download source"),
+ dest: str = Body(description="download destination"),
+ priority: int = Body(default=10, description="queue priority"),
+ access_token: Optional[str] = Body(default=None, description="token for authorization to download"),
+) -> DownloadJob:
+ """Download the source URL to the file or directory indicted in dest."""
+ validated_dest = _validate_dest(dest)
+ queue = ApiDependencies.invoker.services.download_queue
+ return queue.download(source, FsPath(validated_dest), priority, access_token)
+
+
+@download_queue_router.get(
+ "/i/{id}",
+ operation_id="get_download_job",
+ responses={
+ 200: {"description": "Success"},
+ 404: {"description": "The requested download JobID could not be found"},
+ },
+)
+async def get_download_job(
+ current_user: CurrentUserOrDefault,
+ id: int = Path(description="ID of the download job to fetch."),
+) -> DownloadJob:
+ """Get a download job using its ID."""
+ try:
+ job = ApiDependencies.invoker.services.download_queue.id_to_job(id)
+ return job
+ except UnknownJobIDException as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@download_queue_router.delete(
+ "/i/{id}",
+ operation_id="cancel_download_job",
+ responses={
+ 204: {"description": "Job has been cancelled"},
+ 404: {"description": "The requested download JobID could not be found"},
+ },
+)
+async def cancel_download_job(
+ current_user: CurrentUserOrDefault,
+ id: int = Path(description="ID of the download job to cancel."),
+) -> Response:
+ """Cancel a download job using its ID."""
+ try:
+ queue = ApiDependencies.invoker.services.download_queue
+ job = queue.id_to_job(id)
+ queue.cancel_job(job)
+ return Response(status_code=204)
+ except UnknownJobIDException as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@download_queue_router.delete(
+ "/i",
+ operation_id="cancel_all_download_jobs",
+ responses={
+ 204: {"description": "Download jobs have been cancelled"},
+ },
+)
+async def cancel_all_download_jobs(current_user: AdminUserOrDefault) -> Response:
+ """Cancel all download jobs."""
+ ApiDependencies.invoker.services.download_queue.cancel_all_jobs()
+ return Response(status_code=204)
diff --git a/invokeai/app/api/routers/images.py b/invokeai/app/api/routers/images.py
new file mode 100644
index 00000000000..976434c68f2
--- /dev/null
+++ b/invokeai/app/api/routers/images.py
@@ -0,0 +1,752 @@
+import io
+import json
+import traceback
+from typing import ClassVar, Optional
+
+from fastapi import BackgroundTasks, Body, HTTPException, Path, Query, Request, Response, UploadFile
+from fastapi.responses import FileResponse
+from fastapi.routing import APIRouter
+from PIL import Image
+from pydantic import BaseModel, Field, model_validator
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.extract_metadata_from_image import extract_metadata_from_image
+from invokeai.app.api.routers._access import (
+ assert_board_read_access as _assert_board_read_access,
+)
+from invokeai.app.api.routers._access import (
+ assert_image_owner as _assert_image_owner,
+)
+from invokeai.app.api.routers._access import (
+ assert_image_read_access as _assert_image_read_access,
+)
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ ImageNamesResult,
+ ImageRecordChanges,
+ ResourceOrigin,
+)
+from invokeai.app.services.images.images_common import (
+ DeleteImagesResult,
+ ImageDTO,
+ ImageUrlsDTO,
+ StarredImagesResult,
+ UnstarredImagesResult,
+)
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.util.controlnet_utils import heuristic_resize_fast
+from invokeai.backend.image_util.util import np_to_pil, pil_to_np
+
+images_router = APIRouter(prefix="/v1/images", tags=["images"])
+
+
+# images are immutable; set a high max-age
+IMAGE_MAX_AGE = 31536000
+
+
+class ResizeToDimensions(BaseModel):
+ width: int = Field(..., gt=0)
+ height: int = Field(..., gt=0)
+
+ MAX_SIZE: ClassVar[int] = 4096 * 4096
+
+ @model_validator(mode="after")
+ def validate_total_output_size(self):
+ if self.width * self.height > self.MAX_SIZE:
+ raise ValueError(f"Max total output size for resizing is {self.MAX_SIZE} pixels")
+ return self
+
+
+@images_router.post(
+ "/upload",
+ operation_id="upload_image",
+ responses={
+ 201: {"description": "The image was uploaded successfully"},
+ 415: {"description": "Image upload failed"},
+ },
+ status_code=201,
+ response_model=ImageDTO,
+)
+async def upload_image(
+ current_user: CurrentUserOrDefault,
+ file: UploadFile,
+ request: Request,
+ response: Response,
+ image_category: ImageCategory = Query(description="The category of the image"),
+ is_intermediate: bool = Query(description="Whether this is an intermediate image"),
+ board_id: Optional[str] = Query(default=None, description="The board to add this image to, if any"),
+ session_id: Optional[str] = Query(default=None, description="The session ID associated with this upload, if any"),
+ crop_visible: Optional[bool] = Query(default=False, description="Whether to crop the image"),
+ resize_to: Optional[str] = Body(
+ default=None,
+ description=f"Dimensions to resize the image to, must be stringified tuple of 2 integers. Max total pixel count: {ResizeToDimensions.MAX_SIZE}",
+ examples=['"[1024,1024]"'],
+ ),
+ metadata: Optional[str] = Body(
+ default=None,
+ description="The metadata to associate with the image, must be a stringified JSON dict",
+ embed=True,
+ ),
+) -> ImageDTO:
+ """Uploads an image for the current user"""
+ # If uploading into a board, verify the user has write access.
+ # Public boards allow uploads from any authenticated user.
+ if board_id is not None:
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ except Exception:
+ raise HTTPException(status_code=404, detail="Board not found")
+ if (
+ not current_user.is_admin
+ and board.user_id != current_user.user_id
+ and board.board_visibility != BoardVisibility.Public
+ ):
+ raise HTTPException(status_code=403, detail="Not authorized to upload to this board")
+
+ if not file.content_type or not file.content_type.startswith("image"):
+ raise HTTPException(status_code=415, detail="Not an image")
+
+ contents = await file.read()
+ try:
+ pil_image = Image.open(io.BytesIO(contents))
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read image")
+
+ if crop_visible:
+ try:
+ bbox = pil_image.getbbox()
+ pil_image = pil_image.crop(bbox)
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to crop image")
+
+ if resize_to:
+ try:
+ dims = json.loads(resize_to)
+ resize_dims = ResizeToDimensions(**dims)
+ except Exception:
+ raise HTTPException(status_code=400, detail="Invalid resize_to format or size")
+
+ try:
+ # heuristic_resize_fast expects an RGB or RGBA image
+ pil_rgba = pil_image.convert("RGBA")
+ np_image = pil_to_np(pil_rgba)
+ np_image = heuristic_resize_fast(np_image, (resize_dims.width, resize_dims.height))
+ pil_image = np_to_pil(np_image)
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to resize image")
+
+ extracted_metadata = extract_metadata_from_image(
+ pil_image=pil_image,
+ invokeai_metadata_override=metadata,
+ invokeai_workflow_override=None,
+ invokeai_graph_override=None,
+ logger=ApiDependencies.invoker.services.logger,
+ )
+
+ try:
+ image_dto = ApiDependencies.invoker.services.images.create(
+ image=pil_image,
+ image_origin=ResourceOrigin.EXTERNAL,
+ image_category=image_category,
+ session_id=session_id,
+ board_id=board_id,
+ metadata=extracted_metadata.invokeai_metadata,
+ workflow=extracted_metadata.invokeai_workflow,
+ graph=extracted_metadata.invokeai_graph,
+ is_intermediate=is_intermediate,
+ user_id=current_user.user_id,
+ )
+
+ response.status_code = 201
+ response.headers["Location"] = image_dto.image_url
+
+ return image_dto
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=500, detail="Failed to create image")
+
+
+class ImageUploadEntry(BaseModel):
+ image_dto: ImageDTO = Body(description="The image DTO")
+ presigned_url: str = Body(description="The URL to get the presigned URL for the image upload")
+
+
+@images_router.post("/", operation_id="create_image_upload_entry")
+async def create_image_upload_entry(
+ width: int = Body(description="The width of the image"),
+ height: int = Body(description="The height of the image"),
+ board_id: Optional[str] = Body(default=None, description="The board to add this image to, if any"),
+) -> ImageUploadEntry:
+ """Uploads an image from a URL, not implemented"""
+
+ raise HTTPException(status_code=501, detail="Not implemented")
+
+
+@images_router.delete("/i/{image_name}", operation_id="delete_image", response_model=DeleteImagesResult)
+async def delete_image(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of the image to delete"),
+) -> DeleteImagesResult:
+ """Deletes an image"""
+ _assert_image_owner(image_name, current_user)
+
+ deleted_images: set[str] = set()
+ affected_boards: set[str] = set()
+
+ try:
+ image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
+ board_id = image_dto.board_id or "none"
+ ApiDependencies.invoker.services.images.delete(image_name)
+ deleted_images.add(image_name)
+ affected_boards.add(board_id)
+ except Exception:
+ # TODO: Does this need any exception handling at all?
+ pass
+
+ return DeleteImagesResult(
+ deleted_images=list(deleted_images),
+ affected_boards=list(affected_boards),
+ )
+
+
+@images_router.delete("/intermediates", operation_id="clear_intermediates")
+async def clear_intermediates(
+ current_user: CurrentUserOrDefault,
+) -> int:
+ """Clears all intermediates. Requires admin."""
+ if not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Only admins can clear all intermediates")
+
+ try:
+ count_deleted = ApiDependencies.invoker.services.images.delete_intermediates()
+ return count_deleted
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to clear intermediates")
+
+
+@images_router.get("/intermediates", operation_id="get_intermediates_count")
+async def get_intermediates_count(
+ current_user: CurrentUserOrDefault,
+) -> int:
+ """Gets the count of intermediate images. Non-admin users only see their own intermediates."""
+
+ try:
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.images.get_intermediates_count(user_id=user_id)
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get intermediates")
+
+
+@images_router.patch(
+ "/i/{image_name}",
+ operation_id="update_image",
+ response_model=ImageDTO,
+)
+async def update_image(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of the image to update"),
+ image_changes: ImageRecordChanges = Body(description="The changes to apply to the image"),
+) -> ImageDTO:
+ """Updates an image"""
+ _assert_image_owner(image_name, current_user)
+
+ try:
+ return ApiDependencies.invoker.services.images.update(image_name, image_changes)
+ except Exception:
+ raise HTTPException(status_code=400, detail="Failed to update image")
+
+
+@images_router.get(
+ "/i/{image_name}",
+ operation_id="get_image_dto",
+ response_model=ImageDTO,
+)
+async def get_image_dto(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of image to get"),
+) -> ImageDTO:
+ """Gets an image's DTO"""
+ _assert_image_read_access(image_name, current_user)
+
+ try:
+ return ApiDependencies.invoker.services.images.get_dto(image_name)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get(
+ "/i/{image_name}/metadata",
+ operation_id="get_image_metadata",
+ response_model=Optional[MetadataField],
+)
+async def get_image_metadata(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of image to get"),
+) -> Optional[MetadataField]:
+ """Gets an image's metadata"""
+ _assert_image_read_access(image_name, current_user)
+
+ try:
+ return ApiDependencies.invoker.services.images.get_metadata(image_name)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+class WorkflowAndGraphResponse(BaseModel):
+ workflow: Optional[str] = Field(description="The workflow used to generate the image, as stringified JSON")
+ graph: Optional[str] = Field(description="The graph used to generate the image, as stringified JSON")
+
+
+@images_router.get(
+ "/i/{image_name}/workflow", operation_id="get_image_workflow", response_model=WorkflowAndGraphResponse
+)
+async def get_image_workflow(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of image whose workflow to get"),
+) -> WorkflowAndGraphResponse:
+ _assert_image_read_access(image_name, current_user)
+
+ try:
+ workflow = ApiDependencies.invoker.services.images.get_workflow(image_name)
+ graph = ApiDependencies.invoker.services.images.get_graph(image_name)
+ return WorkflowAndGraphResponse(workflow=workflow, graph=graph)
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get(
+ "/i/{image_name}/full",
+ operation_id="get_image_full",
+ response_class=Response,
+ responses={
+ 200: {
+ "description": "Return the full-resolution image",
+ "content": {"image/png": {}},
+ },
+ 404: {"description": "Image not found"},
+ },
+)
+@images_router.head(
+ "/i/{image_name}/full",
+ operation_id="get_image_full_head",
+ response_class=Response,
+ responses={
+ 200: {
+ "description": "Return the full-resolution image",
+ "content": {"image/png": {}},
+ },
+ 404: {"description": "Image not found"},
+ },
+)
+async def get_image_full(
+ image_name: str = Path(description="The name of full-resolution image file to get"),
+) -> Response:
+ """Gets a full-resolution image file.
+
+ This endpoint is intentionally unauthenticated because browsers load images
+ via tags which cannot send Bearer tokens. Image names are UUIDs,
+ providing security through unguessability.
+ """
+ try:
+ path = ApiDependencies.invoker.services.images.get_path(image_name)
+ with open(path, "rb") as f:
+ content = f.read()
+ response = Response(content, media_type="image/png")
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ response.headers["Content-Disposition"] = f'inline; filename="{image_name}"'
+ return response
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get(
+ "/i/{image_name}/thumbnail",
+ operation_id="get_image_thumbnail",
+ response_class=Response,
+ responses={
+ 200: {
+ "description": "Return the image thumbnail",
+ "content": {"image/webp": {}},
+ },
+ 404: {"description": "Image not found"},
+ },
+)
+async def get_image_thumbnail(
+ image_name: str = Path(description="The name of thumbnail image file to get"),
+) -> Response:
+ """Gets a thumbnail image file.
+
+ This endpoint is intentionally unauthenticated because browsers load images
+ via tags which cannot send Bearer tokens. Image names are UUIDs,
+ providing security through unguessability.
+ """
+ try:
+ path = ApiDependencies.invoker.services.images.get_path(image_name, thumbnail=True)
+ with open(path, "rb") as f:
+ content = f.read()
+ response = Response(content, media_type="image/webp")
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ return response
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get(
+ "/i/{image_name}/urls",
+ operation_id="get_image_urls",
+ response_model=ImageUrlsDTO,
+)
+async def get_image_urls(
+ current_user: CurrentUserOrDefault,
+ image_name: str = Path(description="The name of the image whose URL to get"),
+) -> ImageUrlsDTO:
+ """Gets an image and thumbnail URL"""
+ _assert_image_read_access(image_name, current_user)
+
+ try:
+ image_url = ApiDependencies.invoker.services.images.get_url(image_name)
+ thumbnail_url = ApiDependencies.invoker.services.images.get_url(image_name, thumbnail=True)
+ return ImageUrlsDTO(
+ image_name=image_name,
+ image_url=image_url,
+ thumbnail_url=thumbnail_url,
+ )
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get(
+ "/",
+ operation_id="list_image_dtos",
+ response_model=OffsetPaginatedResults[ImageDTO],
+)
+async def list_image_dtos(
+ current_user: CurrentUserOrDefault,
+ image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
+ categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find images without a board.",
+ ),
+ offset: int = Query(default=0, description="The page offset"),
+ limit: int = Query(default=10, description="The number of images per page"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred images first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> OffsetPaginatedResults[ImageDTO]:
+ """Gets a list of image DTOs for the current user"""
+
+ # Validate that the caller can read from this board before listing its images.
+ # "none" is a sentinel for uncategorized images and is handled by the SQL layer.
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ image_dtos = ApiDependencies.invoker.services.images.get_many(
+ offset,
+ limit,
+ starred_first,
+ order_dir,
+ image_origin,
+ categories,
+ is_intermediate,
+ board_id,
+ search_term,
+ current_user.user_id,
+ )
+
+ return image_dtos
+
+
+@images_router.post("/delete", operation_id="delete_images_from_list", response_model=DeleteImagesResult)
+async def delete_images_from_list(
+ current_user: CurrentUserOrDefault,
+ image_names: list[str] = Body(description="The list of names of images to delete", embed=True),
+) -> DeleteImagesResult:
+ try:
+ deleted_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ _assert_image_owner(image_name, current_user)
+ image_dto = ApiDependencies.invoker.services.images.get_dto(image_name)
+ board_id = image_dto.board_id or "none"
+ ApiDependencies.invoker.services.images.delete(image_name)
+ deleted_images.add(image_name)
+ affected_boards.add(board_id)
+ except HTTPException:
+ raise
+ except Exception:
+ pass
+ return DeleteImagesResult(
+ deleted_images=list(deleted_images),
+ affected_boards=list(affected_boards),
+ )
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to delete images")
+
+
+@images_router.delete("/uncategorized", operation_id="delete_uncategorized_images", response_model=DeleteImagesResult)
+async def delete_uncategorized_images(
+ current_user: CurrentUserOrDefault,
+) -> DeleteImagesResult:
+ """Deletes all uncategorized images owned by the current user (or all if admin)"""
+
+ image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
+ board_id="none", categories=None, is_intermediate=None
+ )
+
+ try:
+ deleted_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ _assert_image_owner(image_name, current_user)
+ ApiDependencies.invoker.services.images.delete(image_name)
+ deleted_images.add(image_name)
+ affected_boards.add("none")
+ except HTTPException:
+ # Skip images not owned by the current user
+ pass
+ except Exception:
+ pass
+ return DeleteImagesResult(
+ deleted_images=list(deleted_images),
+ affected_boards=list(affected_boards),
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to delete images")
+
+
+class ImagesUpdatedFromListResult(BaseModel):
+ updated_image_names: list[str] = Field(description="The image names that were updated")
+
+
+@images_router.post("/star", operation_id="star_images_in_list", response_model=StarredImagesResult)
+async def star_images_in_list(
+ current_user: CurrentUserOrDefault,
+ image_names: list[str] = Body(description="The list of names of images to star", embed=True),
+) -> StarredImagesResult:
+ try:
+ starred_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ _assert_image_owner(image_name, current_user)
+ updated_image_dto = ApiDependencies.invoker.services.images.update(
+ image_name, changes=ImageRecordChanges(starred=True)
+ )
+ starred_images.add(image_name)
+ affected_boards.add(updated_image_dto.board_id or "none")
+ except HTTPException:
+ raise
+ except Exception:
+ pass
+ return StarredImagesResult(
+ starred_images=list(starred_images),
+ affected_boards=list(affected_boards),
+ )
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to star images")
+
+
+@images_router.post("/unstar", operation_id="unstar_images_in_list", response_model=UnstarredImagesResult)
+async def unstar_images_in_list(
+ current_user: CurrentUserOrDefault,
+ image_names: list[str] = Body(description="The list of names of images to unstar", embed=True),
+) -> UnstarredImagesResult:
+ try:
+ unstarred_images: set[str] = set()
+ affected_boards: set[str] = set()
+ for image_name in image_names:
+ try:
+ _assert_image_owner(image_name, current_user)
+ updated_image_dto = ApiDependencies.invoker.services.images.update(
+ image_name, changes=ImageRecordChanges(starred=False)
+ )
+ unstarred_images.add(image_name)
+ affected_boards.add(updated_image_dto.board_id or "none")
+ except HTTPException:
+ raise
+ except Exception:
+ pass
+ return UnstarredImagesResult(
+ unstarred_images=list(unstarred_images),
+ affected_boards=list(affected_boards),
+ )
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to unstar images")
+
+
+class ImagesDownloaded(BaseModel):
+ response: Optional[str] = Field(
+ default=None, description="The message to display to the user when images begin downloading"
+ )
+ bulk_download_item_name: Optional[str] = Field(
+ default=None, description="The name of the bulk download item for which events will be emitted"
+ )
+
+
+@images_router.post(
+ "/download", operation_id="download_images_from_list", response_model=ImagesDownloaded, status_code=202
+)
+async def download_images_from_list(
+ current_user: CurrentUserOrDefault,
+ background_tasks: BackgroundTasks,
+ image_names: Optional[list[str]] = Body(
+ default=None, 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", embed=True
+ ),
+) -> ImagesDownloaded:
+ if (image_names is None or len(image_names) == 0) and board_id is None:
+ raise HTTPException(status_code=400, detail="No images or board id specified.")
+
+ # Validate that the caller can read every image they are requesting.
+ # For a board_id request, check board visibility; for explicit image names,
+ # check each image individually.
+ if board_id:
+ _assert_board_read_access(board_id, current_user)
+ if image_names:
+ for name in image_names:
+ _assert_image_read_access(name, current_user)
+
+ bulk_download_item_id: str = ApiDependencies.invoker.services.bulk_download.generate_item_id(board_id)
+
+ background_tasks.add_task(
+ ApiDependencies.invoker.services.bulk_download.handler,
+ image_names,
+ board_id,
+ bulk_download_item_id,
+ current_user.user_id,
+ )
+ return ImagesDownloaded(bulk_download_item_name=bulk_download_item_id + ".zip")
+
+
+@images_router.api_route(
+ "/download/{bulk_download_item_name}",
+ methods=["GET"],
+ operation_id="get_bulk_download_item",
+ response_class=Response,
+ responses={
+ 200: {
+ "description": "Return the complete bulk download item",
+ "content": {"application/zip": {}},
+ },
+ 404: {"description": "Image not found"},
+ },
+)
+async def get_bulk_download_item(
+ current_user: CurrentUserOrDefault,
+ background_tasks: BackgroundTasks,
+ bulk_download_item_name: str = Path(description="The bulk_download_item_name of the bulk download item to get"),
+) -> FileResponse:
+ """Gets a bulk download zip file.
+
+ Requires authentication. The caller must be the user who initiated the
+ download (tracked by the bulk download service) or an admin.
+ """
+ try:
+ # Verify the caller owns this download (or is an admin)
+ owner = ApiDependencies.invoker.services.bulk_download.get_owner(bulk_download_item_name)
+ if owner is not None and owner != current_user.user_id and not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Not authorized to access this download")
+
+ path = ApiDependencies.invoker.services.bulk_download.get_path(bulk_download_item_name)
+
+ response = FileResponse(
+ path,
+ media_type="application/zip",
+ filename=bulk_download_item_name,
+ content_disposition_type="inline",
+ )
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ background_tasks.add_task(ApiDependencies.invoker.services.bulk_download.delete, bulk_download_item_name)
+ return response
+ except HTTPException:
+ raise
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@images_router.get("/names", operation_id="get_image_names")
+async def get_image_names(
+ current_user: CurrentUserOrDefault,
+ image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
+ categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
+ is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
+ board_id: Optional[str] = Query(
+ default=None,
+ description="The board id to filter by. Use 'none' to find images without a board.",
+ ),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+ starred_first: bool = Query(default=True, description="Whether to sort by starred images first"),
+ search_term: Optional[str] = Query(default=None, description="The term to search for"),
+) -> ImageNamesResult:
+ """Gets ordered list of image names with metadata for optimistic updates"""
+
+ # Validate that the caller can read from this board before listing its images.
+ if board_id is not None and board_id != "none":
+ _assert_board_read_access(board_id, current_user)
+
+ try:
+ result = ApiDependencies.invoker.services.images.get_image_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ image_origin=image_origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ return result
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get image names")
+
+
+@images_router.post(
+ "/images_by_names",
+ operation_id="get_images_by_names",
+ responses={200: {"model": list[ImageDTO]}},
+)
+async def get_images_by_names(
+ current_user: CurrentUserOrDefault,
+ image_names: list[str] = Body(embed=True, description="Object containing list of image names to fetch DTOs for"),
+) -> list[ImageDTO]:
+ """Gets image DTOs for the specified image names. Maintains order of input names."""
+
+ try:
+ image_service = ApiDependencies.invoker.services.images
+
+ # Fetch DTOs preserving the order of requested names
+ image_dtos: list[ImageDTO] = []
+ for name in image_names:
+ try:
+ _assert_image_read_access(name, current_user)
+ dto = image_service.get_dto(name)
+ image_dtos.append(dto)
+ except HTTPException:
+ # Skip images the user is not authorized to view
+ continue
+ except Exception:
+ # Skip missing images - they may have been deleted between name fetch and DTO fetch
+ continue
+
+ return image_dtos
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get image DTOs")
diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py
new file mode 100644
index 00000000000..bdd2e406444
--- /dev/null
+++ b/invokeai/app/api/routers/model_manager.py
@@ -0,0 +1,1447 @@
+# Copyright (c) 2023 Lincoln D. Stein
+"""FastAPI route for model configuration records."""
+
+import contextlib
+import io
+import pathlib
+import traceback
+from copy import deepcopy
+from enum import Enum
+from tempfile import TemporaryDirectory
+from typing import List, Optional, Type
+
+import huggingface_hub
+from fastapi import Body, Path, Query, Response, UploadFile
+from fastapi.responses import FileResponse, HTMLResponse
+from fastapi.routing import APIRouter
+from PIL import Image
+from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
+from starlette.exceptions import HTTPException
+from typing_extensions import Annotated
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
+from invokeai.app.services.model_install.model_install_common import ModelInstallJob
+from invokeai.app.services.model_records import (
+ InvalidModelException,
+ ModelRecordChanges,
+ ModelRecordOrderBy,
+ UnknownModelException,
+)
+from invokeai.app.services.orphaned_models import OrphanedModelInfo
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.util.suppress_output import SuppressOutput
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig, ModelConfigFactory
+from invokeai.backend.model_manager.configs.main import (
+ Main_Checkpoint_SD1_Config,
+ Main_Checkpoint_SD2_Config,
+ Main_Checkpoint_SDXL_Config,
+ Main_Checkpoint_SDXLRefiner_Config,
+)
+from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
+from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
+from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+from invokeai.backend.model_manager.search import ModelSearch
+from invokeai.backend.model_manager.starter_models import (
+ STARTER_BUNDLES,
+ STARTER_MODELS,
+ StarterModel,
+ StarterModelBundle,
+ StarterModelWithoutDependencies,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+model_manager_router = APIRouter(prefix="/v2/models", tags=["model_manager"])
+
+# images are immutable; set a high max-age
+IMAGE_MAX_AGE = 31536000
+
+
+class ModelsList(BaseModel):
+ """Return list of configs."""
+
+ models: List[AnyModelConfig]
+
+ model_config = ConfigDict(use_enum_values=True)
+
+
+class CacheType(str, Enum):
+ """Cache type - one of vram or ram."""
+
+ RAM = "RAM"
+ VRAM = "VRAM"
+
+
+def add_cover_image_to_model_config(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig:
+ """Add a cover image URL to a model configuration."""
+ cover_image = dependencies.invoker.services.model_images.get_url(config.key)
+ return config.model_copy(update={"cover_image": cover_image})
+
+
+def apply_external_starter_model_overrides(config: AnyModelConfig) -> AnyModelConfig:
+ """Overlay starter-model metadata onto installed external model configs."""
+ if not isinstance(config, ExternalApiModelConfig):
+ return config
+
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == config.source), None)
+ if starter_match is None:
+ return config
+
+ model_updates: dict[str, object] = {}
+ if starter_match.capabilities is not None:
+ model_updates["capabilities"] = starter_match.capabilities
+ if starter_match.default_settings is not None:
+ model_updates["default_settings"] = starter_match.default_settings
+ if starter_match.panel_schema is not None:
+ model_updates["panel_schema"] = starter_match.panel_schema
+
+ if not model_updates:
+ return config
+
+ return config.model_copy(update=model_updates)
+
+
+def prepare_model_config_for_response(config: AnyModelConfig, dependencies: Type[ApiDependencies]) -> AnyModelConfig:
+ """Apply API-only model config overlays before returning a response."""
+ config = apply_external_starter_model_overrides(config)
+ return add_cover_image_to_model_config(config, dependencies)
+
+
+##############################################################################
+# These are example inputs and outputs that are used in places where Swagger
+# is unable to generate a correct example.
+##############################################################################
+example_model_config = {
+ "path": "string",
+ "name": "string",
+ "base": "sd-1",
+ "type": "main",
+ "format": "checkpoint",
+ "config_path": "string",
+ "key": "string",
+ "hash": "string",
+ "file_size": 1,
+ "description": "string",
+ "source": "string",
+ "converted_at": 0,
+ "variant": "normal",
+ "prediction_type": "epsilon",
+ "repo_variant": "fp16",
+ "upcast_attention": False,
+}
+
+example_model_input = {
+ "path": "/path/to/model",
+ "name": "model_name",
+ "base": "sd-1",
+ "type": "main",
+ "format": "checkpoint",
+ "config_path": "configs/stable-diffusion/v1-inference.yaml",
+ "description": "Model description",
+ "vae": None,
+ "variant": "normal",
+}
+
+##############################################################################
+# ROUTES
+##############################################################################
+
+
+@model_manager_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"),
+ model_name: Optional[str] = Query(default=None, description="Exact match on the name of the model"),
+ model_format: Optional[ModelFormat] = Query(
+ default=None, description="Exact match on the format of the model (e.g. 'diffusers')"
+ ),
+ order_by: ModelRecordOrderBy = Query(default=ModelRecordOrderBy.Name, description="The field to order by"),
+ direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"),
+) -> ModelsList:
+ """Get a list of models."""
+ record_store = ApiDependencies.invoker.services.model_manager.store
+ 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,
+ model_name=model_name,
+ model_format=model_format,
+ order_by=order_by,
+ direction=direction,
+ )
+ )
+ else:
+ found_models.extend(
+ record_store.search_by_attr(
+ model_type=model_type,
+ model_name=model_name,
+ model_format=model_format,
+ order_by=order_by,
+ direction=direction,
+ )
+ )
+ for index, model in enumerate(found_models):
+ found_models[index] = prepare_model_config_for_response(model, ApiDependencies)
+ return ModelsList(models=found_models)
+
+
+@model_manager_router.get(
+ "/missing",
+ operation_id="list_missing_models",
+ responses={200: {"description": "List of models with missing files"}},
+)
+async def list_missing_models() -> ModelsList:
+ """Get models whose files are missing from disk.
+
+ These are models that have database entries but their corresponding
+ weight files have been deleted externally (not via Model Manager).
+ """
+ record_store = ApiDependencies.invoker.services.model_manager.store
+ models_path = ApiDependencies.invoker.services.configuration.models_path
+
+ missing_models: list[AnyModelConfig] = []
+ for model_config in record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
+ if not (models_path / model_config.path).resolve().exists():
+ missing_models.append(model_config)
+
+ return ModelsList(models=missing_models)
+
+
+@model_manager_router.get(
+ "/get_by_attrs",
+ operation_id="get_model_records_by_attrs",
+ response_model=AnyModelConfig,
+)
+async def get_model_records_by_attrs(
+ name: str = Query(description="The name of the model"),
+ type: ModelType = Query(description="The type of the model"),
+ base: BaseModelType = Query(description="The base model of the model"),
+) -> AnyModelConfig:
+ """Gets a model by its attributes. The main use of this route is to provide backwards compatibility with the old
+ model manager, which identified models by a combination of name, base and type."""
+ configs = ApiDependencies.invoker.services.model_manager.store.search_by_attr(
+ base_model=base, model_type=type, model_name=name
+ )
+ if not configs:
+ raise HTTPException(status_code=404, detail="No model found with these attributes")
+
+ return prepare_model_config_for_response(configs[0], ApiDependencies)
+
+
+@model_manager_router.get(
+ "/get_by_hash",
+ operation_id="get_model_records_by_hash",
+ response_model=AnyModelConfig,
+)
+async def get_model_records_by_hash(
+ hash: str = Query(description="The hash of the model"),
+) -> AnyModelConfig:
+ """Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled,
+ as the hash remains stable across reinstallations while the key (UUID) changes."""
+ configs = ApiDependencies.invoker.services.model_manager.store.search_by_hash(hash)
+ if not configs:
+ raise HTTPException(status_code=404, detail="No model found with this hash")
+
+ return prepare_model_config_for_response(configs[0], ApiDependencies)
+
+
+@model_manager_router.get(
+ "/i/{key}",
+ operation_id="get_model_record",
+ responses={
+ 200: {
+ "description": "The model configuration was retrieved successfully",
+ "content": {"application/json": {"example": example_model_config}},
+ },
+ 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"""
+ try:
+ config = ApiDependencies.invoker.services.model_manager.store.get_model(key)
+ return prepare_model_config_for_response(config, ApiDependencies)
+ except UnknownModelException as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@model_manager_router.post(
+ "/i/{key}/reidentify",
+ operation_id="reidentify_model",
+ responses={
+ 200: {
+ "description": "The model configuration was retrieved successfully",
+ "content": {"application/json": {"example": example_model_config}},
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "The model could not be found"},
+ },
+)
+async def reidentify_model(
+ key: Annotated[str, Path(description="Key of the model to reidentify.")],
+ current_admin: AdminUserOrDefault,
+) -> AnyModelConfig:
+ """Attempt to reidentify a model by re-probing its weights file."""
+ try:
+ config = ApiDependencies.invoker.services.model_manager.store.get_model(key)
+ models_path = ApiDependencies.invoker.services.configuration.models_path
+ if pathlib.Path(config.path).is_relative_to(models_path):
+ model_path = pathlib.Path(config.path)
+ else:
+ model_path = models_path / config.path
+ mod = ModelOnDisk(model_path)
+ result = ModelConfigFactory.from_model_on_disk(mod)
+ if result.config is None:
+ raise InvalidModelException("Unable to identify model format")
+
+ # Retain user-editable fields from the original config
+ result.config.path = config.path
+ result.config.key = config.key
+ result.config.name = config.name
+ result.config.description = config.description
+ result.config.cover_image = config.cover_image
+ if hasattr(result.config, "trigger_phrases") and hasattr(config, "trigger_phrases"):
+ result.config.trigger_phrases = config.trigger_phrases
+ result.config.source = config.source
+ result.config.source_type = config.source_type
+
+ new_config = ApiDependencies.invoker.services.model_manager.store.replace_model(config.key, result.config)
+ return new_config
+ except UnknownModelException as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+class FoundModel(BaseModel):
+ path: str = Field(description="Path to the model")
+ is_installed: bool = Field(description="Whether or not the model is already installed")
+
+
+@model_manager_router.get(
+ "/scan_folder",
+ operation_id="scan_for_models",
+ responses={
+ 200: {"description": "Directory scanned successfully"},
+ 400: {"description": "Invalid directory path"},
+ },
+ status_code=200,
+ response_model=List[FoundModel],
+)
+async def scan_for_models(
+ scan_path: str = Query(description="Directory path to search for models", default=None),
+) -> List[FoundModel]:
+ path = pathlib.Path(scan_path)
+ if not scan_path or not path.is_dir():
+ raise HTTPException(
+ status_code=400,
+ detail=f"The search path '{scan_path}' does not exist or is not directory",
+ )
+
+ search = ModelSearch()
+ try:
+ found_model_paths = search.search(path)
+ models_path = ApiDependencies.invoker.services.configuration.models_path
+
+ # If the search path includes the main models directory, we need to exclude core models from the list.
+ # TODO(MM2): Core models should be handled by the model manager so we can determine if they are installed
+ # without needing to crawl the filesystem.
+ core_models_path = pathlib.Path(models_path, "core").resolve()
+ non_core_model_paths = [p for p in found_model_paths if not p.is_relative_to(core_models_path)]
+
+ installed_models = ApiDependencies.invoker.services.model_manager.store.search_by_attr()
+
+ scan_results: list[FoundModel] = []
+
+ # Check if the model is installed by comparing paths, appending to the scan result.
+ for p in non_core_model_paths:
+ path = str(p)
+ is_installed = any(str(models_path / m.path) == path for m in installed_models)
+ found_model = FoundModel(path=path, is_installed=is_installed)
+ scan_results.append(found_model)
+ except Exception as e:
+ error_type = type(e).__name__
+ raise HTTPException(
+ status_code=500,
+ detail=f"An error occurred while searching the directory: {error_type}",
+ )
+ return scan_results
+
+
+class HuggingFaceModels(BaseModel):
+ urls: List[AnyHttpUrl] | None = Field(description="URLs for all checkpoint format models in the metadata")
+ is_diffusers: bool = Field(description="Whether the metadata is for a Diffusers format model")
+
+
+@model_manager_router.get(
+ "/hugging_face",
+ operation_id="get_hugging_face_models",
+ responses={
+ 200: {"description": "Hugging Face repo scanned successfully"},
+ 400: {"description": "Invalid hugging face repo"},
+ },
+ status_code=200,
+ response_model=HuggingFaceModels,
+)
+async def get_hugging_face_models(
+ hugging_face_repo: str = Query(description="Hugging face repo to search for models", default=None),
+) -> HuggingFaceModels:
+ try:
+ metadata = HuggingFaceMetadataFetch().from_id(hugging_face_repo)
+ except UnknownMetadataException:
+ raise HTTPException(
+ status_code=400,
+ detail="No HuggingFace repository found",
+ )
+
+ assert isinstance(metadata, ModelMetadataWithFiles)
+
+ return HuggingFaceModels(
+ urls=metadata.ckpt_urls,
+ is_diffusers=metadata.is_diffusers,
+ )
+
+
+@model_manager_router.patch(
+ "/i/{key}",
+ operation_id="update_model_record",
+ responses={
+ 200: {
+ "description": "The model was updated successfully",
+ "content": {"application/json": {"example": example_model_config}},
+ },
+ 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,
+)
+async def update_model_record(
+ key: Annotated[str, Path(description="Unique key of model")],
+ changes: Annotated[ModelRecordChanges, Body(description="Model config", examples=[example_model_input])],
+ current_admin: AdminUserOrDefault,
+) -> AnyModelConfig:
+ """Update a model's config."""
+ logger = ApiDependencies.invoker.services.logger
+ record_store = ApiDependencies.invoker.services.model_manager.store
+ try:
+ previous_config = record_store.get_model(key)
+ config = record_store.update_model(key, changes=changes, allow_class_change=True)
+ # Settings that change how the model loads (e.g. fp8_storage, cpu_only) are baked into the cached
+ # nn.Module at load time, so toggling them on a cached model is otherwise silently a no-op until
+ # the entry is evicted. Drop any unlocked cached entries for this model so the next load rebuilds.
+ if _load_settings_changed(previous_config, config):
+ dropped = ApiDependencies.invoker.services.model_manager.load.ram_cache.drop_model(key)
+ if dropped:
+ logger.info(
+ f"Dropped {dropped} cached entr{'y' if dropped == 1 else 'ies'} for model {key} after settings change."
+ )
+ config = prepare_model_config_for_response(config, ApiDependencies)
+ 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 config
+
+
+_LOAD_AFFECTING_SETTINGS: tuple[str, ...] = ("fp8_storage", "cpu_only")
+
+
+def _load_settings_changed(previous: AnyModelConfig, updated: AnyModelConfig) -> bool:
+ """Return True if any setting that influences how the model is loaded changed.
+
+ Such settings are read by the loader during `_load_model` and baked into the resulting
+ nn.Module, so a cached entry built under the old value must be evicted for the change
+ to take effect.
+ """
+ if getattr(previous, "cpu_only", None) != getattr(updated, "cpu_only", None):
+ return True
+ previous_settings = getattr(previous, "default_settings", None)
+ updated_settings = getattr(updated, "default_settings", None)
+ for field in _LOAD_AFFECTING_SETTINGS:
+ if getattr(previous_settings, field, None) != getattr(updated_settings, field, None):
+ return True
+ return False
+
+
+@model_manager_router.get(
+ "/i/{key}/image",
+ operation_id="get_model_image",
+ responses={
+ 200: {
+ "description": "The model image was fetched successfully",
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "The model image could not be found"},
+ },
+ status_code=200,
+)
+async def get_model_image(
+ key: str = Path(description="The name of model image file to get"),
+) -> FileResponse:
+ """Gets an image file that previews the model"""
+
+ try:
+ path = ApiDependencies.invoker.services.model_images.get_path(key)
+
+ response = FileResponse(
+ path,
+ media_type="image/png",
+ filename=key + ".png",
+ content_disposition_type="inline",
+ )
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ return response
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@model_manager_router.patch(
+ "/i/{key}/image",
+ operation_id="update_model_image",
+ responses={
+ 200: {
+ "description": "The model image was updated successfully",
+ },
+ 400: {"description": "Bad request"},
+ },
+ status_code=200,
+)
+async def update_model_image(
+ key: Annotated[str, Path(description="Unique key of model")],
+ image: UploadFile,
+ current_admin: AdminUserOrDefault,
+) -> None:
+ if not image.content_type or not image.content_type.startswith("image"):
+ raise HTTPException(status_code=415, detail="Not an image")
+
+ contents = await image.read()
+ try:
+ pil_image = Image.open(io.BytesIO(contents))
+
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read image")
+
+ logger = ApiDependencies.invoker.services.logger
+ model_images = ApiDependencies.invoker.services.model_images
+ try:
+ model_images.save(pil_image, key)
+ logger.info(f"Updated image for model: {key}")
+ except ValueError as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=409, detail=str(e))
+ return
+
+
+@model_manager_router.delete(
+ "/i/{key}",
+ operation_id="delete_model",
+ responses={
+ 204: {"description": "Model deleted successfully"},
+ 404: {"description": "Model not found"},
+ },
+ status_code=204,
+)
+async def delete_model(
+ current_admin: AdminUserOrDefault,
+ key: str = Path(description="Unique key of model to remove from model registry."),
+) -> Response:
+ """
+ Delete model record from database.
+
+ The configuration record will be removed. The corresponding weights files will be
+ deleted as well if they reside within the InvokeAI "models" directory.
+ """
+ logger = ApiDependencies.invoker.services.logger
+
+ try:
+ installer = ApiDependencies.invoker.services.model_manager.install
+ installer.delete(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))
+
+
+class BulkDeleteModelsRequest(BaseModel):
+ """Request body for bulk model deletion."""
+
+ keys: List[str] = Field(description="List of model keys to delete")
+
+
+class BulkDeleteModelsResponse(BaseModel):
+ """Response body for bulk model deletion."""
+
+ deleted: List[str] = Field(description="List of successfully deleted model keys")
+ failed: List[dict] = Field(description="List of failed deletions with error messages")
+
+
+class BulkReidentifyModelsRequest(BaseModel):
+ """Request body for bulk model reidentification."""
+
+ keys: List[str] = Field(description="List of model keys to reidentify")
+
+
+class BulkReidentifyModelsResponse(BaseModel):
+ """Response body for bulk model reidentification."""
+
+ succeeded: List[str] = Field(description="List of successfully reidentified model keys")
+ failed: List[dict] = Field(description="List of failed reidentifications with error messages")
+
+
+@model_manager_router.post(
+ "/i/bulk_delete",
+ operation_id="bulk_delete_models",
+ responses={
+ 200: {"description": "Models deleted (possibly with some failures)"},
+ },
+ status_code=200,
+)
+async def bulk_delete_models(
+ current_admin: AdminUserOrDefault,
+ request: BulkDeleteModelsRequest = Body(description="List of model keys to delete"),
+) -> BulkDeleteModelsResponse:
+ """
+ Delete multiple model records from database.
+
+ The configuration records will be removed. The corresponding weights files will be
+ deleted as well if they reside within the InvokeAI "models" directory.
+ Returns a list of successfully deleted keys and failed deletions with error messages.
+ """
+ logger = ApiDependencies.invoker.services.logger
+ installer = ApiDependencies.invoker.services.model_manager.install
+
+ deleted = []
+ failed = []
+
+ for key in request.keys:
+ try:
+ installer.delete(key)
+ deleted.append(key)
+ logger.info(f"Deleted model: {key}")
+ except UnknownModelException as e:
+ logger.error(f"Failed to delete model {key}: {str(e)}")
+ failed.append({"key": key, "error": str(e)})
+ except Exception as e:
+ logger.error(f"Failed to delete model {key}: {str(e)}")
+ failed.append({"key": key, "error": str(e)})
+
+ logger.info(f"Bulk delete completed: {len(deleted)} deleted, {len(failed)} failed")
+ return BulkDeleteModelsResponse(deleted=deleted, failed=failed)
+
+
+@model_manager_router.post(
+ "/i/bulk_reidentify",
+ operation_id="bulk_reidentify_models",
+ responses={
+ 200: {"description": "Models reidentified (possibly with some failures)"},
+ },
+ status_code=200,
+)
+async def bulk_reidentify_models(
+ current_admin: AdminUserOrDefault,
+ request: BulkReidentifyModelsRequest = Body(description="List of model keys to reidentify"),
+) -> BulkReidentifyModelsResponse:
+ """
+ Reidentify multiple models by re-probing their weights files.
+
+ Returns a list of successfully reidentified keys and failed reidentifications with error messages.
+ """
+ logger = ApiDependencies.invoker.services.logger
+ store = ApiDependencies.invoker.services.model_manager.store
+ models_path = ApiDependencies.invoker.services.configuration.models_path
+
+ succeeded = []
+ failed = []
+
+ for key in request.keys:
+ try:
+ config = store.get_model(key)
+ if pathlib.Path(config.path).is_relative_to(models_path):
+ model_path = pathlib.Path(config.path)
+ else:
+ model_path = models_path / config.path
+ mod = ModelOnDisk(model_path)
+ result = ModelConfigFactory.from_model_on_disk(mod)
+ if result.config is None:
+ raise InvalidModelException("Unable to identify model format")
+
+ # Retain user-editable fields from the original config
+ result.config.path = config.path
+ result.config.key = config.key
+ result.config.name = config.name
+ result.config.description = config.description
+ result.config.cover_image = config.cover_image
+ if hasattr(config, "trigger_phrases") and hasattr(result.config, "trigger_phrases"):
+ result.config.trigger_phrases = config.trigger_phrases
+ result.config.source = config.source
+ result.config.source_type = config.source_type
+
+ store.replace_model(config.key, result.config)
+ succeeded.append(key)
+ logger.info(f"Reidentified model: {key}")
+ except UnknownModelException as e:
+ logger.error(f"Failed to reidentify model {key}: {str(e)}")
+ failed.append({"key": key, "error": str(e)})
+ except Exception as e:
+ logger.error(f"Failed to reidentify model {key}: {str(e)}")
+ failed.append({"key": key, "error": str(e)})
+
+ logger.info(f"Bulk reidentify completed: {len(succeeded)} succeeded, {len(failed)} failed")
+ return BulkReidentifyModelsResponse(succeeded=succeeded, failed=failed)
+
+
+@model_manager_router.delete(
+ "/i/{key}/image",
+ operation_id="delete_model_image",
+ responses={
+ 204: {"description": "Model image deleted successfully"},
+ 404: {"description": "Model image not found"},
+ },
+ status_code=204,
+)
+async def delete_model_image(
+ current_admin: AdminUserOrDefault,
+ key: str = Path(description="Unique key of model image to remove from model_images directory."),
+) -> None:
+ logger = ApiDependencies.invoker.services.logger
+ model_images = ApiDependencies.invoker.services.model_images
+ try:
+ model_images.delete(key)
+ logger.info(f"Deleted model image: {key}")
+ return
+ except UnknownModelException as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@model_manager_router.post(
+ "/install",
+ operation_id="install_model",
+ responses={
+ 201: {"description": "The model imported successfully"},
+ 415: {"description": "Unrecognized file/folder format"},
+ 424: {"description": "The model appeared to import successfully, but could not be found in the model manager"},
+ 409: {"description": "There is already a model corresponding to this path or repo_id"},
+ },
+ status_code=201,
+)
+async def install_model(
+ current_admin: AdminUserOrDefault,
+ source: str = Query(description="Model source to install, can be a local path, repo_id, or remote URL"),
+ inplace: Optional[bool] = Query(description="Whether or not to install a local model in place", default=False),
+ access_token: Optional[str] = Query(description="access token for the remote resource", default=None),
+ config: ModelRecordChanges = Body(
+ description="Object containing fields that override auto-probed values in the model config record, such as name, description and prediction_type ",
+ examples=[{"name": "string", "description": "string"}],
+ ),
+) -> ModelInstallJob:
+ """Install a model using a string identifier.
+
+ `source` can be any of the following.
+
+ 1. A path on the local filesystem ('C:\\users\\fred\\model.safetensors')
+ 2. A Url pointing to a single downloadable model file
+ 3. A HuggingFace repo_id with any of the following formats:
+ - model/name
+ - model/name:fp16:vae
+ - model/name::vae -- use default precision
+ - model/name:fp16:path/to/model.safetensors
+ - model/name::path/to/model.safetensors
+
+ `config` is a ModelRecordChanges object. Fields in this object will override
+ the ones that are probed automatically. Pass an empty object to accept
+ all the defaults.
+
+ `access_token` is an optional access token for use with Urls that require
+ authentication.
+
+ Models will be downloaded, probed, configured and installed in a
+ series of background threads. The return object has `status` attribute
+ that can be used to monitor progress.
+
+ See the documentation for `import_model_record` for more information on
+ interpreting the job information returned by this route.
+ """
+ logger = ApiDependencies.invoker.services.logger
+
+ try:
+ installer = ApiDependencies.invoker.services.model_manager.install
+ result: ModelInstallJob = installer.heuristic_import(
+ source=source,
+ config=config,
+ access_token=access_token,
+ inplace=bool(inplace),
+ )
+ logger.info(f"Started installation of {source}")
+ except UnknownModelException as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=424, detail=str(e))
+ except InvalidModelException as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=415)
+ except ValueError as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=409, detail=str(e))
+ return result
+
+
+@model_manager_router.get(
+ "/install/huggingface",
+ operation_id="install_hugging_face_model",
+ responses={
+ 201: {"description": "The model is being installed"},
+ 400: {"description": "Bad request"},
+ 409: {"description": "There is already a model corresponding to this path or repo_id"},
+ },
+ status_code=201,
+ response_class=HTMLResponse,
+)
+async def install_hugging_face_model(
+ current_admin: AdminUserOrDefault,
+ source: str = Query(description="HuggingFace repo_id to install"),
+) -> HTMLResponse:
+ """Install a Hugging Face model using a string identifier."""
+
+ def generate_html(title: str, heading: str, repo_id: str, is_error: bool, message: str | None = "") -> str:
+ if message:
+ message = f"{message}
"
+ title_class = "error" if is_error else "success"
+ return f"""
+
+
+
+ {title}
+
+
+
+
+
+
+
{heading}
+ {message}
+
Repo ID: {repo_id}
+
+
+
+
+
+ """
+
+ try:
+ metadata = HuggingFaceMetadataFetch().from_id(source)
+ assert isinstance(metadata, ModelMetadataWithFiles)
+ except UnknownMetadataException:
+ title = "Unable to Install Model"
+ heading = "No HuggingFace repository found with that repo ID."
+ message = "Ensure the repo ID is correct and try again."
+ return HTMLResponse(content=generate_html(title, heading, source, True, message), status_code=400)
+
+ logger = ApiDependencies.invoker.services.logger
+
+ try:
+ installer = ApiDependencies.invoker.services.model_manager.install
+ if metadata.is_diffusers:
+ installer.heuristic_import(
+ source=source,
+ inplace=False,
+ )
+ elif metadata.ckpt_urls is not None and len(metadata.ckpt_urls) == 1:
+ installer.heuristic_import(
+ source=str(metadata.ckpt_urls[0]),
+ inplace=False,
+ )
+ else:
+ title = "Unable to Install Model"
+ heading = "This HuggingFace repo has multiple models."
+ message = "Please use the Model Manager to install this model."
+ return HTMLResponse(content=generate_html(title, heading, source, True, message), status_code=200)
+
+ title = "Model Install Started"
+ heading = "Your HuggingFace model is installing now."
+ message = "You can close this tab and check the Model Manager for installation progress."
+ return HTMLResponse(content=generate_html(title, heading, source, False, message), status_code=201)
+ except Exception as e:
+ logger.error(str(e))
+ title = "Unable to Install Model"
+ heading = "There was an problem installing this model."
+ message = 'Please use the Model Manager directly to install this model. If the issue persists, ask for help on discord .'
+ return HTMLResponse(content=generate_html(title, heading, source, True, message), status_code=500)
+
+
+@model_manager_router.get(
+ "/install",
+ operation_id="list_model_installs",
+)
+async def list_model_installs(current_admin: AdminUserOrDefault) -> List[ModelInstallJob]:
+ """Return the list of model install jobs.
+
+ Install jobs have a numeric `id`, a `status`, and other fields that provide information on
+ the nature of the job and its progress. The `status` is one of:
+
+ * "waiting" -- Job is waiting in the queue to run
+ * "downloading" -- Model file(s) are downloading
+ * "running" -- Model has downloaded and the model probing and registration process is running
+ * "paused" -- Job is paused and can be resumed
+ * "completed" -- Installation completed successfully
+ * "error" -- An error occurred. Details will be in the "error_type" and "error" fields.
+ * "cancelled" -- Job was cancelled before completion.
+
+ Once completed, information about the model such as its size, base
+ model and type can be retrieved from the `config_out` field. For multi-file models such as diffusers,
+ information on individual files can be retrieved from `download_parts`.
+
+ See the example and schema below for more information.
+ """
+ jobs: List[ModelInstallJob] = ApiDependencies.invoker.services.model_manager.install.list_jobs()
+ return jobs
+
+
+@model_manager_router.get(
+ "/install/{id}",
+ operation_id="get_model_install_job",
+ responses={
+ 200: {"description": "Success"},
+ 404: {"description": "No such job"},
+ },
+)
+async def get_model_install_job(
+ current_admin: AdminUserOrDefault, id: int = Path(description="Model install id")
+) -> ModelInstallJob:
+ """
+ Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs'
+ for information on the format of the return value.
+ """
+ try:
+ result: ModelInstallJob = ApiDependencies.invoker.services.model_manager.install.get_job_by_id(id)
+ return result
+ except ValueError as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@model_manager_router.delete(
+ "/install/{id}",
+ operation_id="cancel_model_install_job",
+ responses={
+ 201: {"description": "The job was cancelled successfully"},
+ 415: {"description": "No such job"},
+ },
+ status_code=201,
+)
+async def cancel_model_install_job(
+ current_admin: AdminUserOrDefault,
+ id: int = Path(description="Model install job ID"),
+) -> None:
+ """Cancel the model install job(s) corresponding to the given job ID."""
+ installer = ApiDependencies.invoker.services.model_manager.install
+ try:
+ job = installer.get_job_by_id(id)
+ except ValueError as e:
+ raise HTTPException(status_code=415, detail=str(e))
+ installer.cancel_job(job)
+
+
+@model_manager_router.post(
+ "/install/{id}/pause",
+ operation_id="pause_model_install_job",
+ responses={
+ 201: {"description": "The job was paused successfully"},
+ 415: {"description": "No such job"},
+ },
+ status_code=201,
+)
+async def pause_model_install_job(
+ current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
+) -> ModelInstallJob:
+ """Pause the model install job corresponding to the given job ID."""
+ installer = ApiDependencies.invoker.services.model_manager.install
+ try:
+ job = installer.get_job_by_id(id)
+ except ValueError as e:
+ raise HTTPException(status_code=415, detail=str(e))
+ installer.pause_job(job)
+ return job
+
+
+@model_manager_router.post(
+ "/install/{id}/resume",
+ operation_id="resume_model_install_job",
+ responses={
+ 201: {"description": "The job was resumed successfully"},
+ 415: {"description": "No such job"},
+ },
+ status_code=201,
+)
+async def resume_model_install_job(
+ current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
+) -> ModelInstallJob:
+ """Resume a paused model install job corresponding to the given job ID."""
+ installer = ApiDependencies.invoker.services.model_manager.install
+ try:
+ job = installer.get_job_by_id(id)
+ except ValueError as e:
+ raise HTTPException(status_code=415, detail=str(e))
+ installer.resume_job(job)
+ return job
+
+
+@model_manager_router.post(
+ "/install/{id}/restart_failed",
+ operation_id="restart_failed_model_install_job",
+ responses={
+ 201: {"description": "Failed files restarted successfully"},
+ 415: {"description": "No such job"},
+ },
+ status_code=201,
+)
+async def restart_failed_model_install_job(
+ current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
+) -> ModelInstallJob:
+ """Restart failed or non-resumable file downloads for the given job."""
+ installer = ApiDependencies.invoker.services.model_manager.install
+ try:
+ job = installer.get_job_by_id(id)
+ except ValueError as e:
+ raise HTTPException(status_code=415, detail=str(e))
+ installer.restart_failed(job)
+ return job
+
+
+@model_manager_router.post(
+ "/install/{id}/restart_file",
+ operation_id="restart_model_install_file",
+ responses={
+ 201: {"description": "File restarted successfully"},
+ 415: {"description": "No such job"},
+ },
+ status_code=201,
+)
+async def restart_model_install_file(
+ current_admin: AdminUserOrDefault,
+ id: int = Path(description="Model install job ID"),
+ file_source: AnyHttpUrl = Body(description="File download URL to restart"),
+) -> ModelInstallJob:
+ """Restart a specific file download for the given job."""
+ installer = ApiDependencies.invoker.services.model_manager.install
+ try:
+ job = installer.get_job_by_id(id)
+ except ValueError as e:
+ raise HTTPException(status_code=415, detail=str(e))
+ installer.restart_file(job, str(file_source))
+ return job
+
+
+@model_manager_router.delete(
+ "/install",
+ operation_id="prune_model_install_jobs",
+ responses={
+ 204: {"description": "All completed and errored jobs have been pruned"},
+ 400: {"description": "Bad request"},
+ },
+)
+async def prune_model_install_jobs(current_admin: AdminUserOrDefault) -> Response:
+ """Prune all completed and errored jobs from the install job list."""
+ ApiDependencies.invoker.services.model_manager.install.prune_jobs()
+ return Response(status_code=204)
+
+
+@model_manager_router.put(
+ "/convert/{key}",
+ operation_id="convert_model",
+ responses={
+ 200: {
+ "description": "Model converted successfully",
+ "content": {"application/json": {"example": example_model_config}},
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "Model not found"},
+ 409: {"description": "There is already a model registered at this location"},
+ },
+)
+async def convert_model(
+ current_admin: AdminUserOrDefault,
+ key: str = Path(description="Unique key of the safetensors main model to convert to diffusers format."),
+) -> AnyModelConfig:
+ """
+ Permanently convert a model into diffusers format, replacing the safetensors version.
+ Note that during the conversion process the key and model hash will change.
+ The return value is the model configuration for the converted model.
+ """
+ model_manager = ApiDependencies.invoker.services.model_manager
+ loader = model_manager.load
+ logger = ApiDependencies.invoker.services.logger
+ store = ApiDependencies.invoker.services.model_manager.store
+ installer = ApiDependencies.invoker.services.model_manager.install
+
+ try:
+ model_config = store.get_model(key)
+ except UnknownModelException as e:
+ logger.error(str(e))
+ raise HTTPException(status_code=424, detail=str(e))
+
+ if not isinstance(
+ model_config,
+ (
+ Main_Checkpoint_SD1_Config,
+ Main_Checkpoint_SD2_Config,
+ Main_Checkpoint_SDXL_Config,
+ Main_Checkpoint_SDXLRefiner_Config,
+ ),
+ ):
+ msg = f"The model with key {key} is not a main SD 1/2/XL checkpoint model."
+ logger.error(msg)
+ raise HTTPException(400, msg)
+
+ with TemporaryDirectory(dir=ApiDependencies.invoker.services.configuration.models_path) as tmpdir:
+ convert_path = pathlib.Path(tmpdir) / pathlib.Path(model_config.path).stem
+ converted_model = loader.load_model(model_config)
+ # write the converted file to the convert path
+ raw_model = converted_model.model
+ assert hasattr(raw_model, "save_pretrained")
+ raw_model.save_pretrained(convert_path) # type: ignore
+ assert convert_path.exists()
+
+ # temporarily rename the original safetensors file so that there is no naming conflict
+ original_name = model_config.name
+ model_config.name = f"{original_name}.DELETE"
+ changes = ModelRecordChanges(name=model_config.name)
+ store.update_model(key, changes=changes)
+
+ # install the diffusers
+ try:
+ new_key = installer.install_path(
+ convert_path,
+ config=ModelRecordChanges(
+ name=original_name,
+ description=model_config.description,
+ hash=model_config.hash,
+ source=model_config.source,
+ ),
+ )
+ except Exception as e:
+ logger.error(str(e))
+ store.update_model(key, changes=ModelRecordChanges(name=original_name))
+ raise HTTPException(status_code=409, detail=str(e))
+
+ # Update the model image if the model had one
+ try:
+ model_image = ApiDependencies.invoker.services.model_images.get(key)
+ ApiDependencies.invoker.services.model_images.save(model_image, new_key)
+ ApiDependencies.invoker.services.model_images.delete(key)
+ except ModelImageFileNotFoundException:
+ pass
+
+ # delete the original safetensors file
+ installer.delete(key)
+
+ # delete the temporary directory
+ # shutil.rmtree(cache_path)
+
+ # return the config record for the new diffusers directory
+ new_config = store.get_model(new_key)
+ new_config = prepare_model_config_for_response(new_config, ApiDependencies)
+ return new_config
+
+
+class StarterModelResponse(BaseModel):
+ starter_models: list[StarterModel]
+ starter_bundles: dict[str, StarterModelBundle]
+
+
+def get_is_installed(
+ starter_model: StarterModel | StarterModelWithoutDependencies, installed_models: list[AnyModelConfig]
+) -> bool:
+ from invokeai.backend.model_manager.taxonomy import ModelType
+
+ for model in installed_models:
+ # Check if source matches exactly
+ if model.source == starter_model.source:
+ return True
+ # Check if name (or previous names), base and type match
+ if (
+ (model.name == starter_model.name or model.name in starter_model.previous_names)
+ and model.base == starter_model.base
+ and model.type == starter_model.type
+ ):
+ return True
+
+ # Special handling for Qwen3Encoder models - check by type and variant
+ # This allows renamed models to still be detected as installed
+ if starter_model.type == ModelType.Qwen3Encoder:
+ from invokeai.backend.model_manager.taxonomy import Qwen3VariantType
+
+ # Determine expected variant from source pattern
+ expected_variant: Qwen3VariantType | None = None
+ if "klein-9B" in starter_model.source or "qwen3_8b" in starter_model.source.lower():
+ expected_variant = Qwen3VariantType.Qwen3_8B
+ elif (
+ "klein-4B" in starter_model.source
+ or "qwen3_4b" in starter_model.source.lower()
+ or "Z-Image" in starter_model.source
+ ):
+ expected_variant = Qwen3VariantType.Qwen3_4B
+
+ if expected_variant is not None:
+ for model in installed_models:
+ if model.type == ModelType.Qwen3Encoder and hasattr(model, "variant"):
+ model_variant = model.variant
+ # Handle both enum and string values
+ if isinstance(model_variant, Qwen3VariantType):
+ if model_variant == expected_variant:
+ return True
+ elif isinstance(model_variant, str):
+ if model_variant == expected_variant.value:
+ return True
+
+ return False
+
+
+@model_manager_router.get("/starter_models", operation_id="get_starter_models", response_model=StarterModelResponse)
+async def get_starter_models() -> StarterModelResponse:
+ installed_models = ApiDependencies.invoker.services.model_manager.store.search_by_attr()
+ starter_models = deepcopy(STARTER_MODELS)
+ starter_bundles = deepcopy(STARTER_BUNDLES)
+ for model in starter_models:
+ model.is_installed = get_is_installed(model, installed_models)
+ # Remove already-installed dependencies
+ missing_deps: list[StarterModelWithoutDependencies] = []
+
+ for dep in model.dependencies or []:
+ if not get_is_installed(dep, installed_models):
+ missing_deps.append(dep)
+ model.dependencies = missing_deps
+
+ for bundle in starter_bundles.values():
+ for model in bundle.models:
+ model.is_installed = get_is_installed(model, installed_models)
+ # Remove already-installed dependencies
+ missing_deps: list[StarterModelWithoutDependencies] = []
+ for dep in model.dependencies or []:
+ if not get_is_installed(dep, installed_models):
+ missing_deps.append(dep)
+ model.dependencies = missing_deps
+
+ return StarterModelResponse(starter_models=starter_models, starter_bundles=starter_bundles)
+
+
+@model_manager_router.get(
+ "/stats",
+ operation_id="get_stats",
+ response_model=Optional[CacheStats],
+ summary="Get model manager RAM cache performance statistics.",
+)
+async def get_stats() -> Optional[CacheStats]:
+ """Return performance statistics on the model manager's RAM cache. Will return null if no models have been loaded."""
+
+ return ApiDependencies.invoker.services.model_manager.load.ram_cache.stats
+
+
+@model_manager_router.post(
+ "/empty_model_cache",
+ operation_id="empty_model_cache",
+ status_code=200,
+)
+async def empty_model_cache(current_admin: AdminUserOrDefault) -> None:
+ """Drop all models from the model cache to free RAM/VRAM. 'Locked' models that are in active use will not be dropped."""
+ # Request 1000GB of room in order to force the cache to drop all models.
+ ApiDependencies.invoker.services.logger.info("Emptying model cache.")
+ ApiDependencies.invoker.services.model_manager.load.ram_cache.make_room(1000 * 2**30)
+
+
+class HFTokenStatus(str, Enum):
+ VALID = "valid"
+ INVALID = "invalid"
+ UNKNOWN = "unknown"
+
+
+class HFTokenHelper:
+ @classmethod
+ def get_status(cls) -> HFTokenStatus:
+ try:
+ token = huggingface_hub.get_token()
+ if not token:
+ return HFTokenStatus.INVALID
+ huggingface_hub.whoami(token=token)
+ return HFTokenStatus.VALID
+ except Exception:
+ return HFTokenStatus.UNKNOWN
+
+ @classmethod
+ def set_token(cls, token: str) -> HFTokenStatus:
+ with SuppressOutput(), contextlib.suppress(Exception):
+ huggingface_hub.login(token=token, add_to_git_credential=False)
+ return cls.get_status()
+
+ @classmethod
+ def reset_token(cls) -> HFTokenStatus:
+ with SuppressOutput(), contextlib.suppress(Exception):
+ huggingface_hub.logout()
+ return cls.get_status()
+
+
+@model_manager_router.get("/hf_login", operation_id="get_hf_login_status", response_model=HFTokenStatus)
+async def get_hf_login_status() -> HFTokenStatus:
+ token_status = HFTokenHelper.get_status()
+
+ if token_status is HFTokenStatus.UNKNOWN:
+ ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
+
+ return token_status
+
+
+@model_manager_router.post("/hf_login", operation_id="do_hf_login", response_model=HFTokenStatus)
+async def do_hf_login(
+ current_admin: AdminUserOrDefault,
+ token: str = Body(description="Hugging Face token to use for login", embed=True),
+) -> HFTokenStatus:
+ HFTokenHelper.set_token(token)
+ token_status = HFTokenHelper.get_status()
+
+ if token_status is HFTokenStatus.UNKNOWN:
+ ApiDependencies.invoker.services.logger.warning("Unable to verify HF token")
+
+ return token_status
+
+
+@model_manager_router.delete("/hf_login", operation_id="reset_hf_token", response_model=HFTokenStatus)
+async def reset_hf_token(current_admin: AdminUserOrDefault) -> HFTokenStatus:
+ return HFTokenHelper.reset_token()
+
+
+# Orphaned Models Management Routes
+
+
+class DeleteOrphanedModelsRequest(BaseModel):
+ """Request to delete specific orphaned model directories."""
+
+ paths: list[str] = Field(description="List of relative paths to delete")
+
+
+class DeleteOrphanedModelsResponse(BaseModel):
+ """Response from deleting orphaned models."""
+
+ deleted: list[str] = Field(description="Paths that were successfully deleted")
+ errors: dict[str, str] = Field(description="Paths that had errors, with error messages")
+
+
+@model_manager_router.get(
+ "/sync/orphaned",
+ operation_id="get_orphaned_models",
+ response_model=list[OrphanedModelInfo],
+)
+async def get_orphaned_models(_: AdminUserOrDefault) -> list[OrphanedModelInfo]:
+ """Find orphaned model directories.
+
+ Orphaned models are directories in the models folder that contain model files
+ but are not referenced in the database. This can happen when models are deleted
+ from the database but the files remain on disk.
+
+ Returns:
+ List of orphaned model directory information
+ """
+ from invokeai.app.services.orphaned_models import OrphanedModelsService
+
+ # Access the database through the model records service
+ model_records_service = ApiDependencies.invoker.services.model_manager.store
+
+ service = OrphanedModelsService(
+ config=ApiDependencies.invoker.services.configuration,
+ db=model_records_service._db, # Access the database from model records service
+ )
+ return service.find_orphaned_models()
+
+
+@model_manager_router.delete(
+ "/sync/orphaned",
+ operation_id="delete_orphaned_models",
+ response_model=DeleteOrphanedModelsResponse,
+)
+async def delete_orphaned_models(
+ request: DeleteOrphanedModelsRequest, _: AdminUserOrDefault
+) -> DeleteOrphanedModelsResponse:
+ """Delete specified orphaned model directories.
+
+ Args:
+ request: Request containing list of relative paths to delete
+
+ Returns:
+ Response indicating which paths were deleted and which had errors
+ """
+ from invokeai.app.services.orphaned_models import OrphanedModelsService
+
+ # Access the database through the model records service
+ model_records_service = ApiDependencies.invoker.services.model_manager.store
+
+ service = OrphanedModelsService(
+ config=ApiDependencies.invoker.services.configuration,
+ db=model_records_service._db, # Access the database from model records service
+ )
+
+ results = service.delete_orphaned_models(request.paths)
+
+ # Separate successful deletions from errors
+ deleted = [path for path, status in results.items() if status == "deleted"]
+ errors = {path: status for path, status in results.items() if status != "deleted"}
+
+ return DeleteOrphanedModelsResponse(deleted=deleted, errors=errors)
diff --git a/invokeai/app/api/routers/model_relationships.py b/invokeai/app/api/routers/model_relationships.py
new file mode 100644
index 00000000000..0ec45070955
--- /dev/null
+++ b/invokeai/app/api/routers/model_relationships.py
@@ -0,0 +1,210 @@
+"""FastAPI route for model relationship records."""
+
+from typing import List
+
+from fastapi import APIRouter, Body, HTTPException, Path, status
+from pydantic import BaseModel, Field
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+
+model_relationships_router = APIRouter(prefix="/v1/model_relationships", tags=["model_relationships"])
+
+# === Schemas ===
+
+
+class ModelRelationshipCreateRequest(BaseModel):
+ model_key_1: str = Field(
+ ...,
+ description="The key of the first model in the relationship",
+ examples=[
+ "aa3b247f-90c9-4416-bfcd-aeaa57a5339e",
+ "ac32b914-10ab-496e-a24a-3068724b9c35",
+ "d944abfd-c7c3-42e2-a4ff-da640b29b8b4",
+ "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
+ "12345678-90ab-cdef-1234-567890abcdef",
+ "fedcba98-7654-3210-fedc-ba9876543210",
+ ],
+ )
+ model_key_2: str = Field(
+ ...,
+ description="The key of the second model in the relationship",
+ examples=[
+ "3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4",
+ "f0c3da4e-d9ff-42b5-a45c-23be75c887c9",
+ "38170dd8-f1e5-431e-866c-2c81f1277fcc",
+ "c57fea2d-7646-424c-b9ad-c0ba60fc68be",
+ "10f7807b-ab54-46a9-ab03-600e88c630a1",
+ "f6c1d267-cf87-4ee0-bee0-37e791eacab7",
+ ],
+ )
+
+
+class ModelRelationshipBatchRequest(BaseModel):
+ model_keys: List[str] = Field(
+ ...,
+ description="List of model keys to fetch related models for",
+ examples=[
+ [
+ "aa3b247f-90c9-4416-bfcd-aeaa57a5339e",
+ "ac32b914-10ab-496e-a24a-3068724b9c35",
+ ],
+ [
+ "b1c2d3e4-f5a6-7890-abcd-ef1234567890",
+ "12345678-90ab-cdef-1234-567890abcdef",
+ "fedcba98-7654-3210-fedc-ba9876543210",
+ ],
+ [
+ "3bb7c0eb-b6c8-469c-ad8c-4d69c06075e4",
+ ],
+ ],
+ )
+
+
+# === Routes ===
+
+
+@model_relationships_router.get(
+ "/i/{model_key}",
+ operation_id="get_related_models",
+ response_model=list[str],
+ responses={
+ 200: {
+ "description": "A list of related model keys was retrieved successfully",
+ "content": {
+ "application/json": {
+ "example": [
+ "15e9eb28-8cfe-47c9-b610-37907a79fc3c",
+ "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
+ "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2",
+ ]
+ }
+ },
+ },
+ 404: {"description": "The specified model could not be found"},
+ 422: {"description": "Validation error"},
+ },
+)
+async def get_related_models(
+ current_user: CurrentUserOrDefault,
+ model_key: str = Path(..., description="The key of the model to get relationships for"),
+) -> list[str]:
+ """
+ Get a list of model keys related to a given model.
+ """
+ return ApiDependencies.invoker.services.model_relationships.get_related_model_keys(model_key)
+
+
+@model_relationships_router.post(
+ "/",
+ status_code=status.HTTP_204_NO_CONTENT,
+ responses={
+ 204: {"description": "The relationship was successfully created"},
+ 400: {"description": "Invalid model keys or self-referential relationship"},
+ 409: {"description": "The relationship already exists"},
+ 422: {"description": "Validation error"},
+ 500: {"description": "Internal server error"},
+ },
+ summary="Add Model Relationship",
+ description="Creates a **bidirectional** relationship between two models, allowing each to reference the other as related.",
+)
+async def add_model_relationship(
+ current_user: AdminUserOrDefault,
+ req: ModelRelationshipCreateRequest = Body(..., description="The model keys to relate"),
+) -> None:
+ """
+ Add a relationship between two models.
+
+ Relationships are bidirectional and will be accessible from both models.
+
+ - Raises 400 if keys are invalid or identical.
+ - Raises 409 if the relationship already exists.
+ """
+ if req.model_key_1 == req.model_key_2:
+ raise HTTPException(status_code=400, detail="Cannot relate a model to itself.")
+
+ try:
+ ApiDependencies.invoker.services.model_relationships.add_model_relationship(
+ req.model_key_1,
+ req.model_key_2,
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=409, detail=str(e))
+
+
+@model_relationships_router.delete(
+ "/",
+ status_code=status.HTTP_204_NO_CONTENT,
+ responses={
+ 204: {"description": "The relationship was successfully removed"},
+ 400: {"description": "Invalid model keys or self-referential relationship"},
+ 404: {"description": "The relationship does not exist"},
+ 422: {"description": "Validation error"},
+ 500: {"description": "Internal server error"},
+ },
+ summary="Remove Model Relationship",
+ description="Removes a **bidirectional** relationship between two models. The relationship must already exist.",
+)
+async def remove_model_relationship(
+ current_user: AdminUserOrDefault,
+ req: ModelRelationshipCreateRequest = Body(..., description="The model keys to disconnect"),
+) -> None:
+ """
+ Removes a bidirectional relationship between two model keys.
+
+ - Raises 400 if attempting to unlink a model from itself.
+ - Raises 404 if the relationship was not found.
+ """
+ if req.model_key_1 == req.model_key_2:
+ raise HTTPException(status_code=400, detail="Cannot unlink a model from itself.")
+
+ try:
+ ApiDependencies.invoker.services.model_relationships.remove_model_relationship(
+ req.model_key_1,
+ req.model_key_2,
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=404, detail=str(e))
+
+
+@model_relationships_router.post(
+ "/batch",
+ operation_id="get_related_models_batch",
+ response_model=List[str],
+ responses={
+ 200: {
+ "description": "Related model keys retrieved successfully",
+ "content": {
+ "application/json": {
+ "example": [
+ "ca562b14-995e-4a42-90c1-9528f1a5921d",
+ "cc0c2b8a-c62e-41d6-878e-cc74dde5ca8f",
+ "18ca7649-6a9e-47d5-bc17-41ab1e8cec81",
+ "7c12d1b2-0ef9-4bec-ba55-797b2d8f2ee1",
+ "c382eaa3-0e28-4ab0-9446-408667699aeb",
+ "71272e82-0e5f-46d5-bca9-9a61f4bd8a82",
+ "a5d7cd49-1b98-4534-a475-aeee4ccf5fa2",
+ ]
+ }
+ },
+ },
+ 422: {"description": "Validation error"},
+ 500: {"description": "Internal server error"},
+ },
+ summary="Get Related Model Keys (Batch)",
+ description="Retrieves all **unique related model keys** for a list of given models. This is useful for contextual suggestions or filtering.",
+)
+async def get_related_models_batch(
+ current_user: CurrentUserOrDefault,
+ req: ModelRelationshipBatchRequest = Body(..., description="Model keys to check for related connections"),
+) -> list[str]:
+ """
+ Accepts multiple model keys and returns a flat list of all unique related keys.
+
+ Useful when working with multiple selections in the UI or cross-model comparisons.
+ """
+ all_related: set[str] = set()
+ for key in req.model_keys:
+ related = ApiDependencies.invoker.services.model_relationships.get_related_model_keys(key)
+ all_related.update(related)
+ return list(all_related)
diff --git a/invokeai/app/api/routers/recall_parameters.py b/invokeai/app/api/routers/recall_parameters.py
new file mode 100644
index 00000000000..31120d59a02
--- /dev/null
+++ b/invokeai/app/api/routers/recall_parameters.py
@@ -0,0 +1,592 @@
+"""Router for updating recallable parameters on the frontend."""
+
+import json
+from typing import Any, Literal, Optional
+
+from fastapi import Body, HTTPException, Path, Query
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, ConfigDict, Field
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.backend.image_util.controlnet_processor import process_controlnet_image
+from invokeai.backend.model_manager.taxonomy import ModelType
+
+recall_parameters_router = APIRouter(prefix="/v1/recall", tags=["recall"])
+
+
+class LoRARecallParameter(BaseModel):
+ """LoRA configuration for recall"""
+
+ model_name: str = Field(description="The name of the LoRA model")
+ weight: float = Field(default=0.75, ge=-10, le=10, description="The weight for the LoRA")
+ is_enabled: bool = Field(default=True, description="Whether the LoRA is enabled")
+
+
+class ControlNetRecallParameter(BaseModel):
+ """ControlNet configuration for recall"""
+
+ model_name: str = Field(description="The name of the ControlNet/T2I Adapter/Control LoRA model")
+ image_name: Optional[str] = Field(default=None, description="The filename of the control image in outputs/images")
+ weight: float = Field(default=1.0, ge=-1, le=2, description="The weight for the control adapter")
+ begin_step_percent: Optional[float] = Field(
+ default=None, ge=0, le=1, description="When the control adapter is first applied (% of total steps)"
+ )
+ end_step_percent: Optional[float] = Field(
+ default=None, ge=0, le=1, description="When the control adapter is last applied (% of total steps)"
+ )
+ control_mode: Optional[Literal["balanced", "more_prompt", "more_control"]] = Field(
+ default=None, description="The control mode (ControlNet only)"
+ )
+
+
+class IPAdapterRecallParameter(BaseModel):
+ """IP Adapter configuration for recall"""
+
+ model_name: str = Field(description="The name of the IP Adapter model")
+ image_name: Optional[str] = Field(default=None, description="The filename of the reference image in outputs/images")
+ weight: float = Field(default=1.0, ge=-1, le=2, description="The weight for the IP Adapter")
+ begin_step_percent: Optional[float] = Field(
+ default=None, ge=0, le=1, description="When the IP Adapter is first applied (% of total steps)"
+ )
+ end_step_percent: Optional[float] = Field(
+ default=None, ge=0, le=1, description="When the IP Adapter is last applied (% of total steps)"
+ )
+ method: Optional[Literal["full", "style", "composition"]] = Field(default=None, description="The IP Adapter method")
+ image_influence: Optional[Literal["lowest", "low", "medium", "high", "highest"]] = Field(
+ default=None, description="FLUX Redux image influence (if model is flux_redux)"
+ )
+
+
+class ReferenceImageRecallParameter(BaseModel):
+ """Global reference-image configuration for recall.
+
+ Used for reference images that feed directly into the main model rather
+ than through a separate IP-Adapter / ControlNet model — for example
+ FLUX.2 Klein, FLUX Kontext, and Qwen Image Edit. The receiving frontend
+ picks the correct config type (``flux2_reference_image`` /
+ ``qwen_image_reference_image`` / ``flux_kontext_reference_image``) based
+ on the currently-selected main model.
+ """
+
+ image_name: str = Field(description="The filename of the reference image in outputs/images")
+
+
+class RecallParameter(BaseModel):
+ """Request model for updating recallable parameters."""
+
+ model_config = ConfigDict(extra="forbid")
+
+ # Prompts
+ positive_prompt: Optional[str] = Field(None, description="Positive prompt text")
+ negative_prompt: Optional[str] = Field(None, description="Negative prompt text")
+
+ # Model configuration
+ model: Optional[str] = Field(None, description="Main model name/identifier")
+ refiner_model: Optional[str] = Field(None, description="Refiner model name/identifier")
+ vae_model: Optional[str] = Field(None, description="VAE model name/identifier")
+ scheduler: Optional[str] = Field(None, description="Scheduler name")
+
+ # Generation parameters
+ steps: Optional[int] = Field(None, ge=1, description="Number of generation steps")
+ refiner_steps: Optional[int] = Field(None, ge=0, description="Number of refiner steps")
+ cfg_scale: Optional[float] = Field(None, description="CFG scale for guidance")
+ cfg_rescale_multiplier: Optional[float] = Field(None, description="CFG rescale multiplier")
+ refiner_cfg_scale: Optional[float] = Field(None, description="Refiner CFG scale")
+ guidance: Optional[float] = Field(None, description="Guidance scale")
+
+ # Image parameters
+ width: Optional[int] = Field(None, ge=64, description="Image width in pixels")
+ height: Optional[int] = Field(None, ge=64, description="Image height in pixels")
+ seed: Optional[int] = Field(None, ge=0, description="Random seed")
+
+ # Advanced parameters
+ denoise_strength: Optional[float] = Field(None, ge=0, le=1, description="Denoising strength")
+ refiner_denoise_start: Optional[float] = Field(None, ge=0, le=1, description="Refiner denoising start")
+ clip_skip: Optional[int] = Field(None, ge=0, description="CLIP skip layers")
+ seamless_x: Optional[bool] = Field(None, description="Enable seamless X tiling")
+ seamless_y: Optional[bool] = Field(None, description="Enable seamless Y tiling")
+
+ # Refiner aesthetics
+ refiner_positive_aesthetic_score: Optional[float] = Field(None, description="Refiner positive aesthetic score")
+ refiner_negative_aesthetic_score: Optional[float] = Field(None, description="Refiner negative aesthetic score")
+
+ # LoRAs, ControlNets, and IP Adapters
+ loras: Optional[list[LoRARecallParameter]] = Field(None, description="List of LoRAs with their weights")
+ control_layers: Optional[list[ControlNetRecallParameter]] = Field(
+ None, description="List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings"
+ )
+ ip_adapters: Optional[list[IPAdapterRecallParameter]] = Field(
+ None, description="List of IP Adapters with their settings"
+ )
+ reference_images: Optional[list[ReferenceImageRecallParameter]] = Field(
+ None,
+ description=(
+ "List of model-free reference images for architectures that consume reference "
+ "images directly (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit). The frontend "
+ "picks the correct config type based on the currently-selected main model."
+ ),
+ )
+
+
+def resolve_model_name_to_key(model_name: str, model_type: ModelType = ModelType.Main) -> Optional[str]:
+ """
+ Look up a model by name and return its key.
+
+ Args:
+ model_name: The name of the model to look up
+ model_type: The type of model to search for (default: Main)
+
+ Returns:
+ The key of the first matching model, or None if not found.
+ """
+ logger = ApiDependencies.invoker.services.logger
+ try:
+ models = ApiDependencies.invoker.services.model_manager.store.search_by_attr(
+ model_name=model_name, model_type=model_type
+ )
+
+ if models:
+ logger.info(f"Resolved {model_type.value} model name '{model_name}' to key '{models[0].key}'")
+ return models[0].key
+
+ logger.warning(f"Could not find {model_type.value} model with name '{model_name}'")
+ return None
+ except Exception as e:
+ logger.error(f"Exception during {model_type.value} model lookup: {e}", exc_info=True)
+ return None
+
+
+def load_image_file(image_name: str) -> Optional[dict[str, Any]]:
+ """
+ Load an image from the outputs/images directory.
+
+ Args:
+ image_name: The filename of the image in outputs/images
+
+ Returns:
+ A dictionary with image_name, width, and height, or None if the image cannot be found
+ """
+ logger = ApiDependencies.invoker.services.logger
+ try:
+ images_service = ApiDependencies.invoker.services.images
+ # Use images service which handles subfolder resolution via DB record
+ path = images_service.get_path(image_name)
+
+ if not images_service.validate_path(path):
+ logger.warning(f"Image file not found: {image_name}")
+ return None
+
+ pil_image = images_service.get_pil_image(image_name)
+ width, height = pil_image.size
+ logger.info(f"Found image file: {image_name} ({width}x{height})")
+ return {"image_name": image_name, "width": width, "height": height}
+ except Exception as e:
+ logger.warning(f"Error loading image file {image_name}: {e}")
+ return None
+
+
+def resolve_lora_models(loras: list[LoRARecallParameter]) -> list[dict[str, Any]]:
+ """
+ Resolve LoRA model names to keys and build configuration list.
+
+ Args:
+ loras: List of LoRA recall parameters
+
+ Returns:
+ List of resolved LoRA configurations with model keys
+ """
+ logger = ApiDependencies.invoker.services.logger
+ resolved_loras = []
+
+ for lora in loras:
+ model_key = resolve_model_name_to_key(lora.model_name, ModelType.LoRA)
+ if model_key:
+ resolved_loras.append({"model_key": model_key, "weight": lora.weight, "is_enabled": lora.is_enabled})
+ else:
+ logger.warning(f"Skipping LoRA '{lora.model_name}' - model not found")
+
+ return resolved_loras
+
+
+def resolve_control_models(control_layers: list[ControlNetRecallParameter]) -> list[dict[str, Any]]:
+ """
+ Resolve control adapter model names to keys and build configuration list.
+
+ Tries to resolve as ControlNet, T2I Adapter, or Control LoRA in that order.
+
+ Args:
+ control_layers: List of control adapter recall parameters
+
+ Returns:
+ List of resolved control adapter configurations with model keys
+ """
+ logger = ApiDependencies.invoker.services.logger
+ services = ApiDependencies.invoker.services
+ resolved_controls = []
+
+ for control in control_layers:
+ model_key = None
+
+ # Try ControlNet first
+ model_key = resolve_model_name_to_key(control.model_name, ModelType.ControlNet)
+ if not model_key:
+ # Try T2I Adapter
+ model_key = resolve_model_name_to_key(control.model_name, ModelType.T2IAdapter)
+ if not model_key:
+ # Try Control LoRA (also uses LoRA type)
+ model_key = resolve_model_name_to_key(control.model_name, ModelType.LoRA)
+
+ if model_key:
+ config: dict[str, Any] = {"model_key": model_key, "weight": control.weight}
+ if control.image_name is not None:
+ image_data = load_image_file(control.image_name)
+ if image_data:
+ config["image"] = image_data
+
+ # Try to process the image using the model's default processor
+ processed_image_data = process_controlnet_image(control.image_name, model_key, services)
+ if processed_image_data:
+ config["processed_image"] = processed_image_data
+ logger.info(f"Added processed image for control adapter {control.model_name}")
+ else:
+ logger.warning(f"Could not load image for control adapter: {control.image_name}")
+ if control.begin_step_percent is not None:
+ config["begin_step_percent"] = control.begin_step_percent
+ if control.end_step_percent is not None:
+ config["end_step_percent"] = control.end_step_percent
+ if control.control_mode is not None:
+ config["control_mode"] = control.control_mode
+
+ resolved_controls.append(config)
+ else:
+ logger.warning(f"Skipping control adapter '{control.model_name}' - model not found")
+
+ return resolved_controls
+
+
+def resolve_ip_adapter_models(ip_adapters: list[IPAdapterRecallParameter]) -> list[dict[str, Any]]:
+ """
+ Resolve IP Adapter model names to keys and build configuration list.
+
+ Args:
+ ip_adapters: List of IP Adapter recall parameters
+
+ Returns:
+ List of resolved IP Adapter configurations with model keys
+ """
+ logger = ApiDependencies.invoker.services.logger
+ resolved_adapters = []
+
+ for adapter in ip_adapters:
+ # Try resolving as IP Adapter; if not found, try FLUX Redux
+ model_key = resolve_model_name_to_key(adapter.model_name, ModelType.IPAdapter)
+ if not model_key:
+ model_key = resolve_model_name_to_key(adapter.model_name, ModelType.FluxRedux)
+ if model_key:
+ config: dict[str, Any] = {
+ "model_key": model_key,
+ # Always include weight; ignored by FLUX Redux on the frontend
+ "weight": adapter.weight,
+ }
+ if adapter.image_name is not None:
+ image_data = load_image_file(adapter.image_name)
+ if image_data:
+ config["image"] = image_data
+ else:
+ logger.warning(f"Could not load image for IP Adapter: {adapter.image_name}")
+ if adapter.begin_step_percent is not None:
+ config["begin_step_percent"] = adapter.begin_step_percent
+ if adapter.end_step_percent is not None:
+ config["end_step_percent"] = adapter.end_step_percent
+ if adapter.method is not None:
+ config["method"] = adapter.method
+ # Include FLUX Redux image influence when provided
+ if adapter.image_influence is not None:
+ config["image_influence"] = adapter.image_influence
+
+ resolved_adapters.append(config)
+ else:
+ logger.warning(f"Skipping IP Adapter '{adapter.model_name}' - model not found")
+
+ return resolved_adapters
+
+
+def resolve_reference_images(
+ reference_images: list[ReferenceImageRecallParameter],
+) -> list[dict[str, Any]]:
+ """
+ Validate model-free reference images and build the configuration list.
+
+ Unlike IP Adapters and ControlNets, these reference images are consumed
+ directly by the main model (FLUX.2 Klein, FLUX Kontext, Qwen Image Edit),
+ so there is no adapter-model name to resolve. We simply verify that each
+ referenced file exists in ``outputs/images`` and pass the image metadata
+ through to the frontend.
+
+ Args:
+ reference_images: List of reference-image recall parameters
+
+ Returns:
+ List of reference-image configurations with resolved image metadata.
+ Entries whose image file cannot be loaded are dropped with a warning.
+ """
+ logger = ApiDependencies.invoker.services.logger
+ resolved: list[dict[str, Any]] = []
+
+ for ref in reference_images:
+ image_data = load_image_file(ref.image_name)
+ if image_data is None:
+ logger.warning(f"Skipping reference image '{ref.image_name}' - file not found")
+ continue
+ resolved.append({"image": image_data})
+
+ return resolved
+
+
+def _assert_recall_image_access(parameters: "RecallParameter", current_user: CurrentUserOrDefault) -> None:
+ """Validate that the caller can read every image referenced in the recall parameters.
+
+ Control layers, IP adapters, and reference images may reference image_name fields.
+ Without this check an attacker who knows another user's image UUID could use the recall
+ endpoint to extract image dimensions and — for ControlNet preprocessors — mint
+ a derived processed image they can then fetch.
+ """
+ from invokeai.app.services.board_records.board_records_common import BoardVisibility
+
+ image_names: list[str] = []
+ if parameters.control_layers:
+ for layer in parameters.control_layers:
+ if layer.image_name is not None:
+ image_names.append(layer.image_name)
+ if parameters.ip_adapters:
+ for adapter in parameters.ip_adapters:
+ if adapter.image_name is not None:
+ image_names.append(adapter.image_name)
+ if parameters.reference_images:
+ for ref in parameters.reference_images:
+ if ref.image_name is not None:
+ image_names.append(ref.image_name)
+
+ if not image_names:
+ return
+
+ # Admin can access all images
+ if current_user.is_admin:
+ return
+
+ for image_name in image_names:
+ owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
+ if owner is not None and owner == current_user.user_id:
+ continue
+
+ # Check board visibility
+ board_id = ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name)
+ if board_id is not None:
+ try:
+ board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
+ if board.board_visibility in (BoardVisibility.Shared, BoardVisibility.Public):
+ continue
+ except Exception:
+ pass
+
+ raise HTTPException(status_code=403, detail=f"Not authorized to access image {image_name}")
+
+
+@recall_parameters_router.post(
+ "/{queue_id}",
+ operation_id="update_recall_parameters",
+ response_model=dict[str, Any],
+)
+async def update_recall_parameters(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(..., description="The queue id to perform this operation on"),
+ parameters: RecallParameter = Body(..., description="Recall parameters to update"),
+ strict: bool = Query(
+ default=False,
+ description="When true, parameters not included in the request are reset to their defaults (cleared).",
+ ),
+) -> dict[str, Any]:
+ """
+ Update recallable parameters that can be recalled on the frontend.
+
+ This endpoint allows updating parameters such as prompt, model, steps, and other
+ generation settings. These parameters are stored in client state and can be
+ accessed by the frontend to populate UI elements.
+
+ Args:
+ queue_id: The queue ID to associate these parameters with
+ parameters: The RecallParameter object containing the parameters to update
+ strict: When true, parameters not included in the request body are reset
+ to their defaults (cleared on the frontend). Defaults to false,
+ which preserves the existing behaviour of only updating the
+ parameters that are explicitly provided.
+
+ Returns:
+ A dictionary containing the updated parameters and status
+
+ Example:
+ POST /api/v1/recall/{queue_id}?strict=true
+ {
+ "positive_prompt": "a beautiful landscape",
+ "model": "sd-1.5",
+ "steps": 20
+ }
+ # In strict mode, all other parameters (reference_images, loras, etc.)
+ # are cleared. In non-strict mode (default) they would be left as-is.
+ """
+ logger = ApiDependencies.invoker.services.logger
+
+ # Validate image access before processing — prevents information leakage
+ # (dimensions) and derived-image minting via ControlNet preprocessors.
+ _assert_recall_image_access(parameters, current_user)
+
+ try:
+ # In strict mode, include all parameters so the frontend clears anything
+ # not explicitly provided. List-typed fields use [] instead of None so
+ # the frontend sees an empty collection rather than a null it might skip.
+ if strict:
+ _list_fields = {
+ name for name, field in RecallParameter.model_fields.items() if "list" in str(field.annotation).lower()
+ }
+ provided_params = {
+ k: ([] if v is None and k in _list_fields else v) for k, v in parameters.model_dump().items()
+ }
+ else:
+ provided_params = {k: v for k, v in parameters.model_dump().items() if v is not None}
+
+ if not provided_params:
+ return {"status": "no_parameters_provided", "updated_count": 0}
+
+ # Store each parameter in client state scoped to the current user
+ updated_count = 0
+ for param_key, param_value in provided_params.items():
+ # Convert parameter values to JSON strings for storage
+ value_str = json.dumps(param_value)
+ try:
+ ApiDependencies.invoker.services.client_state_persistence.set_by_key(
+ current_user.user_id, f"recall_{param_key}", value_str
+ )
+ updated_count += 1
+ except Exception as e:
+ logger.error(f"Error setting recall parameter {param_key}: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail=f"Error setting recall parameter {param_key}",
+ )
+
+ logger.info(f"Updated {updated_count} recall parameters for queue {queue_id}")
+
+ # Resolve model name to key if a model was provided
+ if "model" in provided_params and isinstance(provided_params["model"], str):
+ model_name = provided_params["model"]
+ model_key = resolve_model_name_to_key(model_name, ModelType.Main)
+
+ if model_key:
+ logger.info(f"Resolved model name '{model_name}' to key '{model_key}'")
+ provided_params["model"] = model_key
+ else:
+ logger.warning(f"Could not resolve model name '{model_name}' to a model key")
+ # Remove model from parameters if we couldn't resolve it
+ del provided_params["model"]
+
+ # Process LoRAs if provided
+ if "loras" in provided_params:
+ loras_param = parameters.loras
+ if loras_param is not None:
+ resolved_loras = resolve_lora_models(loras_param)
+ provided_params["loras"] = resolved_loras
+ logger.info(f"Resolved {len(resolved_loras)} LoRA(s)")
+
+ # Process control layers if provided
+ if "control_layers" in provided_params:
+ control_layers_param = parameters.control_layers
+ if control_layers_param is not None:
+ resolved_controls = resolve_control_models(control_layers_param)
+ provided_params["control_layers"] = resolved_controls
+ logger.info(f"Resolved {len(resolved_controls)} control layer(s)")
+
+ # Process IP adapters if provided
+ if "ip_adapters" in provided_params:
+ ip_adapters_param = parameters.ip_adapters
+ if ip_adapters_param is not None:
+ resolved_adapters = resolve_ip_adapter_models(ip_adapters_param)
+ provided_params["ip_adapters"] = resolved_adapters
+ logger.info(f"Resolved {len(resolved_adapters)} IP adapter(s)")
+
+ # Process model-free reference images if provided
+ if "reference_images" in provided_params:
+ reference_images_param = parameters.reference_images
+ if reference_images_param is not None:
+ resolved_refs = resolve_reference_images(reference_images_param)
+ provided_params["reference_images"] = resolved_refs
+ logger.info(f"Resolved {len(resolved_refs)} reference image(s)")
+
+ # Emit event to notify frontend of parameter updates
+ try:
+ logger.info(
+ f"Emitting recall_parameters_updated event for queue {queue_id} with {len(provided_params)} parameters"
+ )
+ ApiDependencies.invoker.services.events.emit_recall_parameters_updated(
+ queue_id, current_user.user_id, provided_params
+ )
+ logger.info("Successfully emitted recall_parameters_updated event")
+ except Exception as e:
+ logger.error(f"Error emitting recall parameters event: {e}", exc_info=True)
+ # Don't fail the request if event emission fails, just log it
+
+ return {
+ "status": "success",
+ "queue_id": queue_id,
+ "updated_count": updated_count,
+ "parameters": provided_params,
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error updating recall parameters: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail="Error updating recall parameters",
+ )
+
+
+@recall_parameters_router.get(
+ "/{queue_id}",
+ operation_id="get_recall_parameters",
+ response_model=dict[str, Any],
+)
+async def get_recall_parameters(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(..., description="The queue id to retrieve parameters for"),
+) -> dict[str, Any]:
+ """
+ Retrieve all stored recall parameters for a given queue.
+
+ Returns a dictionary of all recall parameters that have been set for the queue.
+
+ Args:
+ queue_id: The queue ID to retrieve parameters for
+
+ Returns:
+ A dictionary containing all stored recall parameters
+ """
+ logger = ApiDependencies.invoker.services.logger
+
+ try:
+ # Retrieve all recall parameters by iterating through expected keys
+ # Since client_state_persistence doesn't have a "get_all" method, we'll
+ # return an informative response
+ return {
+ "status": "success",
+ "queue_id": queue_id,
+ "note": "Use the frontend to access stored recall parameters, or set specific parameters using POST",
+ }
+
+ except Exception as e:
+ logger.error(f"Error retrieving recall parameters: {e}")
+ raise HTTPException(
+ status_code=500,
+ detail="Error retrieving recall parameters",
+ )
diff --git a/invokeai/app/api/routers/session_queue.py b/invokeai/app/api/routers/session_queue.py
new file mode 100644
index 00000000000..41a5a411c7a
--- /dev/null
+++ b/invokeai/app/api/routers/session_queue.py
@@ -0,0 +1,592 @@
+from typing import Optional
+
+from fastapi import Body, HTTPException, Path, Query
+from fastapi.routing import APIRouter
+from pydantic import BaseModel
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
+from invokeai.app.services.session_queue.session_queue_common import (
+ Batch,
+ BatchStatus,
+ CancelAllExceptCurrentResult,
+ CancelByBatchIDsResult,
+ CancelByDestinationResult,
+ ClearResult,
+ DeleteAllExceptCurrentResult,
+ DeleteByDestinationResult,
+ EnqueueBatchResult,
+ ItemIdsResult,
+ PruneResult,
+ RetryItemsResult,
+ SessionQueueCountsByDestination,
+ SessionQueueItem,
+ SessionQueueItemNotFoundError,
+ SessionQueueStatus,
+)
+from invokeai.app.services.shared.graph import Graph, GraphExecutionState
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+session_queue_router = APIRouter(prefix="/v1/queue", tags=["queue"])
+
+
+class SessionQueueAndProcessorStatus(BaseModel):
+ """The overall status of session queue and processor"""
+
+ queue: SessionQueueStatus
+ processor: SessionProcessorStatus
+
+
+def sanitize_queue_item_for_user(
+ queue_item: SessionQueueItem, current_user_id: str, is_admin: bool
+) -> SessionQueueItem:
+ """Sanitize queue item for non-admin users viewing other users' items.
+
+ For non-admin users viewing queue items belonging to other users,
+ only timestamps, status, and error information are exposed. All other
+ fields (user identity, generation parameters, graphs, workflows) are stripped.
+
+ Args:
+ queue_item: The queue item to sanitize
+ current_user_id: The ID of the current user viewing the item
+ is_admin: Whether the current user is an admin
+
+ Returns:
+ The sanitized queue item (sensitive fields cleared if necessary)
+ """
+ # Admins and item owners can see everything
+ if is_admin or queue_item.user_id == current_user_id:
+ return queue_item
+
+ # For non-admins viewing other users' items, strip everything except
+ # item_id, queue_id, status, and timestamps
+ sanitized_item = queue_item.model_copy(deep=False)
+ sanitized_item.user_id = "redacted"
+ sanitized_item.user_display_name = None
+ sanitized_item.user_email = None
+ sanitized_item.batch_id = "redacted"
+ sanitized_item.session_id = "redacted"
+ sanitized_item.origin = None
+ sanitized_item.destination = None
+ sanitized_item.priority = 0
+ sanitized_item.field_values = None
+ sanitized_item.retried_from_item_id = None
+ sanitized_item.workflow = None
+ sanitized_item.error_type = None
+ sanitized_item.error_message = None
+ sanitized_item.error_traceback = None
+ sanitized_item.session = GraphExecutionState(
+ id="redacted",
+ graph=Graph(),
+ )
+ return sanitized_item
+
+
+@session_queue_router.post(
+ "/{queue_id}/enqueue_batch",
+ operation_id="enqueue_batch",
+ responses={
+ 201: {"model": EnqueueBatchResult},
+ },
+)
+async def enqueue_batch(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ batch: Batch = Body(description="Batch to process"),
+ prepend: bool = Body(default=False, description="Whether or not to prepend this batch in the queue"),
+) -> EnqueueBatchResult:
+ """Processes a batch and enqueues the output graphs for execution for the current user."""
+ try:
+ return await ApiDependencies.invoker.services.session_queue.enqueue_batch(
+ queue_id=queue_id, batch=batch, prepend=prepend, user_id=current_user.user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while enqueuing batch: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/list_all",
+ operation_id="list_all_queue_items",
+ responses={
+ 200: {"model": list[SessionQueueItem]},
+ },
+)
+async def list_all_queue_items(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
+) -> list[SessionQueueItem]:
+ """Gets all queue items"""
+ try:
+ items = ApiDependencies.invoker.services.session_queue.list_all_queue_items(
+ queue_id=queue_id,
+ destination=destination,
+ )
+ # Sanitize items for non-admin users
+ return [sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin) for item in items]
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue items: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/item_ids",
+ operation_id="get_queue_item_ids",
+ responses={
+ 200: {"model": ItemIdsResult},
+ },
+)
+async def get_queue_item_ids(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
+) -> ItemIdsResult:
+ """Gets all queue item ids that match the given parameters. Non-admin users only see their own items."""
+ try:
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(
+ queue_id=queue_id, order_dir=order_dir, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue item ids: {e}")
+
+
+@session_queue_router.post(
+ "/{queue_id}/items_by_ids",
+ operation_id="get_queue_items_by_item_ids",
+ responses={200: {"model": list[SessionQueueItem]}},
+)
+async def get_queue_items_by_item_ids(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_ids: list[int] = Body(
+ embed=True, description="Object containing list of queue item ids to fetch queue items for"
+ ),
+) -> list[SessionQueueItem]:
+ """Gets queue items for the specified queue item ids. Maintains order of item ids."""
+ try:
+ session_queue_service = ApiDependencies.invoker.services.session_queue
+
+ # Fetch queue items preserving the order of requested item ids
+ queue_items: list[SessionQueueItem] = []
+ for item_id in item_ids:
+ try:
+ queue_item = session_queue_service.get_queue_item(item_id=item_id)
+ if queue_item.queue_id != queue_id: # Auth protection for items from other queues
+ continue
+ # Sanitize item for non-admin users
+ sanitized_item = sanitize_queue_item_for_user(queue_item, current_user.user_id, current_user.is_admin)
+ queue_items.append(sanitized_item)
+ except Exception:
+ # Skip missing queue items - they may have been deleted between item id fetch and queue item fetch
+ continue
+
+ return queue_items
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get queue items")
+
+
+@session_queue_router.put(
+ "/{queue_id}/processor/resume",
+ operation_id="resume",
+ responses={200: {"model": SessionProcessorStatus}},
+)
+async def resume(
+ current_user: AdminUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> SessionProcessorStatus:
+ """Resumes session processor. Admin only."""
+ try:
+ return ApiDependencies.invoker.services.session_processor.resume()
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while resuming queue: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/processor/pause",
+ operation_id="pause",
+ responses={200: {"model": SessionProcessorStatus}},
+)
+async def pause(
+ current_user: AdminUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> SessionProcessorStatus:
+ """Pauses session processor. Admin only."""
+ try:
+ return ApiDependencies.invoker.services.session_processor.pause()
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while pausing queue: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/cancel_all_except_current",
+ operation_id="cancel_all_except_current",
+ responses={200: {"model": CancelAllExceptCurrentResult}},
+)
+async def cancel_all_except_current(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> CancelAllExceptCurrentResult:
+ """Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items."""
+ try:
+ # Admin users can cancel all items, non-admin users can only cancel their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(
+ queue_id=queue_id, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while canceling all except current: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/delete_all_except_current",
+ operation_id="delete_all_except_current",
+ responses={200: {"model": DeleteAllExceptCurrentResult}},
+)
+async def delete_all_except_current(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> DeleteAllExceptCurrentResult:
+ """Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items."""
+ try:
+ # Admin users can delete all items, non-admin users can only delete their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.delete_all_except_current(
+ queue_id=queue_id, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while deleting all except current: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/cancel_by_batch_ids",
+ operation_id="cancel_by_batch_ids",
+ responses={200: {"model": CancelByBatchIDsResult}},
+)
+async def cancel_by_batch_ids(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ batch_ids: list[str] = Body(description="The list of batch_ids to cancel all queue items for", embed=True),
+) -> CancelByBatchIDsResult:
+ """Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items."""
+ try:
+ # Admin users can cancel all items, non-admin users can only cancel their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(
+ queue_id=queue_id, batch_ids=batch_ids, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by batch id: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/cancel_by_destination",
+ operation_id="cancel_by_destination",
+ responses={200: {"model": CancelByDestinationResult}},
+)
+async def cancel_by_destination(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ destination: str = Query(description="The destination to cancel all queue items for"),
+) -> CancelByDestinationResult:
+ """Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items."""
+ try:
+ # Admin users can cancel all items, non-admin users can only cancel their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
+ queue_id=queue_id, destination=destination, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by destination: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/retry_items_by_id",
+ operation_id="retry_items_by_id",
+ responses={200: {"model": RetryItemsResult}},
+)
+async def retry_items_by_id(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_ids: list[int] = Body(description="The queue item ids to retry"),
+) -> RetryItemsResult:
+ """Retries the given queue items. Users can only retry their own items unless they are an admin."""
+ try:
+ # Check authorization: user must own all items or be an admin
+ if not current_user.is_admin:
+ for item_id in item_ids:
+ try:
+ queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
+ if queue_item.user_id != current_user.user_id:
+ raise HTTPException(
+ status_code=403, detail=f"You do not have permission to retry queue item {item_id}"
+ )
+ except SessionQueueItemNotFoundError:
+ # Skip items that don't exist - they will be handled by retry_items_by_id
+ continue
+
+ return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids)
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while retrying queue items: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/clear",
+ operation_id="clear",
+ responses={
+ 200: {"model": ClearResult},
+ },
+)
+async def clear(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> ClearResult:
+ """Clears the queue entirely. Admin users clear all items; non-admin users only clear their own items. If there's a currently-executing item, users can only cancel it if they own it or are an admin."""
+ try:
+ queue_item = ApiDependencies.invoker.services.session_queue.get_current(queue_id)
+ if queue_item is not None:
+ # Check authorization for canceling the current item
+ if queue_item.user_id != current_user.user_id and not current_user.is_admin:
+ raise HTTPException(
+ status_code=403, detail="You do not have permission to cancel the currently executing queue item"
+ )
+ ApiDependencies.invoker.services.session_queue.cancel_queue_item(queue_item.item_id)
+ # Admin users can clear all items, non-admin users can only clear their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id, user_id=user_id)
+ return clear_result
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while clearing queue: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/prune",
+ operation_id="prune",
+ responses={
+ 200: {"model": PruneResult},
+ },
+)
+async def prune(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> PruneResult:
+ """Prunes all completed or errored queue items. Non-admin users can only prune their own items."""
+ try:
+ # Admin users can prune all items, non-admin users can only prune their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.prune(queue_id, user_id=user_id)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while pruning queue: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/current",
+ operation_id="get_current_queue_item",
+ responses={
+ 200: {"model": Optional[SessionQueueItem]},
+ },
+)
+async def get_current_queue_item(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> Optional[SessionQueueItem]:
+ """Gets the currently execution queue item"""
+ try:
+ item = ApiDependencies.invoker.services.session_queue.get_current(queue_id)
+ if item is not None:
+ item = sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin)
+ return item
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while getting current queue item: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/next",
+ operation_id="get_next_queue_item",
+ responses={
+ 200: {"model": Optional[SessionQueueItem]},
+ },
+)
+async def get_next_queue_item(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> Optional[SessionQueueItem]:
+ """Gets the next queue item, without executing it"""
+ try:
+ item = ApiDependencies.invoker.services.session_queue.get_next(queue_id)
+ if item is not None:
+ item = sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin)
+ return item
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while getting next queue item: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/status",
+ operation_id="get_queue_status",
+ responses={
+ 200: {"model": SessionQueueAndProcessorStatus},
+ },
+)
+async def get_queue_status(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+) -> SessionQueueAndProcessorStatus:
+ """Gets the status of the session queue. Non-admin users see only their own counts and cannot see current item details unless they own it."""
+ try:
+ user_id = None if current_user.is_admin else current_user.user_id
+ queue = ApiDependencies.invoker.services.session_queue.get_queue_status(queue_id, user_id=user_id)
+ processor = ApiDependencies.invoker.services.session_processor.get_status()
+ return SessionQueueAndProcessorStatus(queue=queue, processor=processor)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while getting queue status: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/b/{batch_id}/status",
+ operation_id="get_batch_status",
+ responses={
+ 200: {"model": BatchStatus},
+ },
+)
+async def get_batch_status(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ batch_id: str = Path(description="The batch to get the status of"),
+) -> BatchStatus:
+ """Gets the status of a batch. Non-admin users only see their own batches."""
+ try:
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.get_batch_status(
+ queue_id=queue_id, batch_id=batch_id, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while getting batch status: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/i/{item_id}",
+ operation_id="get_queue_item",
+ responses={
+ 200: {"model": SessionQueueItem},
+ },
+ response_model_exclude_none=True,
+)
+async def get_queue_item(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_id: int = Path(description="The queue item to get"),
+) -> SessionQueueItem:
+ """Gets a queue item"""
+ try:
+ queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id=item_id)
+ if queue_item.queue_id != queue_id:
+ raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
+ # Sanitize item for non-admin users
+ return sanitize_queue_item_for_user(queue_item, current_user.user_id, current_user.is_admin)
+ except SessionQueueItemNotFoundError:
+ raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while fetching queue item: {e}")
+
+
+@session_queue_router.delete(
+ "/{queue_id}/i/{item_id}",
+ operation_id="delete_queue_item",
+)
+async def delete_queue_item(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_id: int = Path(description="The queue item to delete"),
+) -> None:
+ """Deletes a queue item. Users can only delete their own items unless they are an admin."""
+ try:
+ # Get the queue item to check ownership
+ queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
+
+ # Check authorization: user must own the item or be an admin
+ if queue_item.user_id != current_user.user_id and not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="You do not have permission to delete this queue item")
+
+ ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id)
+ except SessionQueueItemNotFoundError:
+ raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while deleting queue item: {e}")
+
+
+@session_queue_router.put(
+ "/{queue_id}/i/{item_id}/cancel",
+ operation_id="cancel_queue_item",
+ responses={
+ 200: {"model": SessionQueueItem},
+ },
+)
+async def cancel_queue_item(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to perform this operation on"),
+ item_id: int = Path(description="The queue item to cancel"),
+) -> SessionQueueItem:
+ """Cancels a queue item. Users can only cancel their own items unless they are an admin."""
+ try:
+ # Get the queue item to check ownership
+ queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
+
+ # Check authorization: user must own the item or be an admin
+ if queue_item.user_id != current_user.user_id and not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="You do not have permission to cancel this queue item")
+
+ return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id)
+ except SessionQueueItemNotFoundError:
+ raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while canceling queue item: {e}")
+
+
+@session_queue_router.get(
+ "/{queue_id}/counts_by_destination",
+ operation_id="counts_by_destination",
+ responses={200: {"model": SessionQueueCountsByDestination}},
+)
+async def counts_by_destination(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to query"),
+ destination: str = Query(description="The destination to query"),
+) -> SessionQueueCountsByDestination:
+ """Gets the counts of queue items by destination. Non-admin users only see their own items."""
+ try:
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.get_counts_by_destination(
+ queue_id=queue_id, destination=destination, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while fetching counts by destination: {e}")
+
+
+@session_queue_router.delete(
+ "/{queue_id}/d/{destination}",
+ operation_id="delete_by_destination",
+ responses={200: {"model": DeleteByDestinationResult}},
+)
+async def delete_by_destination(
+ current_user: CurrentUserOrDefault,
+ queue_id: str = Path(description="The queue id to query"),
+ destination: str = Path(description="The destination to query"),
+) -> DeleteByDestinationResult:
+ """Deletes all items with the given destination. Non-admin users can only delete their own items."""
+ try:
+ # Admin users can delete all items, non-admin users can only delete their own
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.session_queue.delete_by_destination(
+ queue_id=queue_id, destination=destination, user_id=user_id
+ )
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Unexpected error while deleting by destination: {e}")
diff --git a/invokeai/app/api/routers/style_presets.py b/invokeai/app/api/routers/style_presets.py
new file mode 100644
index 00000000000..91acf8e7a6b
--- /dev/null
+++ b/invokeai/app/api/routers/style_presets.py
@@ -0,0 +1,339 @@
+import csv
+import io
+import json
+import traceback
+from typing import Optional
+
+import pydantic
+from fastapi import APIRouter, File, Form, HTTPException, Path, Response, UploadFile
+from fastapi.responses import FileResponse
+from PIL import Image
+from pydantic import BaseModel, Field
+
+from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.routers.model_manager import IMAGE_MAX_AGE
+from invokeai.app.services.auth.token_service import TokenData
+from invokeai.app.services.style_preset_images.style_preset_images_common import StylePresetImageFileNotFoundException
+from invokeai.app.services.style_preset_records.style_preset_records_common import (
+ InvalidPresetImportDataError,
+ PresetData,
+ PresetType,
+ StylePresetChanges,
+ StylePresetNotFoundError,
+ StylePresetRecordDTO,
+ StylePresetRecordWithImage,
+ StylePresetWithoutId,
+ UnsupportedFileTypeError,
+ parse_presets_from_file,
+)
+
+
+class StylePresetFormData(BaseModel):
+ name: str = Field(description="Preset name")
+ positive_prompt: str = Field(description="Positive prompt")
+ negative_prompt: str = Field(description="Negative prompt")
+ type: PresetType = Field(description="Preset type")
+ is_public: bool = Field(default=False, description="Whether the preset is visible to other users")
+
+
+style_presets_router = APIRouter(prefix="/v1/style_presets", tags=["style_presets"])
+
+
+def _assert_preset_read(record: StylePresetRecordDTO, current_user: TokenData) -> None:
+ """Allow read access if admin, owner, default preset, or public preset."""
+ if current_user.is_admin:
+ return
+ if record.type == PresetType.Default:
+ return
+ if record.is_public:
+ return
+ if record.user_id == current_user.user_id:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to access this style preset")
+
+
+def _assert_preset_write(record: StylePresetRecordDTO, current_user: TokenData) -> None:
+ """Allow write access only for admin or owner. Defaults are immutable for non-admins."""
+ if current_user.is_admin:
+ return
+ if record.type == PresetType.Default:
+ raise HTTPException(status_code=403, detail="Default style presets cannot be modified")
+ if record.user_id == current_user.user_id:
+ return
+ raise HTTPException(status_code=403, detail="Not authorized to modify this style preset")
+
+
+def _load_record_or_404(style_preset_id: str) -> StylePresetRecordDTO:
+ try:
+ return ApiDependencies.invoker.services.style_preset_records.get(style_preset_id)
+ except StylePresetNotFoundError:
+ raise HTTPException(status_code=404, detail="Style preset not found")
+
+
+@style_presets_router.get(
+ "/i/{style_preset_id}",
+ operation_id="get_style_preset",
+ responses={
+ 200: {"model": StylePresetRecordWithImage},
+ },
+)
+async def get_style_preset(
+ current_user: CurrentUserOrDefault,
+ style_preset_id: str = Path(description="The style preset to get"),
+) -> StylePresetRecordWithImage:
+ """Gets a style preset"""
+ record = _load_record_or_404(style_preset_id)
+ _assert_preset_read(record, current_user)
+ image = ApiDependencies.invoker.services.style_preset_image_files.get_url(style_preset_id)
+ return StylePresetRecordWithImage(image=image, **record.model_dump())
+
+
+@style_presets_router.patch(
+ "/i/{style_preset_id}",
+ operation_id="update_style_preset",
+ responses={
+ 200: {"model": StylePresetRecordWithImage},
+ },
+)
+async def update_style_preset(
+ current_user: CurrentUserOrDefault,
+ image: Optional[UploadFile] = File(description="The image file to upload", default=None),
+ style_preset_id: str = Path(description="The id of the style preset to update"),
+ data: str = Form(description="The data of the style preset to update"),
+) -> StylePresetRecordWithImage:
+ """Updates a style preset"""
+ # Validate the data payload BEFORE any image-state mutation so a malformed
+ # request can't leave the preset image partially updated.
+ try:
+ parsed_data = json.loads(data)
+ validated_data = StylePresetFormData(**parsed_data)
+
+ name = validated_data.name
+ type = validated_data.type
+ positive_prompt = validated_data.positive_prompt
+ negative_prompt = validated_data.negative_prompt
+ is_public = validated_data.is_public
+
+ except (json.JSONDecodeError, pydantic.ValidationError):
+ raise HTTPException(status_code=400, detail="Invalid preset data")
+
+ record = _load_record_or_404(style_preset_id)
+ _assert_preset_write(record, current_user)
+
+ if image is not None:
+ if not image.content_type or not image.content_type.startswith("image"):
+ raise HTTPException(status_code=415, detail="Not an image")
+
+ contents = await image.read()
+ try:
+ pil_image = Image.open(io.BytesIO(contents))
+
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read image")
+
+ try:
+ ApiDependencies.invoker.services.style_preset_image_files.save(style_preset_id, pil_image)
+ except ValueError as e:
+ raise HTTPException(status_code=409, detail=str(e))
+ else:
+ try:
+ ApiDependencies.invoker.services.style_preset_image_files.delete(style_preset_id)
+ except StylePresetImageFileNotFoundException:
+ pass
+
+ preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
+ changes = StylePresetChanges(name=name, preset_data=preset_data, type=type, is_public=is_public)
+
+ style_preset_image = ApiDependencies.invoker.services.style_preset_image_files.get_url(style_preset_id)
+ style_preset = ApiDependencies.invoker.services.style_preset_records.update(
+ style_preset_id=style_preset_id, changes=changes
+ )
+ return StylePresetRecordWithImage(image=style_preset_image, **style_preset.model_dump())
+
+
+@style_presets_router.delete(
+ "/i/{style_preset_id}",
+ operation_id="delete_style_preset",
+)
+async def delete_style_preset(
+ current_user: CurrentUserOrDefault,
+ style_preset_id: str = Path(description="The style preset to delete"),
+) -> None:
+ """Deletes a style preset"""
+ record = _load_record_or_404(style_preset_id)
+ _assert_preset_write(record, current_user)
+
+ try:
+ ApiDependencies.invoker.services.style_preset_image_files.delete(style_preset_id)
+ except StylePresetImageFileNotFoundException:
+ pass
+
+ ApiDependencies.invoker.services.style_preset_records.delete(style_preset_id)
+
+
+@style_presets_router.post(
+ "/",
+ operation_id="create_style_preset",
+ responses={
+ 200: {"model": StylePresetRecordWithImage},
+ },
+)
+async def create_style_preset(
+ current_user: CurrentUserOrDefault,
+ image: Optional[UploadFile] = File(description="The image file to upload", default=None),
+ data: str = Form(description="The data of the style preset to create"),
+) -> StylePresetRecordWithImage:
+ """Creates a style preset"""
+
+ try:
+ parsed_data = json.loads(data)
+ validated_data = StylePresetFormData(**parsed_data)
+
+ name = validated_data.name
+ type = validated_data.type
+ positive_prompt = validated_data.positive_prompt
+ negative_prompt = validated_data.negative_prompt
+ is_public = validated_data.is_public
+
+ except (json.JSONDecodeError, pydantic.ValidationError):
+ raise HTTPException(status_code=400, detail="Invalid preset data")
+
+ # Only admins may create default-typed presets — they're the shipped catalog.
+ if type == PresetType.Default and not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Only admins can create default presets")
+
+ preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
+ style_preset = StylePresetWithoutId(name=name, preset_data=preset_data, type=type, is_public=is_public)
+ new_style_preset = ApiDependencies.invoker.services.style_preset_records.create(
+ style_preset=style_preset, user_id=current_user.user_id
+ )
+
+ if image is not None:
+ if not image.content_type or not image.content_type.startswith("image"):
+ raise HTTPException(status_code=415, detail="Not an image")
+
+ contents = await image.read()
+ try:
+ pil_image = Image.open(io.BytesIO(contents))
+
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read image")
+
+ try:
+ ApiDependencies.invoker.services.style_preset_image_files.save(new_style_preset.id, pil_image)
+ except ValueError as e:
+ raise HTTPException(status_code=409, detail=str(e))
+
+ preset_image = ApiDependencies.invoker.services.style_preset_image_files.get_url(new_style_preset.id)
+ return StylePresetRecordWithImage(image=preset_image, **new_style_preset.model_dump())
+
+
+@style_presets_router.get(
+ "/",
+ operation_id="list_style_presets",
+ responses={
+ 200: {"model": list[StylePresetRecordWithImage]},
+ },
+)
+async def list_style_presets(current_user: CurrentUserOrDefault) -> list[StylePresetRecordWithImage]:
+ """Gets the style presets visible to the current user."""
+ style_presets_with_image: list[StylePresetRecordWithImage] = []
+ style_presets = ApiDependencies.invoker.services.style_preset_records.get_many(
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ for preset in style_presets:
+ image = ApiDependencies.invoker.services.style_preset_image_files.get_url(preset.id)
+ style_preset_with_image = StylePresetRecordWithImage(image=image, **preset.model_dump())
+ style_presets_with_image.append(style_preset_with_image)
+
+ return style_presets_with_image
+
+
+@style_presets_router.get(
+ "/i/{style_preset_id}/image",
+ operation_id="get_style_preset_image",
+ responses={
+ 200: {
+ "description": "The style preset image was fetched successfully",
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "The style preset image could not be found"},
+ },
+ status_code=200,
+)
+async def get_style_preset_image(
+ current_user: CurrentUserOrDefault,
+ style_preset_id: str = Path(description="The id of the style preset image to get"),
+) -> FileResponse:
+ """Gets an image file that previews the model"""
+ record = _load_record_or_404(style_preset_id)
+ _assert_preset_read(record, current_user)
+
+ try:
+ path = ApiDependencies.invoker.services.style_preset_image_files.get_path(style_preset_id)
+
+ response = FileResponse(
+ path,
+ media_type="image/png",
+ filename=style_preset_id + ".png",
+ content_disposition_type="inline",
+ )
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ return response
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@style_presets_router.get(
+ "/export",
+ operation_id="export_style_presets",
+ responses={200: {"content": {"text/csv": {}}, "description": "A CSV file with the requested data."}},
+ status_code=200,
+)
+async def export_style_presets(current_user: AdminUserOrDefault):
+ # Admin-only export covers every user preset.
+ output = io.StringIO()
+ writer = csv.writer(output)
+
+ writer.writerow(["name", "prompt", "negative_prompt"])
+
+ style_presets = ApiDependencies.invoker.services.style_preset_records.get_many(
+ type=PresetType.User,
+ user_id=current_user.user_id,
+ is_admin=True,
+ )
+
+ for preset in style_presets:
+ writer.writerow([preset.name, preset.preset_data.positive_prompt, preset.preset_data.negative_prompt])
+
+ csv_data = output.getvalue()
+ output.close()
+
+ return Response(
+ content=csv_data,
+ media_type="text/csv",
+ headers={"Content-Disposition": "attachment; filename=prompt_templates.csv"},
+ )
+
+
+@style_presets_router.post(
+ "/import",
+ operation_id="import_style_presets",
+)
+async def import_style_presets(
+ current_user: AdminUserOrDefault,
+ file: UploadFile = File(description="The file to import"),
+):
+ try:
+ style_presets = await parse_presets_from_file(file)
+ ApiDependencies.invoker.services.style_preset_records.create_many(style_presets, user_id=current_user.user_id)
+ except InvalidPresetImportDataError as e:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=400, detail=str(e))
+ except UnsupportedFileTypeError as e:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail=str(e))
diff --git a/invokeai/app/api/routers/utilities.py b/invokeai/app/api/routers/utilities.py
new file mode 100644
index 00000000000..568546603ab
--- /dev/null
+++ b/invokeai/app/api/routers/utilities.py
@@ -0,0 +1,227 @@
+import asyncio
+import logging
+import threading
+from pathlib import Path
+from typing import Optional, Union
+
+import torch
+from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPromptGenerator
+from fastapi import Body, HTTPException
+from fastapi.routing import APIRouter
+from pydantic import BaseModel, Field
+from pyparsing import ParseException
+from transformers import AutoProcessor, AutoTokenizer, LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.routers._access import assert_image_read_access
+from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
+from invokeai.app.services.model_records.model_records_base import UnknownModelException
+from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
+from invokeai.backend.model_manager.taxonomy import ModelType
+from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, TextLLMPipeline
+from invokeai.backend.util.devices import TorchDevice
+
+logger = logging.getLogger(__name__)
+
+utilities_router = APIRouter(prefix="/v1/utilities", tags=["utilities"])
+
+# The underlying model loader is not thread-safe, so we serialize load_model calls.
+_model_load_lock = threading.Lock()
+
+
+class DynamicPromptsResponse(BaseModel):
+ prompts: list[str]
+ error: Optional[str] = None
+
+
+@utilities_router.post(
+ "/dynamicprompts",
+ operation_id="parse_dynamicprompts",
+ responses={
+ 200: {"model": DynamicPromptsResponse},
+ },
+)
+async def parse_dynamicprompts(
+ current_user: CurrentUserOrDefault,
+ prompt: str = Body(description="The prompt to parse with dynamicprompts"),
+ max_prompts: int = Body(ge=1, le=10000, default=1000, description="The max number of prompts to generate"),
+ combinatorial: bool = Body(default=True, description="Whether to use the combinatorial generator"),
+ seed: int | None = Body(None, description="The seed to use for random generation. Only used if not combinatorial"),
+) -> DynamicPromptsResponse:
+ """Creates a batch process"""
+ max_prompts = min(max_prompts, 10000)
+ generator: Union[RandomPromptGenerator, CombinatorialPromptGenerator]
+ try:
+ error: Optional[str] = None
+ if combinatorial:
+ generator = CombinatorialPromptGenerator()
+ prompts = generator.generate(prompt, max_prompts=max_prompts)
+ else:
+ generator = RandomPromptGenerator(seed=seed)
+ prompts = generator.generate(prompt, num_images=max_prompts)
+ except ParseException as e:
+ prompts = [prompt]
+ error = str(e)
+ return DynamicPromptsResponse(prompts=prompts if prompts else [""], error=error)
+
+
+# --- Expand Prompt ---
+
+
+class ExpandPromptRequest(BaseModel):
+ prompt: str
+ model_key: str
+ max_tokens: int = Field(default=300, ge=1, le=2048)
+ system_prompt: str | None = None
+
+
+class ExpandPromptResponse(BaseModel):
+ expanded_prompt: str
+ error: str | None = None
+
+
+def _resolve_model_path(model_config_path: str) -> Path:
+ """Resolve a model config path to an absolute path."""
+ model_path = Path(model_config_path)
+ if model_path.is_absolute():
+ return model_path.resolve()
+ base_models_path = ApiDependencies.invoker.services.configuration.models_path
+ return (base_models_path / model_path).resolve()
+
+
+def _run_expand_prompt(prompt: str, model_key: str, max_tokens: int, system_prompt: str | None) -> str:
+ """Run text LLM inference synchronously (called from thread)."""
+ model_manager = ApiDependencies.invoker.services.model_manager
+ model_config = model_manager.store.get_model(model_key)
+
+ if model_config.type != ModelType.TextLLM:
+ raise ValueError(f"Model '{model_key}' is not a TextLLM model (got {model_config.type})")
+
+ with _model_load_lock:
+ loaded_model = model_manager.load.load_model(model_config)
+
+ with torch.no_grad(), loaded_model.model_on_device() as (_, model):
+ model_abs_path = _resolve_model_path(model_config.path)
+ tokenizer = AutoTokenizer.from_pretrained(model_abs_path, local_files_only=True)
+
+ pipeline = TextLLMPipeline(model, tokenizer)
+ model_device = next(model.parameters()).device
+ output = pipeline.run(
+ prompt=prompt,
+ system_prompt=system_prompt or DEFAULT_SYSTEM_PROMPT,
+ max_new_tokens=max_tokens,
+ device=model_device,
+ dtype=TorchDevice.choose_torch_dtype(),
+ )
+
+ return output
+
+
+@utilities_router.post(
+ "/expand-prompt",
+ operation_id="expand_prompt",
+ responses={
+ 200: {"model": ExpandPromptResponse},
+ },
+)
+async def expand_prompt(current_user: CurrentUserOrDefault, body: ExpandPromptRequest) -> ExpandPromptResponse:
+ """Expand a brief prompt into a detailed image generation prompt using a text LLM."""
+ try:
+ expanded = await asyncio.to_thread(
+ _run_expand_prompt,
+ body.prompt,
+ body.model_key,
+ body.max_tokens,
+ body.system_prompt,
+ )
+ return ExpandPromptResponse(expanded_prompt=expanded)
+ except UnknownModelException:
+ raise HTTPException(status_code=404, detail=f"Model '{body.model_key}' not found")
+ except ValueError as e:
+ raise HTTPException(status_code=422, detail=str(e))
+ except Exception as e:
+ logger.error(f"Error expanding prompt: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+# --- Image to Prompt ---
+
+
+class ImageToPromptRequest(BaseModel):
+ image_name: str
+ model_key: str
+ instruction: str = "Describe this image in detail for use as an AI image generation prompt."
+
+
+class ImageToPromptResponse(BaseModel):
+ prompt: str
+ error: str | None = None
+
+
+def _run_image_to_prompt(image_name: str, model_key: str, instruction: str) -> str:
+ """Run LLaVA OneVision inference synchronously (called from thread)."""
+ model_manager = ApiDependencies.invoker.services.model_manager
+ model_config = model_manager.store.get_model(model_key)
+
+ if model_config.type != ModelType.LlavaOnevision:
+ raise ValueError(f"Model '{model_key}' is not a LLaVA OneVision model (got {model_config.type})")
+
+ with _model_load_lock:
+ loaded_model = model_manager.load.load_model(model_config)
+
+ # Load the image from InvokeAI's image store
+ image = ApiDependencies.invoker.services.images.get_pil_image(image_name)
+ image = image.convert("RGB")
+
+ with torch.no_grad(), loaded_model.model_on_device() as (_, model):
+ if not isinstance(model, LlavaOnevisionForConditionalGeneration):
+ raise TypeError(f"Expected LlavaOnevisionForConditionalGeneration, got {type(model).__name__}")
+
+ model_abs_path = _resolve_model_path(model_config.path)
+ processor = AutoProcessor.from_pretrained(model_abs_path, local_files_only=True)
+ if not isinstance(processor, LlavaOnevisionProcessor):
+ raise TypeError(f"Expected LlavaOnevisionProcessor, got {type(processor).__name__}")
+
+ pipeline = LlavaOnevisionPipeline(model, processor)
+ model_device = next(model.parameters()).device
+ output = pipeline.run(
+ prompt=instruction,
+ images=[image],
+ device=model_device,
+ dtype=TorchDevice.choose_torch_dtype(),
+ )
+
+ return output
+
+
+@utilities_router.post(
+ "/image-to-prompt",
+ operation_id="image_to_prompt",
+ responses={
+ 200: {"model": ImageToPromptResponse},
+ },
+)
+async def image_to_prompt(current_user: CurrentUserOrDefault, body: ImageToPromptRequest) -> ImageToPromptResponse:
+ """Generate a descriptive prompt from an image using a vision-language model."""
+ # Reuse the image-read access check so non-owners can't probe stored images
+ # via this endpoint (mirrors the policy in routers/images.py).
+ assert_image_read_access(body.image_name, current_user)
+
+ try:
+ prompt = await asyncio.to_thread(
+ _run_image_to_prompt,
+ body.image_name,
+ body.model_key,
+ body.instruction,
+ )
+ return ImageToPromptResponse(prompt=prompt)
+ except UnknownModelException:
+ raise HTTPException(status_code=404, detail=f"Model '{body.model_key}' not found")
+ except ImageFileNotFoundException:
+ raise HTTPException(status_code=404, detail=f"Image '{body.image_name}' not found")
+ except (ValueError, TypeError) as e:
+ raise HTTPException(status_code=422, detail=str(e))
+ except Exception as e:
+ logger.error(f"Error generating prompt from image: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/invokeai/app/api/routers/virtual_boards.py b/invokeai/app/api/routers/virtual_boards.py
new file mode 100644
index 00000000000..f0c9e2edc51
--- /dev/null
+++ b/invokeai/app/api/routers/virtual_boards.py
@@ -0,0 +1,56 @@
+from fastapi import HTTPException, Path, Query
+from fastapi.routing import APIRouter
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ImageNamesResult
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
+
+virtual_boards_router = APIRouter(prefix="/v1/virtual_boards", tags=["virtual_boards"])
+
+
+@virtual_boards_router.get(
+ "/by_date",
+ operation_id="list_virtual_boards_by_date",
+ response_model=list[VirtualSubBoardDTO],
+)
+async def list_virtual_boards_by_date(
+ current_user: CurrentUserOrDefault,
+) -> list[VirtualSubBoardDTO]:
+ """Gets a list of virtual sub-boards grouped by date."""
+ try:
+ return ApiDependencies.invoker.services.image_records.get_image_dates(
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get virtual boards by date")
+
+
+@virtual_boards_router.get(
+ "/by_date/{date}/image_names",
+ operation_id="list_virtual_board_image_names_by_date",
+ response_model=ImageNamesResult,
+)
+async def list_virtual_board_image_names_by_date(
+ current_user: CurrentUserOrDefault,
+ date: str = Path(description="The ISO date string, e.g. '2026-03-18'"),
+ starred_first: bool = Query(default=True, description="Whether to sort starred images first"),
+ order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The sort direction"),
+ categories: list[ImageCategory] | None = Query(default=None, description="The categories of images to include"),
+ search_term: str | None = Query(default=None, description="Search term to filter images"),
+) -> ImageNamesResult:
+ """Gets ordered image names for a specific date."""
+ try:
+ return ApiDependencies.invoker.services.image_records.get_image_names_by_date(
+ date=date,
+ starred_first=starred_first,
+ order_dir=order_dir,
+ categories=categories,
+ search_term=search_term,
+ user_id=current_user.user_id,
+ is_admin=current_user.is_admin,
+ )
+ except Exception:
+ raise HTTPException(status_code=500, detail="Failed to get image names for date")
diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py
new file mode 100644
index 00000000000..eb893251953
--- /dev/null
+++ b/invokeai/app/api/routers/workflows.py
@@ -0,0 +1,399 @@
+import io
+import traceback
+from typing import Optional
+
+from fastapi import APIRouter, Body, File, HTTPException, Path, Query, UploadFile
+from fastapi.responses import FileResponse
+from PIL import Image
+
+from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.services.shared.pagination import PaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.workflow_records.workflow_records_common import (
+ Workflow,
+ WorkflowCategory,
+ WorkflowNotFoundError,
+ WorkflowRecordDTO,
+ WorkflowRecordListItemWithThumbnailDTO,
+ WorkflowRecordOrderBy,
+ WorkflowRecordWithThumbnailDTO,
+ WorkflowWithoutID,
+)
+from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_common import WorkflowThumbnailFileNotFoundException
+
+IMAGE_MAX_AGE = 31536000
+workflows_router = APIRouter(prefix="/v1/workflows", tags=["workflows"])
+
+
+@workflows_router.get(
+ "/i/{workflow_id}",
+ operation_id="get_workflow",
+ responses={
+ 200: {"model": WorkflowRecordWithThumbnailDTO},
+ },
+)
+async def get_workflow(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to get"),
+) -> WorkflowRecordWithThumbnailDTO:
+ """Gets a workflow"""
+ try:
+ workflow = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser:
+ is_default = workflow.workflow.meta.category is WorkflowCategory.Default
+ is_owner = workflow.user_id == current_user.user_id
+ if not (is_default or is_owner or workflow.is_public or current_user.is_admin):
+ raise HTTPException(status_code=403, detail="Not authorized to access this workflow")
+
+ thumbnail_url = ApiDependencies.invoker.services.workflow_thumbnails.get_url(workflow_id)
+ return WorkflowRecordWithThumbnailDTO(thumbnail_url=thumbnail_url, **workflow.model_dump())
+
+
+@workflows_router.patch(
+ "/i/{workflow_id}",
+ operation_id="update_workflow",
+ responses={
+ 200: {"model": WorkflowRecordDTO},
+ },
+)
+async def update_workflow(
+ current_user: CurrentUserOrDefault,
+ workflow: Workflow = Body(description="The updated workflow", embed=True),
+) -> WorkflowRecordDTO:
+ """Updates a workflow"""
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser:
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow.id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+ if not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+ # Pass user_id for defense-in-depth SQL scoping; admins pass None to allow any.
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.workflow_records.update(workflow=workflow, user_id=user_id)
+
+
+@workflows_router.delete(
+ "/i/{workflow_id}",
+ operation_id="delete_workflow",
+)
+async def delete_workflow(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to delete"),
+) -> None:
+ """Deletes a workflow"""
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser:
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+ if not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to delete this workflow")
+ try:
+ ApiDependencies.invoker.services.workflow_thumbnails.delete(workflow_id)
+ except WorkflowThumbnailFileNotFoundException:
+ # It's OK if the workflow has no thumbnail file. We can still delete the workflow.
+ pass
+ user_id = None if current_user.is_admin else current_user.user_id
+ ApiDependencies.invoker.services.workflow_records.delete(workflow_id, user_id=user_id)
+
+
+@workflows_router.post(
+ "/",
+ operation_id="create_workflow",
+ responses={
+ 200: {"model": WorkflowRecordDTO},
+ },
+)
+async def create_workflow(
+ current_user: CurrentUserOrDefault,
+ workflow: WorkflowWithoutID = Body(description="The workflow to create", embed=True),
+) -> WorkflowRecordDTO:
+ """Creates a workflow"""
+ # In single-user mode, workflows are owned by 'system' and shared by default so all legacy/single-user
+ # workflows remain visible. In multiuser mode, workflows are private to the creator by default.
+ config = ApiDependencies.invoker.services.configuration
+ is_public = not config.multiuser
+ return ApiDependencies.invoker.services.workflow_records.create(
+ workflow=workflow, user_id=current_user.user_id, is_public=is_public
+ )
+
+
+@workflows_router.get(
+ "/",
+ operation_id="list_workflows",
+ responses={
+ 200: {"model": PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]},
+ },
+)
+async def list_workflows(
+ current_user: CurrentUserOrDefault,
+ page: int = Query(default=0, description="The page to get"),
+ per_page: Optional[int] = Query(default=None, description="The number of workflows per page"),
+ order_by: WorkflowRecordOrderBy = Query(
+ default=WorkflowRecordOrderBy.Name, description="The attribute to order by"
+ ),
+ direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"),
+ categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories of workflow to get"),
+ tags: Optional[list[str]] = Query(default=None, description="The tags of workflow to get"),
+ query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
+ has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
+ is_public: Optional[bool] = Query(default=None, description="Filter by public/shared status"),
+) -> PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]:
+ """Gets a page of workflows"""
+ config = ApiDependencies.invoker.services.configuration
+
+ # In multiuser mode, scope user-category workflows to the current user unless fetching shared workflows.
+ # Admins skip the user_id filter so they can see and manage all workflows including system-owned ones.
+ user_id_filter: Optional[str] = None
+ if config.multiuser and not current_user.is_admin:
+ has_user_category = not categories or WorkflowCategory.User in categories
+ if has_user_category and is_public is not True:
+ user_id_filter = current_user.user_id
+
+ workflows_with_thumbnails: list[WorkflowRecordListItemWithThumbnailDTO] = []
+ workflows = ApiDependencies.invoker.services.workflow_records.get_many(
+ order_by=order_by,
+ direction=direction,
+ page=page,
+ per_page=per_page,
+ query=query,
+ categories=categories,
+ tags=tags,
+ has_been_opened=has_been_opened,
+ user_id=user_id_filter,
+ is_public=is_public,
+ )
+ for workflow in workflows.items:
+ workflows_with_thumbnails.append(
+ WorkflowRecordListItemWithThumbnailDTO(
+ thumbnail_url=ApiDependencies.invoker.services.workflow_thumbnails.get_url(workflow.workflow_id),
+ **workflow.model_dump(),
+ )
+ )
+ return PaginatedResults[WorkflowRecordListItemWithThumbnailDTO](
+ items=workflows_with_thumbnails,
+ total=workflows.total,
+ page=workflows.page,
+ pages=workflows.pages,
+ per_page=workflows.per_page,
+ )
+
+
+@workflows_router.put(
+ "/i/{workflow_id}/thumbnail",
+ operation_id="set_workflow_thumbnail",
+ responses={
+ 200: {"model": WorkflowRecordDTO},
+ },
+)
+async def set_workflow_thumbnail(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to update"),
+ image: UploadFile = File(description="The image file to upload"),
+):
+ """Sets a workflow's thumbnail image"""
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser and not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+
+ if not image.content_type or not image.content_type.startswith("image"):
+ raise HTTPException(status_code=415, detail="Not an image")
+
+ contents = await image.read()
+ try:
+ pil_image = Image.open(io.BytesIO(contents))
+
+ except Exception:
+ ApiDependencies.invoker.services.logger.error(traceback.format_exc())
+ raise HTTPException(status_code=415, detail="Failed to read image")
+
+ try:
+ ApiDependencies.invoker.services.workflow_thumbnails.save(workflow_id, pil_image)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@workflows_router.delete(
+ "/i/{workflow_id}/thumbnail",
+ operation_id="delete_workflow_thumbnail",
+ responses={
+ 200: {"model": WorkflowRecordDTO},
+ },
+)
+async def delete_workflow_thumbnail(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to update"),
+):
+ """Removes a workflow's thumbnail image"""
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser and not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+
+ try:
+ ApiDependencies.invoker.services.workflow_thumbnails.delete(workflow_id)
+ except ValueError as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@workflows_router.get(
+ "/i/{workflow_id}/thumbnail",
+ operation_id="get_workflow_thumbnail",
+ responses={
+ 200: {
+ "description": "The workflow thumbnail was fetched successfully",
+ },
+ 400: {"description": "Bad request"},
+ 404: {"description": "The workflow thumbnail could not be found"},
+ },
+ status_code=200,
+)
+async def get_workflow_thumbnail(
+ workflow_id: str = Path(description="The id of the workflow thumbnail to get"),
+) -> FileResponse:
+ """Gets a workflow's thumbnail image.
+
+ This endpoint is intentionally unauthenticated because browsers load images
+ via tags which cannot send Bearer tokens. Workflow IDs are UUIDs,
+ providing security through unguessability.
+ """
+ try:
+ path = ApiDependencies.invoker.services.workflow_thumbnails.get_path(workflow_id)
+
+ response = FileResponse(
+ path,
+ media_type="image/png",
+ filename=workflow_id + ".png",
+ content_disposition_type="inline",
+ )
+ response.headers["Cache-Control"] = f"max-age={IMAGE_MAX_AGE}"
+ return response
+ except Exception:
+ raise HTTPException(status_code=404)
+
+
+@workflows_router.patch(
+ "/i/{workflow_id}/is_public",
+ operation_id="update_workflow_is_public",
+ responses={
+ 200: {"model": WorkflowRecordDTO},
+ },
+)
+async def update_workflow_is_public(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to update"),
+ is_public: bool = Body(description="Whether the workflow should be shared publicly", embed=True),
+) -> WorkflowRecordDTO:
+ """Updates whether a workflow is shared publicly"""
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser and not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+
+ user_id = None if current_user.is_admin else current_user.user_id
+ return ApiDependencies.invoker.services.workflow_records.update_is_public(
+ workflow_id=workflow_id, is_public=is_public, user_id=user_id
+ )
+
+
+@workflows_router.get("/tags", operation_id="get_all_tags")
+async def get_all_tags(
+ current_user: CurrentUserOrDefault,
+ categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories to include"),
+ is_public: Optional[bool] = Query(default=None, description="Filter by public/shared status"),
+) -> list[str]:
+ """Gets all unique tags from workflows"""
+ config = ApiDependencies.invoker.services.configuration
+ user_id_filter: Optional[str] = None
+ if config.multiuser and not current_user.is_admin:
+ has_user_category = not categories or WorkflowCategory.User in categories
+ if has_user_category and is_public is not True:
+ user_id_filter = current_user.user_id
+
+ return ApiDependencies.invoker.services.workflow_records.get_all_tags(
+ categories=categories, user_id=user_id_filter, is_public=is_public
+ )
+
+
+@workflows_router.get("/counts_by_tag", operation_id="get_counts_by_tag")
+async def get_counts_by_tag(
+ current_user: CurrentUserOrDefault,
+ tags: list[str] = Query(description="The tags to get counts for"),
+ categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories to include"),
+ has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
+ is_public: Optional[bool] = Query(default=None, description="Filter by public/shared status"),
+) -> dict[str, int]:
+ """Counts workflows by tag"""
+ config = ApiDependencies.invoker.services.configuration
+ user_id_filter: Optional[str] = None
+ if config.multiuser and not current_user.is_admin:
+ has_user_category = not categories or WorkflowCategory.User in categories
+ if has_user_category and is_public is not True:
+ user_id_filter = current_user.user_id
+
+ return ApiDependencies.invoker.services.workflow_records.counts_by_tag(
+ tags=tags, categories=categories, has_been_opened=has_been_opened, user_id=user_id_filter, is_public=is_public
+ )
+
+
+@workflows_router.get("/counts_by_category", operation_id="counts_by_category")
+async def counts_by_category(
+ current_user: CurrentUserOrDefault,
+ categories: list[WorkflowCategory] = Query(description="The categories to include"),
+ has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
+ is_public: Optional[bool] = Query(default=None, description="Filter by public/shared status"),
+) -> dict[str, int]:
+ """Counts workflows by category"""
+ config = ApiDependencies.invoker.services.configuration
+ user_id_filter: Optional[str] = None
+ if config.multiuser and not current_user.is_admin:
+ has_user_category = WorkflowCategory.User in categories
+ if has_user_category and is_public is not True:
+ user_id_filter = current_user.user_id
+
+ return ApiDependencies.invoker.services.workflow_records.counts_by_category(
+ categories=categories, has_been_opened=has_been_opened, user_id=user_id_filter, is_public=is_public
+ )
+
+
+@workflows_router.put(
+ "/i/{workflow_id}/opened_at",
+ operation_id="update_opened_at",
+)
+async def update_opened_at(
+ current_user: CurrentUserOrDefault,
+ workflow_id: str = Path(description="The workflow to update"),
+) -> None:
+ """Updates the opened_at field of a workflow"""
+ try:
+ existing = ApiDependencies.invoker.services.workflow_records.get(workflow_id)
+ except WorkflowNotFoundError:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+
+ config = ApiDependencies.invoker.services.configuration
+ if config.multiuser and not current_user.is_admin and existing.user_id != current_user.user_id:
+ raise HTTPException(status_code=403, detail="Not authorized to update this workflow")
+
+ user_id = None if current_user.is_admin else current_user.user_id
+ ApiDependencies.invoker.services.workflow_records.update_opened_at(workflow_id, user_id=user_id)
diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py
new file mode 100644
index 00000000000..5783b804c0b
--- /dev/null
+++ b/invokeai/app/api/sockets.py
@@ -0,0 +1,362 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+from typing import Any
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+from socketio import ASGIApp, AsyncServer
+
+from invokeai.app.services.auth.token_service import verify_token
+from invokeai.app.services.events.events_common import (
+ BatchEnqueuedEvent,
+ BulkDownloadCompleteEvent,
+ BulkDownloadErrorEvent,
+ BulkDownloadEventBase,
+ BulkDownloadStartedEvent,
+ DownloadCancelledEvent,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadEventBase,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+ FastAPIEvent,
+ InvocationCompleteEvent,
+ InvocationErrorEvent,
+ InvocationProgressEvent,
+ InvocationStartedEvent,
+ ModelEventBase,
+ ModelInstallCancelledEvent,
+ ModelInstallCompleteEvent,
+ ModelInstallDownloadProgressEvent,
+ ModelInstallDownloadsCompleteEvent,
+ ModelInstallErrorEvent,
+ ModelInstallStartedEvent,
+ ModelLoadCompleteEvent,
+ ModelLoadStartedEvent,
+ QueueClearedEvent,
+ QueueEventBase,
+ QueueItemStatusChangedEvent,
+ RecallParametersUpdatedEvent,
+ register_events,
+)
+from invokeai.backend.util.logging import InvokeAILogger
+
+logger = InvokeAILogger.get_logger()
+
+
+class QueueSubscriptionEvent(BaseModel):
+ """Event data for subscribing to the socket.io queue room.
+ This is a pydantic model to ensure the data is in the correct format."""
+
+ queue_id: str
+
+
+class BulkDownloadSubscriptionEvent(BaseModel):
+ """Event data for subscribing to the socket.io bulk downloads room.
+ This is a pydantic model to ensure the data is in the correct format."""
+
+ bulk_download_id: str
+
+
+QUEUE_EVENTS = {
+ InvocationStartedEvent,
+ InvocationProgressEvent,
+ InvocationCompleteEvent,
+ InvocationErrorEvent,
+ QueueItemStatusChangedEvent,
+ BatchEnqueuedEvent,
+ QueueClearedEvent,
+ RecallParametersUpdatedEvent,
+}
+
+MODEL_EVENTS = {
+ DownloadCancelledEvent,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+ ModelLoadStartedEvent,
+ ModelLoadCompleteEvent,
+ ModelInstallDownloadProgressEvent,
+ ModelInstallDownloadsCompleteEvent,
+ ModelInstallStartedEvent,
+ ModelInstallCompleteEvent,
+ ModelInstallCancelledEvent,
+ ModelInstallErrorEvent,
+}
+
+BULK_DOWNLOAD_EVENTS = {BulkDownloadStartedEvent, BulkDownloadCompleteEvent, BulkDownloadErrorEvent}
+
+
+class SocketIO:
+ _sub_queue = "subscribe_queue"
+ _unsub_queue = "unsubscribe_queue"
+
+ _sub_bulk_download = "subscribe_bulk_download"
+ _unsub_bulk_download = "unsubscribe_bulk_download"
+
+ def __init__(self, app: FastAPI):
+ self._sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*")
+ self._app = ASGIApp(socketio_server=self._sio, socketio_path="/ws/socket.io")
+ app.mount("/ws", self._app)
+
+ # Track user information for each socket connection
+ self._socket_users: dict[str, dict[str, Any]] = {}
+
+ # Set up authentication middleware
+ self._sio.on("connect", handler=self._handle_connect)
+ self._sio.on("disconnect", handler=self._handle_disconnect)
+
+ self._sio.on(self._sub_queue, handler=self._handle_sub_queue)
+ self._sio.on(self._unsub_queue, handler=self._handle_unsub_queue)
+ self._sio.on(self._sub_bulk_download, handler=self._handle_sub_bulk_download)
+ self._sio.on(self._unsub_bulk_download, handler=self._handle_unsub_bulk_download)
+
+ register_events(QUEUE_EVENTS, self._handle_queue_event)
+ register_events(MODEL_EVENTS, self._handle_model_event)
+ register_events(BULK_DOWNLOAD_EVENTS, self._handle_bulk_image_download_event)
+
+ async def _handle_connect(self, sid: str, environ: dict, auth: dict | None) -> bool:
+ """Handle socket connection and authenticate the user.
+
+ Returns True to accept the connection, False to reject it.
+ Stores user_id in the internal socket users dict for later use.
+
+ In multiuser mode, connections without a valid token are rejected outright
+ so that anonymous clients cannot subscribe to queue rooms and observe
+ queue activity belonging to other users. In single-user mode, unauthenticated
+ connections are accepted as the system admin user.
+ """
+ # Extract token from auth data or headers
+ token = None
+ if auth and isinstance(auth, dict):
+ token = auth.get("token")
+
+ if not token and environ:
+ # Try to get token from headers
+ headers = environ.get("HTTP_AUTHORIZATION", "")
+ if headers.startswith("Bearer "):
+ token = headers[7:]
+
+ # Verify the token
+ if token:
+ token_data = verify_token(token)
+ if token_data:
+ # In multiuser mode, also verify the backing user record still
+ # exists and is active — mirrors the REST auth check in
+ # auth_dependencies.py. A deleted or deactivated user whose
+ # JWT has not yet expired must not be allowed to open a socket.
+ if self._is_multiuser_enabled():
+ try:
+ from invokeai.app.api.dependencies import ApiDependencies
+
+ user = ApiDependencies.invoker.services.users.get(token_data.user_id)
+ if user is None or not user.is_active:
+ logger.warning(f"Rejecting socket {sid}: user {token_data.user_id} not found or inactive")
+ return False
+ except Exception:
+ # If user service is unavailable, fail closed
+ logger.warning(f"Rejecting socket {sid}: unable to verify user record")
+ return False
+
+ # Store user_id and is_admin in socket users dict
+ self._socket_users[sid] = {
+ "user_id": token_data.user_id,
+ "is_admin": token_data.is_admin,
+ }
+ logger.info(
+ f"Socket {sid} connected with user_id: {token_data.user_id}, is_admin: {token_data.is_admin}"
+ )
+ return True
+
+ # No valid token provided. In multiuser mode this is not allowed — reject
+ # the connection so anonymous clients cannot subscribe to queue rooms.
+ # In single-user mode, fall through and accept the socket as system admin.
+ if self._is_multiuser_enabled():
+ logger.warning(
+ f"Rejecting socket {sid} connection: multiuser mode is enabled and no valid auth token was provided"
+ )
+ return False
+
+ self._socket_users[sid] = {
+ "user_id": "system",
+ "is_admin": True,
+ }
+ logger.debug(f"Socket {sid} connected as system admin (single-user mode)")
+ return True
+
+ @staticmethod
+ def _is_multiuser_enabled() -> bool:
+ """Check whether multiuser mode is enabled. Fails closed if configuration
+ is not yet initialized, which should not happen in practice but prevents
+ accidentally opening the socket during startup races."""
+ try:
+ # Imported here to avoid a circular import at module load time.
+ from invokeai.app.api.dependencies import ApiDependencies
+
+ return bool(ApiDependencies.invoker.services.configuration.multiuser)
+ except Exception:
+ # If dependencies are not initialized, fail closed (treat as multiuser)
+ # so we never accidentally admit an anonymous socket.
+ return True
+
+ async def _handle_disconnect(self, sid: str) -> None:
+ """Handle socket disconnection and cleanup user info."""
+ if sid in self._socket_users:
+ del self._socket_users[sid]
+ logger.debug(f"Socket {sid} disconnected and cleaned up")
+
+ async def _handle_sub_queue(self, sid: str, data: Any) -> None:
+ """Handle queue subscription and add socket to both queue and user-specific rooms."""
+ queue_id = QueueSubscriptionEvent(**data).queue_id
+
+ # Check if we have user info for this socket. In multiuser mode _handle_connect
+ # will have already rejected any socket without a valid token, so missing user
+ # info here is a bug — refuse the subscription rather than silently falling back
+ # to an anonymous system user who could then receive queue item events.
+ if sid not in self._socket_users:
+ if self._is_multiuser_enabled():
+ logger.warning(
+ f"Refusing queue subscription for socket {sid}: no user info (socket not authenticated via connect event)"
+ )
+ return
+ # Single-user mode: safe to fall back to the system admin user.
+ self._socket_users[sid] = {
+ "user_id": "system",
+ "is_admin": True,
+ }
+
+ user_id = self._socket_users[sid]["user_id"]
+ is_admin = self._socket_users[sid]["is_admin"]
+
+ # Add socket to the queue room
+ await self._sio.enter_room(sid, queue_id)
+
+ # Also add socket to a user-specific room for event filtering
+ user_room = f"user:{user_id}"
+ await self._sio.enter_room(sid, user_room)
+
+ # If admin, also add to admin room to receive all events
+ if is_admin:
+ await self._sio.enter_room(sid, "admin")
+
+ logger.debug(
+ f"Socket {sid} (user_id: {user_id}, is_admin: {is_admin}) subscribed to queue {queue_id} and user room {user_room}"
+ )
+
+ async def _handle_unsub_queue(self, sid: str, data: Any) -> None:
+ await self._sio.leave_room(sid, QueueSubscriptionEvent(**data).queue_id)
+
+ async def _handle_sub_bulk_download(self, sid: str, data: Any) -> None:
+ # In multiuser mode, only allow authenticated sockets to subscribe.
+ # Bulk download events are routed to user-specific rooms, so the
+ # bulk_download_id room subscription is only kept for single-user
+ # backward compatibility.
+ if self._is_multiuser_enabled() and sid not in self._socket_users:
+ logger.warning(f"Refusing bulk download subscription for unknown socket {sid} in multiuser mode")
+ return
+ await self._sio.enter_room(sid, BulkDownloadSubscriptionEvent(**data).bulk_download_id)
+
+ async def _handle_unsub_bulk_download(self, sid: str, data: Any) -> None:
+ await self._sio.leave_room(sid, BulkDownloadSubscriptionEvent(**data).bulk_download_id)
+
+ async def _handle_queue_event(self, event: FastAPIEvent[QueueEventBase]):
+ """Handle queue events with user isolation.
+
+ All queue item events (invocation events AND QueueItemStatusChangedEvent) are
+ private to the owning user and admins. They carry unsanitized user_id, batch_id,
+ session_id, origin, destination and error metadata, and must never be broadcast
+ to the whole queue room — otherwise any other authenticated subscriber could
+ observe cross-user queue activity.
+
+ RecallParametersUpdatedEvent is also private to the owner + admins.
+
+ BatchEnqueuedEvent carries the enqueuing user's batch_id/origin/counts and
+ is also routed privately. QueueClearedEvent is the only queue event that
+ is still broadcast to the whole queue room.
+
+ IMPORTANT: Check InvocationEventBase BEFORE QueueItemEventBase since InvocationEventBase
+ inherits from QueueItemEventBase. The order of isinstance checks matters!
+ """
+ try:
+ event_name, event_data = event
+
+ # Import here to avoid circular dependency
+ from invokeai.app.services.events.events_common import InvocationEventBase, QueueItemEventBase
+
+ # Check InvocationEventBase FIRST (before QueueItemEventBase) since it's a subclass
+ # Invocation events (progress, started, complete, error) are private to owner + admins
+ if isinstance(event_data, InvocationEventBase) and hasattr(event_data, "user_id"):
+ user_room = f"user:{event_data.user_id}"
+
+ # Emit to the user's room
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
+
+ # Also emit to admin room so admins can see all events, but strip image preview data
+ # from InvocationProgressEvent to prevent admins from seeing other users' image content
+ if isinstance(event_data, InvocationProgressEvent):
+ admin_event_data = event_data.model_copy(update={"image": None})
+ await self._sio.emit(event=event_name, data=admin_event_data.model_dump(mode="json"), room="admin")
+ else:
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+
+ logger.debug(f"Emitted private invocation event {event_name} to user room {user_room} and admin room")
+
+ # Other queue item events (QueueItemStatusChangedEvent) carry unsanitized
+ # user_id, batch_id, session_id, origin, destination and error metadata.
+ # They are private to the owning user + admins — never broadcast to the
+ # full queue room.
+ elif isinstance(event_data, QueueItemEventBase) and hasattr(event_data, "user_id"):
+ user_room = f"user:{event_data.user_id}"
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+
+ logger.debug(f"Emitted private queue item event {event_name} to user room {user_room} and admin room")
+
+ # RecallParametersUpdatedEvent is private - only emit to owner + admins
+ elif isinstance(event_data, RecallParametersUpdatedEvent):
+ user_room = f"user:{event_data.user_id}"
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+ logger.debug(f"Emitted private recall_parameters_updated event to user room {user_room} and admin room")
+
+ # BatchEnqueuedEvent carries the enqueuing user's batch_id, origin, and
+ # enqueued counts. Route it privately to the owner + admins so other
+ # users do not observe cross-user batch activity.
+ elif isinstance(event_data, BatchEnqueuedEvent):
+ user_room = f"user:{event_data.user_id}"
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+ logger.debug(f"Emitted private batch_enqueued event to user room {user_room} and admin room")
+
+ else:
+ # For remaining queue events (e.g. QueueClearedEvent) that do not
+ # carry user identity, emit to all subscribers in the queue room.
+ await self._sio.emit(
+ event=event_name, data=event_data.model_dump(mode="json"), room=event_data.queue_id
+ )
+ logger.debug(
+ f"Emitted general queue event {event_name} to all subscribers in queue {event_data.queue_id}"
+ )
+ except Exception as e:
+ # Log any unhandled exceptions in event handling to prevent silent failures
+ logger.error(f"Error handling queue event {event[0]}: {e}", exc_info=True)
+
+ async def _handle_model_event(self, event: FastAPIEvent[ModelEventBase | DownloadEventBase]) -> None:
+ await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"))
+
+ async def _handle_bulk_image_download_event(self, event: FastAPIEvent[BulkDownloadEventBase]) -> None:
+ event_name, event_data = event
+ # Route to user-specific + admin rooms so that other authenticated
+ # users cannot learn the bulk_download_item_name (the capability token
+ # needed to fetch the zip from the unauthenticated GET endpoint).
+ # In single-user mode (user_id="system"), fall back to the shared
+ # bulk_download_id room for backward compatibility.
+ if hasattr(event_data, "user_id") and event_data.user_id != "system":
+ user_room = f"user:{event_data.user_id}"
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
+ await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
+ else:
+ await self._sio.emit(
+ event=event_name, data=event_data.model_dump(mode="json"), room=event_data.bulk_download_id
+ )
diff --git a/invokeai/app/api_app.py b/invokeai/app/api_app.py
new file mode 100644
index 00000000000..4b79e1eeb0c
--- /dev/null
+++ b/invokeai/app/api_app.py
@@ -0,0 +1,227 @@
+import asyncio
+import logging
+from contextlib import asynccontextmanager
+from pathlib import Path
+
+from fastapi import FastAPI, Request
+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.responses import HTMLResponse, RedirectResponse
+from fastapi_events.handlers.local import local_handler
+from fastapi_events.middleware import EventHandlerASGIMiddleware
+from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+
+import invokeai.frontend.web as web_dir
+from invokeai.app.api.dependencies import ApiDependencies
+from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
+from invokeai.app.api.routers import (
+ app_info,
+ auth,
+ board_images,
+ boards,
+ client_state,
+ custom_nodes,
+ download_queue,
+ images,
+ model_manager,
+ model_relationships,
+ recall_parameters,
+ session_queue,
+ style_presets,
+ utilities,
+ virtual_boards,
+ workflows,
+)
+from invokeai.app.api.sockets import SocketIO
+from invokeai.app.services.config.config_default import get_config
+from invokeai.app.util.custom_openapi import get_openapi_func
+from invokeai.backend.util.logging import InvokeAILogger
+
+app_config = get_config()
+logger = InvokeAILogger.get_logger(config=app_config)
+
+loop = asyncio.new_event_loop()
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # Add startup event to load dependencies
+ ApiDependencies.initialize(config=app_config, event_handler_id=event_handler_id, loop=loop, logger=logger)
+
+ # Log the server address when it starts - in case the network log level is not high enough to see the startup log
+ proto = "https" if app_config.ssl_certfile else "http"
+ msg = f"Invoke running on {proto}://{app_config.host}:{app_config.port} (Press CTRL+C to quit)"
+
+ # Logging this way ignores the logger's log level and _always_ logs the message
+ record = logger.makeRecord(
+ name=logger.name,
+ level=logging.INFO,
+ fn="",
+ lno=0,
+ msg=msg,
+ args=(),
+ exc_info=None,
+ )
+ logger.handle(record)
+
+ yield
+ # Shut down threads
+ ApiDependencies.shutdown()
+
+
+# Create the app
+# TODO: create this all in a method so configuration/etc. can be passed in?
+app = FastAPI(
+ title="Invoke - Community Edition",
+ docs_url=None,
+ redoc_url=None,
+ separate_input_output_schemas=False,
+ lifespan=lifespan,
+)
+
+
+class SlidingWindowTokenMiddleware(BaseHTTPMiddleware):
+ """Refresh the JWT token on each authenticated response.
+
+ When a request includes a valid Bearer token, the response includes a
+ X-Refreshed-Token header with a new token that has a fresh expiry.
+ This implements sliding-window session expiry: the session only expires
+ after a period of *inactivity*, not a fixed time after login.
+ """
+
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+ response = await call_next(request)
+
+ # Only refresh on mutating requests (POST/PUT/PATCH/DELETE) — these indicate
+ # genuine user activity. GET requests are often background fetches (RTK Query
+ # cache revalidation, refetch-on-focus, etc.) and should not reset the
+ # inactivity timer.
+ if response.status_code < 400 and request.method in ("POST", "PUT", "PATCH", "DELETE"):
+ auth_header = request.headers.get("authorization", "")
+ if auth_header.startswith("Bearer "):
+ token = auth_header[7:]
+ try:
+ from datetime import timedelta
+
+ from invokeai.app.api.routers.auth import TOKEN_EXPIRATION_NORMAL, TOKEN_EXPIRATION_REMEMBER_ME
+ from invokeai.app.services.auth.token_service import create_access_token, verify_token
+
+ token_data = verify_token(token)
+ if token_data is not None:
+ # Use the remember_me claim from the token to determine the
+ # correct refresh duration. This avoids the bug where a 7-day
+ # token with <24h remaining would be silently downgraded to 1 day.
+ if token_data.remember_me:
+ expires_delta = timedelta(days=TOKEN_EXPIRATION_REMEMBER_ME)
+ else:
+ expires_delta = timedelta(days=TOKEN_EXPIRATION_NORMAL)
+
+ new_token = create_access_token(token_data, expires_delta)
+ response.headers["X-Refreshed-Token"] = new_token
+ except Exception:
+ pass # Don't fail the request if token refresh fails
+
+ return response
+
+
+class RedirectRootWithQueryStringMiddleware(BaseHTTPMiddleware):
+ """When a request is made to the root path with a query string, redirect to the root path without the query string.
+
+ For example, to force a Gradio app to use dark mode, users may append `?__theme=dark` to the URL. Their browser may
+ have this query string saved in history or a bookmark, so when the user navigates to `http://127.0.0.1:9090/`, the
+ browser takes them to `http://127.0.0.1:9090/?__theme=dark`.
+
+ This breaks the static file serving in the UI, so we redirect the user to the root path without the query string.
+ """
+
+ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
+ if request.url.path == "/" and request.url.query:
+ return RedirectResponse(url="/")
+
+ response = await call_next(request)
+ return response
+
+
+# Add the middleware
+app.add_middleware(RedirectRootWithQueryStringMiddleware)
+app.add_middleware(SlidingWindowTokenMiddleware)
+
+
+# Add event handler
+event_handler_id: int = id(app)
+app.add_middleware(
+ EventHandlerASGIMiddleware,
+ handlers=[local_handler], # TODO: consider doing this in services to support different configurations
+ middleware_id=event_handler_id,
+)
+
+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,
+ expose_headers=["X-Refreshed-Token"],
+)
+
+app.add_middleware(GZipMiddleware, minimum_size=1000)
+
+
+# Include all routers
+# Authentication router should be first so it's registered before protected routes
+app.include_router(auth.auth_router, prefix="/api")
+app.include_router(utilities.utilities_router, prefix="/api")
+app.include_router(model_manager.model_manager_router, prefix="/api")
+app.include_router(download_queue.download_queue_router, prefix="/api")
+app.include_router(images.images_router, prefix="/api")
+app.include_router(boards.boards_router, prefix="/api")
+app.include_router(board_images.board_images_router, prefix="/api")
+app.include_router(virtual_boards.virtual_boards_router, prefix="/api")
+app.include_router(model_relationships.model_relationships_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(workflows.workflows_router, prefix="/api")
+app.include_router(style_presets.style_presets_router, prefix="/api")
+app.include_router(client_state.client_state_router, prefix="/api")
+app.include_router(recall_parameters.recall_parameters_router, prefix="/api")
+app.include_router(custom_nodes.custom_nodes_router, prefix="/api")
+
+app.openapi = get_openapi_func(app)
+
+
+@app.get("/docs", include_in_schema=False)
+def overridden_swagger() -> HTMLResponse:
+ return get_swagger_ui_html(
+ openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string
+ title=f"{app.title} - Swagger UI",
+ swagger_favicon_url="static/docs/invoke-favicon-docs.svg",
+ )
+
+
+@app.get("/redoc", include_in_schema=False)
+def overridden_redoc() -> HTMLResponse:
+ return get_redoc_html(
+ openapi_url=app.openapi_url, # type: ignore [arg-type] # this is always a string
+ title=f"{app.title} - Redoc",
+ redoc_favicon_url="static/docs/invoke-favicon-docs.svg",
+ )
+
+
+web_root_path = Path(list(web_dir.__path__)[0])
+
+if app_config.unsafe_disable_picklescan:
+ logger.warning(
+ "The unsafe_disable_picklescan option is enabled. This disables malware scanning while installing and"
+ "loading models, which may allow malicious code to be executed. Use at your own risk."
+ )
+
+try:
+ app.mount("/", NoCacheStaticFiles(directory=Path(web_root_path, "dist"), html=True), name="ui")
+except RuntimeError:
+ logger.warning(f"No UI found at {web_root_path}/dist, skipping UI mount")
+app.mount(
+ "/static", NoCacheStaticFiles(directory=Path(web_root_path, "static/")), name="static"
+) # docs favicon is in here
diff --git a/invokeai/app/assets/images/caution.png b/invokeai/app/assets/images/caution.png
new file mode 100644
index 00000000000..91d43bf86ea
Binary files /dev/null and b/invokeai/app/assets/images/caution.png differ
diff --git a/invokeai/app/invocations/__init__.py b/invokeai/app/invocations/__init__.py
new file mode 100644
index 00000000000..c8d64437524
--- /dev/null
+++ b/invokeai/app/invocations/__init__.py
@@ -0,0 +1,5 @@
+from pathlib import Path
+
+# 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
diff --git a/invokeai/app/invocations/anima_denoise.py b/invokeai/app/invocations/anima_denoise.py
new file mode 100644
index 00000000000..9fa4b3fb07a
--- /dev/null
+++ b/invokeai/app/invocations/anima_denoise.py
@@ -0,0 +1,734 @@
+"""Anima denoising invocation.
+
+Implements the rectified flow denoising loop for Anima models:
+- Direct prediction: denoised = input - output * sigma
+- Fixed shift=3.0 via loglinear_timestep_shift (Flux paper by Black Forest Labs)
+- Timestep convention: timestep = sigma * 1.0 (raw sigma, NOT 1-sigma like Z-Image)
+- NO v-prediction negation (unlike Z-Image)
+- 3D latent space: [B, C, T, H, W] with T=1 for images
+- 16 latent channels, 8x spatial compression
+
+Key differences from Z-Image denoise:
+- Anima uses fixed shift=3.0, Z-Image uses dynamic shift based on resolution
+- Anima: timestep = sigma (raw), Z-Image: model_t = 1.0 - sigma
+- Anima: noise_pred = model_output (direct), Z-Image: noise_pred = -model_output (v-pred)
+- Anima transformer takes (x, timesteps, context, t5xxl_ids, t5xxl_weights)
+- Anima uses 3D latents directly, Z-Image converts 4D -> list of 5D
+"""
+
+import math
+from contextlib import ExitStack
+from typing import Callable, Iterator, Optional, Tuple
+
+import torch
+import torchvision.transforms as tv_transforms
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ AnimaConditioningField,
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+)
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import TransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.anima.anima_transformer_patch import patch_anima_for_regional_prompting
+from invokeai.backend.anima.conditioning_data import AnimaRegionalTextConditioning, AnimaTextConditioning
+from invokeai.backend.anima.regional_prompting import AnimaRegionalPromptingExtension
+from invokeai.backend.anima.scheduler_driver import AnimaSchedulerDriver
+from invokeai.backend.flux.schedulers import (
+ ANIMA_SCHEDULER_LABELS,
+ ANIMA_SCHEDULER_NAME_VALUES,
+ ANIMA_SHIFT,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.anima_lora_constants import ANIMA_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import (
+ RectifiedFlowInpaintExtension,
+ assert_broadcastable,
+)
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import AnimaConditioningInfo, Range
+from invokeai.backend.util.devices import TorchDevice
+
+# Anima uses 8x spatial compression (VAE downsamples by 2^3)
+ANIMA_LATENT_SCALE_FACTOR = 8
+# Anima uses 16 latent channels
+ANIMA_LATENT_CHANNELS = 16
+# Anima uses raw sigma values as timesteps (no rescaling)
+ANIMA_MULTIPLIER = 1.0
+
+
+def loglinear_timestep_shift(alpha: float, t: float) -> float:
+ """Apply log-linear timestep shift to a noise schedule value.
+
+ This shift biases the noise schedule toward higher noise levels, as described
+ in the Flux model (Black Forest Labs, 2024). With alpha > 1, the model spends
+ proportionally more denoising steps at higher noise levels.
+
+ Formula: sigma = alpha * t / (1 + (alpha - 1) * t)
+
+ Args:
+ alpha: Shift factor (3.0 for Anima, resolution-dependent for Flux).
+ t: Timestep value in [0, 1].
+
+ Returns:
+ Shifted timestep value.
+ """
+ if alpha == 1.0:
+ return t
+ return alpha * t / (1 + (alpha - 1) * t)
+
+
+def inverse_loglinear_timestep_shift(alpha: float, sigma: float) -> float:
+ """Recover linear t from a shifted sigma value.
+
+ Inverse of loglinear_timestep_shift: given sigma = alpha * t / (1 + (alpha-1) * t),
+ solve for t = sigma / (alpha - (alpha-1) * sigma).
+
+ This is needed for the inpainting extension, which expects linear t values
+ for gradient mask thresholding. With Anima's shift=3.0, the difference
+ between shifted sigma and linear t is large (e.g. at t=0.5, sigma=0.75),
+ causing overly aggressive mask thresholding if sigma is used directly.
+
+ Args:
+ alpha: Shift factor (3.0 for Anima).
+ sigma: Shifted sigma value in [0, 1].
+
+ Returns:
+ Linear t value in [0, 1].
+ """
+ if alpha == 1.0:
+ return sigma
+ denominator = alpha - (alpha - 1) * sigma
+ if abs(denominator) < 1e-8:
+ return 1.0
+ return sigma / denominator
+
+
+class AnimaInpaintExtension(RectifiedFlowInpaintExtension):
+ """Inpaint extension for Anima that accounts for the time-SNR shift.
+
+ Anima uses a fixed shift=3.0 which makes sigma values significantly larger
+ than the corresponding linear t values. The base RectifiedFlowInpaintExtension
+ uses t_prev for both gradient mask thresholding and noise mixing, which assumes
+ linear t values.
+
+ This subclass:
+ - Uses the LINEAR t for gradient mask thresholding (correct progressive reveal)
+ - Uses the SHIFTED sigma for noise mixing (matches the denoiser's noise level)
+ """
+
+ def __init__(
+ self,
+ init_latents: torch.Tensor,
+ inpaint_mask: torch.Tensor,
+ noise: torch.Tensor,
+ shift: float = ANIMA_SHIFT,
+ ):
+ assert_broadcastable(init_latents.shape, inpaint_mask.shape, noise.shape)
+ self._init_latents = init_latents
+ self._inpaint_mask = inpaint_mask
+ self._noise = noise
+ self._shift = shift
+
+ def merge_intermediate_latents_with_init_latents(
+ self, intermediate_latents: torch.Tensor, sigma_prev: float
+ ) -> torch.Tensor:
+ """Merge intermediate latents with init latents, correcting for Anima's shift.
+
+ Args:
+ intermediate_latents: The denoised latents at the current step.
+ sigma_prev: The SHIFTED sigma value for the next step.
+ """
+ # Recover linear t from shifted sigma for gradient mask thresholding.
+ # This ensures the gradient mask is revealed at the correct pace.
+ t_prev = inverse_loglinear_timestep_shift(self._shift, sigma_prev)
+ mask = self._apply_mask_gradient_adjustment(t_prev)
+
+ # Use shifted sigma for noise mixing to match the denoiser's noise level.
+ # The Euler step produces latents at noise level sigma_prev, so the
+ # preserved regions must also be at sigma_prev noise level.
+ noised_init_latents = self._noise * sigma_prev + (1.0 - sigma_prev) * self._init_latents
+
+ return intermediate_latents * mask + noised_init_latents * (1.0 - mask)
+
+
+@invocation(
+ "anima_denoise",
+ title="Denoise - Anima",
+ tags=["image", "anima"],
+ category="image",
+ version="1.6.0",
+ classification=Classification.Prototype,
+)
+class AnimaDenoiseInvocation(BaseInvocation):
+ """Run the denoising process with an Anima model.
+
+ Uses rectified flow sampling with shift=3.0 and the Cosmos Predict2 DiT
+ backbone with integrated LLM Adapter for text conditioning.
+
+ Supports txt2img, img2img (via latents input), and inpainting (via denoise_mask).
+ """
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.latents, input=Input.Connection
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.noise, input=Input.Connection
+ )
+ # denoise_mask is used for inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None, description=FieldDescriptions.denoise_mask, input=Input.Connection
+ )
+ 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)
+ add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
+ transformer: TransformerField = InputField(
+ description="Anima transformer model.", input=Input.Connection, title="Transformer"
+ )
+ positive_conditioning: AnimaConditioningField | list[AnimaConditioningField] = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: AnimaConditioningField | list[AnimaConditioningField] | None = InputField(
+ default=None, description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ guidance_scale: float = InputField(
+ default=4.5,
+ ge=1.0,
+ description="Guidance scale for classifier-free guidance. Recommended: 4.0-5.0 for Anima.",
+ title="Guidance Scale",
+ )
+ width: int = InputField(default=1024, multiple_of=8, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=8, description="Height of the generated image.")
+ steps: int = InputField(default=30, gt=0, description="Number of denoising steps. 30 recommended for Anima.")
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+ scheduler: ANIMA_SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description="Scheduler (sampler) for the denoising process.",
+ ui_choice_labels=ANIMA_SCHEDULER_LABELS,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ """Prepare the inpaint mask for Anima.
+
+ Anima uses 3D latents [B, C, T, H, W] internally but the mask operates
+ on the spatial dimensions [B, C, H, W] which match the squeezed output.
+ """
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+
+ # Invert mask: 0.0 = regions to denoise, 1.0 = regions to preserve
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask
+
+ def _get_noise(
+ self,
+ height: int,
+ width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ seed: int,
+ ) -> torch.Tensor:
+ """Generate initial noise tensor in 3D latent space [B, C, T, H, W]."""
+ rand_device = "cpu"
+ return torch.randn(
+ 1,
+ ANIMA_LATENT_CHANNELS,
+ 1, # T=1 for single image
+ height // ANIMA_LATENT_SCALE_FACTOR,
+ width // ANIMA_LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=torch.float32,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+ def _get_sigmas(self, num_steps: int) -> list[float]:
+ """Generate sigma schedule with fixed shift=3.0.
+
+ Uses the log-linear timestep shift from the Flux model (Black Forest Labs)
+ with a fixed shift factor of 3.0 (no dynamic resolution-based shift).
+
+ Returns:
+ List of num_steps + 1 sigma values from ~1.0 (noise) to 0.0 (clean).
+ """
+ sigmas = []
+ for i in range(num_steps + 1):
+ t = 1.0 - i / num_steps
+ sigma = loglinear_timestep_shift(ANIMA_SHIFT, t)
+ sigmas.append(sigma)
+ return sigmas
+
+ def _load_conditioning(
+ self,
+ context: InvocationContext,
+ cond_field: AnimaConditioningField,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> AnimaConditioningInfo:
+ """Load Anima conditioning data from storage."""
+ cond_data = context.conditioning.load(cond_field.conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ cond_info = cond_data.conditionings[0]
+ assert isinstance(cond_info, AnimaConditioningInfo)
+ return cond_info.to(dtype=dtype, device=device)
+
+ def _load_text_conditionings(
+ self,
+ context: InvocationContext,
+ cond_field: AnimaConditioningField | list[AnimaConditioningField],
+ img_token_height: int,
+ img_token_width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> list[AnimaTextConditioning]:
+ """Load Anima text conditioning with optional regional masks.
+
+ Args:
+ context: The invocation context.
+ cond_field: Single conditioning field or list of fields.
+ img_token_height: Height of the image token grid (H // patch_size).
+ img_token_width: Width of the image token grid (W // patch_size).
+ dtype: Target dtype.
+ device: Target device.
+
+ Returns:
+ List of AnimaTextConditioning objects with optional masks.
+ """
+ cond_list = cond_field if isinstance(cond_field, list) else [cond_field]
+
+ text_conditionings: list[AnimaTextConditioning] = []
+ for cond in cond_list:
+ cond_info = self._load_conditioning(context, cond, dtype, device)
+
+ # Load the mask, if provided
+ mask: torch.Tensor | None = None
+ if cond.mask is not None:
+ mask = context.tensors.load(cond.mask.tensor_name)
+ mask = mask.to(device=device)
+ mask = AnimaRegionalPromptingExtension.preprocess_regional_prompt_mask(
+ mask, img_token_height, img_token_width, dtype, device
+ )
+
+ text_conditionings.append(
+ AnimaTextConditioning(
+ qwen3_embeds=cond_info.qwen3_embeds,
+ t5xxl_ids=cond_info.t5xxl_ids,
+ t5xxl_weights=cond_info.t5xxl_weights,
+ mask=mask,
+ )
+ )
+
+ return text_conditionings
+
+ def _run_llm_adapter_for_regions(
+ self,
+ transformer,
+ text_conditionings: list[AnimaTextConditioning],
+ dtype: torch.dtype,
+ ) -> AnimaRegionalTextConditioning:
+ """Run the LLM Adapter separately for each regional conditioning and concatenate.
+
+ Args:
+ transformer: The AnimaTransformer instance (must be on device).
+ text_conditionings: List of per-region conditioning data.
+ dtype: Inference dtype.
+
+ Returns:
+ AnimaRegionalTextConditioning with concatenated context and masks.
+ """
+ context_embeds_list: list[torch.Tensor] = []
+ context_ranges: list[Range] = []
+ image_masks: list[torch.Tensor | None] = []
+ cur_len = 0
+
+ for tc in text_conditionings:
+ qwen3_embeds = tc.qwen3_embeds.unsqueeze(0) # (1, seq_len, 1024)
+ t5xxl_ids = tc.t5xxl_ids.unsqueeze(0) # (1, seq_len)
+ t5xxl_weights = None
+ if tc.t5xxl_weights is not None:
+ t5xxl_weights = tc.t5xxl_weights.unsqueeze(0).unsqueeze(-1) # (1, seq_len, 1)
+
+ # Run the LLM Adapter to produce context for this region
+ context = transformer.preprocess_text_embeds(
+ qwen3_embeds.to(dtype=dtype),
+ t5xxl_ids,
+ t5xxl_weights=t5xxl_weights.to(dtype=dtype) if t5xxl_weights is not None else None,
+ )
+ # context shape: (1, 512, 1024) — squeeze batch dim
+ context_2d = context.squeeze(0) # (512, 1024)
+
+ context_embeds_list.append(context_2d)
+ context_ranges.append(Range(start=cur_len, end=cur_len + context_2d.shape[0]))
+ image_masks.append(tc.mask)
+ cur_len += context_2d.shape[0]
+
+ concatenated_context = torch.cat(context_embeds_list, dim=0)
+
+ return AnimaRegionalTextConditioning(
+ context_embeds=concatenated_context,
+ image_masks=image_masks,
+ context_ranges=context_ranges,
+ )
+
+ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
+ device = TorchDevice.choose_torch_device()
+ inference_dtype = TorchDevice.choose_anima_inference_dtype(device)
+
+ if self.denoising_start >= self.denoising_end:
+ raise ValueError(
+ f"denoising_start ({self.denoising_start}) must be less than denoising_end ({self.denoising_end})."
+ )
+
+ transformer_info = context.models.load(self.transformer.transformer)
+
+ # Compute image token grid dimensions for regional prompting
+ # Anima: 8x VAE compression, 2x patch size → 16x total
+ patch_size = 2
+ latent_height = self.height // ANIMA_LATENT_SCALE_FACTOR
+ latent_width = self.width // ANIMA_LATENT_SCALE_FACTOR
+ img_token_height = latent_height // patch_size
+ img_token_width = latent_width // patch_size
+ img_seq_len = img_token_height * img_token_width
+
+ # Load positive conditioning with optional regional masks
+ pos_text_conditionings = self._load_text_conditionings(
+ context=context,
+ cond_field=self.positive_conditioning,
+ img_token_height=img_token_height,
+ img_token_width=img_token_width,
+ dtype=inference_dtype,
+ device=device,
+ )
+ has_regional = len(pos_text_conditionings) > 1 or any(tc.mask is not None for tc in pos_text_conditionings)
+
+ # Load negative conditioning if CFG is enabled
+ do_cfg = not math.isclose(self.guidance_scale, 1.0) and self.negative_conditioning is not None
+ neg_text_conditionings: list[AnimaTextConditioning] | None = None
+ if do_cfg:
+ assert self.negative_conditioning is not None
+ neg_text_conditionings = self._load_text_conditionings(
+ context=context,
+ cond_field=self.negative_conditioning,
+ img_token_height=img_token_height,
+ img_token_width=img_token_width,
+ dtype=inference_dtype,
+ device=device,
+ )
+
+ # Generate sigma schedule
+ sigmas = self._get_sigmas(self.steps)
+
+ # Apply denoising_start and denoising_end clipping (for img2img/inpaint)
+ if self.denoising_start > 0 or self.denoising_end < 1:
+ total_sigmas = len(sigmas)
+ start_idx = int(self.denoising_start * (total_sigmas - 1))
+ end_idx = int(self.denoising_end * (total_sigmas - 1)) + 1
+ sigmas = sigmas[start_idx:end_idx]
+
+ total_steps = len(sigmas) - 1
+
+ # Load input latents if provided (image-to-image)
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+ # Anima denoiser works in 3D: add temporal dim if needed
+ if init_latents.ndim == 4:
+ init_latents = init_latents.unsqueeze(2) # [B, C, H, W] -> [B, C, 1, H, W]
+
+ # Generate initial noise (3D latent: [B, C, T, H, W]).
+ # If noise will never be consumed, avoid validating/loading it.
+ should_ignore_noise = init_latents is not None and not self.add_noise and self.denoise_mask is None
+ noise: torch.Tensor | None
+ if should_ignore_noise:
+ noise = None
+ else:
+ noise = self._prepare_noise_tensor(context, inference_dtype, device)
+
+ # Prepare input latents
+ if init_latents is not None:
+ if self.add_noise:
+ assert noise is not None
+ # Noise the init latents using the first sigma from the clipped
+ # InvokeAI schedule.
+ #
+ # Known limitation: if the selected scheduler later starts from a
+ # different first effective sigma/timestep than sigmas[0], the
+ # img2img preblend below may not match that scheduler exactly.
+ # This is an existing pipeline limitation and affects both
+ # internally generated noise and externally supplied noise.
+ s_0 = sigmas[0]
+ latents = s_0 * noise + (1.0 - s_0) * init_latents
+ else:
+ latents = init_latents
+ else:
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ assert noise is not None
+ latents = noise
+
+ if total_steps <= 0:
+ return latents.squeeze(2)
+
+ # Prepare inpaint extension
+ inpaint_mask = self._prep_inpaint_mask(context, latents.squeeze(2))
+ inpaint_extension: AnimaInpaintExtension | None = None
+ if inpaint_mask is not None:
+ if init_latents is None:
+ raise ValueError("Initial latents are required when using an inpaint mask (image-to-image inpainting)")
+ assert noise is not None
+ inpaint_extension = AnimaInpaintExtension(
+ init_latents=init_latents.squeeze(2),
+ inpaint_mask=inpaint_mask,
+ noise=noise.squeeze(2),
+ shift=ANIMA_SHIFT,
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ # Initialize scheduler driver if not using built-in Euler.
+ use_scheduler = self.scheduler != "euler"
+ driver: AnimaSchedulerDriver | None = None
+ if use_scheduler:
+ driver = AnimaSchedulerDriver(
+ scheduler_name=self.scheduler,
+ sigmas=sigmas,
+ steps=self.steps,
+ denoising_start=self.denoising_start,
+ denoising_end=self.denoising_end,
+ device=device,
+ seed=self.seed,
+ )
+
+ with ExitStack() as exit_stack:
+ (cached_weights, transformer) = exit_stack.enter_context(transformer_info.model_on_device())
+
+ # Apply LoRA models to the transformer.
+ # Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=transformer,
+ patches=self._lora_iterator(context),
+ prefix=ANIMA_LORA_TRANSFORMER_PREFIX,
+ dtype=inference_dtype,
+ cached_weights=cached_weights,
+ )
+ )
+
+ # Run LLM Adapter for each regional conditioning to produce context vectors.
+ # This must happen with the transformer on device since it uses the adapter weights.
+ if has_regional:
+ pos_regional = self._run_llm_adapter_for_regions(transformer, pos_text_conditionings, inference_dtype)
+ pos_context = pos_regional.context_embeds.unsqueeze(0) # (1, total_ctx_len, 1024)
+
+ # Build regional prompting extension with cross-attention mask
+ regional_extension = AnimaRegionalPromptingExtension.from_regional_conditioning(
+ pos_regional, img_seq_len
+ )
+
+ # For negative, concatenate all regions without masking (matches Z-Image behavior)
+ neg_context = None
+ if do_cfg and neg_text_conditionings is not None:
+ neg_regional = self._run_llm_adapter_for_regions(
+ transformer, neg_text_conditionings, inference_dtype
+ )
+ neg_context = neg_regional.context_embeds.unsqueeze(0)
+ else:
+ # Single conditioning — run LLM Adapter via normal forward path
+ tc = pos_text_conditionings[0]
+ pos_qwen3_embeds = tc.qwen3_embeds.unsqueeze(0)
+ pos_t5xxl_ids = tc.t5xxl_ids.unsqueeze(0)
+ pos_t5xxl_weights = None
+ if tc.t5xxl_weights is not None:
+ pos_t5xxl_weights = tc.t5xxl_weights.unsqueeze(0).unsqueeze(-1)
+
+ # Pre-compute context via LLM Adapter
+ pos_context = transformer.preprocess_text_embeds(
+ pos_qwen3_embeds.to(dtype=inference_dtype),
+ pos_t5xxl_ids,
+ t5xxl_weights=pos_t5xxl_weights.to(dtype=inference_dtype)
+ if pos_t5xxl_weights is not None
+ else None,
+ )
+
+ neg_context = None
+ if do_cfg and neg_text_conditionings is not None:
+ ntc = neg_text_conditionings[0]
+ neg_qwen3 = ntc.qwen3_embeds.unsqueeze(0)
+ neg_ids = ntc.t5xxl_ids.unsqueeze(0)
+ neg_weights = None
+ if ntc.t5xxl_weights is not None:
+ neg_weights = ntc.t5xxl_weights.unsqueeze(0).unsqueeze(-1)
+ neg_context = transformer.preprocess_text_embeds(
+ neg_qwen3.to(dtype=inference_dtype),
+ neg_ids,
+ t5xxl_weights=neg_weights.to(dtype=inference_dtype) if neg_weights is not None else None,
+ )
+
+ regional_extension = None
+
+ # Apply regional prompting patch if we have regional masks
+ exit_stack.enter_context(patch_anima_for_regional_prompting(transformer, regional_extension))
+
+ # Helper to run transformer with pre-computed context (bypasses LLM Adapter)
+ def _run_transformer(ctx: torch.Tensor, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
+ return transformer(
+ x=x.to(transformer.dtype if hasattr(transformer, "dtype") else inference_dtype),
+ timesteps=t,
+ context=ctx,
+ # t5xxl_ids=None skips the LLM Adapter — context is already pre-computed
+ )
+
+ if driver is not None:
+ user_step = 0
+ pbar = tqdm(total=total_steps, desc="Denoising (Anima)")
+ for it in driver.iterations():
+ timestep = torch.tensor(
+ [it.sigma_curr * ANIMA_MULTIPLIER], device=device, dtype=inference_dtype
+ ).expand(latents.shape[0])
+
+ noise_pred_cond = _run_transformer(pos_context, latents, timestep).float()
+
+ if do_cfg and neg_context is not None:
+ noise_pred_uncond = _run_transformer(neg_context, latents, timestep).float()
+ noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ latents_preview = self._estimate_preview_latents(
+ latents=latents,
+ sigma=it.sigma_curr,
+ noise_pred=noise_pred,
+ )
+
+ latents = driver.step(model_output=noise_pred, timestep=it.sched_timestep, sample=latents)
+
+ if it.completes_user_step:
+ # RectifiedFlowInpaintExtension expects this once per user step (its
+ # docstring), so for Heun we skip the FO half of each pair to avoid
+ # corrupting the second-order corrector's input.
+ if inpaint_extension is not None:
+ latents_4d = latents.squeeze(2)
+ latents_4d = inpaint_extension.merge_intermediate_latents_with_init_latents(
+ latents_4d, it.sigma_prev
+ )
+ latents = latents_4d.unsqueeze(2)
+
+ user_step += 1
+ pbar.update(1)
+ step_callback(
+ PipelineIntermediateState(
+ step=user_step,
+ order=it.order,
+ total_steps=total_steps,
+ timestep=int(it.sigma_curr * 1000),
+ latents=latents_preview.squeeze(2),
+ )
+ )
+ pbar.close()
+ else:
+ # Built-in Euler implementation (default for Anima)
+ for step_idx in tqdm(range(total_steps), desc="Denoising (Anima)"):
+ sigma_curr = sigmas[step_idx]
+ sigma_prev = sigmas[step_idx + 1]
+
+ timestep = torch.tensor(
+ [sigma_curr * ANIMA_MULTIPLIER], device=device, dtype=inference_dtype
+ ).expand(latents.shape[0])
+
+ noise_pred_cond = _run_transformer(pos_context, latents, timestep).float()
+
+ if do_cfg and neg_context is not None:
+ noise_pred_uncond = _run_transformer(neg_context, latents, timestep).float()
+ noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ latents_dtype = latents.dtype
+ latents = latents.to(dtype=torch.float32)
+ latents = latents + (sigma_prev - sigma_curr) * noise_pred
+ latents = latents.to(dtype=latents_dtype)
+ latents_preview = self._estimate_preview_latents(
+ latents=latents, sigma=sigma_prev, noise_pred=noise_pred
+ )
+
+ if inpaint_extension is not None:
+ latents_4d = latents.squeeze(2)
+ latents_4d = inpaint_extension.merge_intermediate_latents_with_init_latents(
+ latents_4d, sigma_prev
+ )
+ latents = latents_4d.unsqueeze(2)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(sigma_curr * 1000),
+ latents=latents_preview.squeeze(2),
+ ),
+ )
+
+ # Remove temporal dimension for output: [B, C, 1, H, W] -> [B, C, H, W]
+ return latents.squeeze(2)
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "Anima", self.width, self.height)
+ return noise
+
+ return self._get_noise(self.height, self.width, inference_dtype, device, self.seed)
+
+ def _estimate_preview_latents(self, latents: torch.Tensor, sigma: float, noise_pred: torch.Tensor) -> torch.Tensor:
+ latents_dtype = latents.dtype
+ latents_fp32 = latents.to(dtype=torch.float32)
+ preview = latents_fp32 - sigma * noise_pred.to(dtype=torch.float32)
+ return preview.to(dtype=latents_dtype)
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.Anima)
+
+ return step_callback
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the transformer."""
+ for lora in self.transformer.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
+ "The LoRA model may be corrupted or incompatible."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/anima_image_to_latents.py b/invokeai/app/invocations/anima_image_to_latents.py
new file mode 100644
index 00000000000..83073ab4a80
--- /dev/null
+++ b/invokeai/app/invocations/anima_image_to_latents.py
@@ -0,0 +1,119 @@
+"""Anima image-to-latents invocation.
+
+Encodes an image to latent space using the Anima VAE (AutoencoderKLWan or FLUX VAE).
+
+For Wan VAE (AutoencoderKLWan):
+- Input image is converted to 5D tensor [B, C, T, H, W] with T=1
+- After encoding, latents are normalized: (latents - mean) / std
+ (inverse of the denormalization in anima_latents_to_image.py)
+
+For FLUX VAE (AutoEncoder):
+- Encoding is handled internally by the FLUX VAE
+"""
+
+from typing import Union
+
+import einops
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder as FluxAutoEncoder
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+AnimaVAE = Union[AutoencoderKLWan, FluxAutoEncoder]
+
+
+@invocation(
+ "anima_i2l",
+ title="Image to Latents - Anima",
+ tags=["image", "latents", "vae", "i2l", "anima"],
+ category="image",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class AnimaImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates latents from an image using the Anima VAE (supports Wan 2.1 and FLUX VAE)."""
+
+ image: ImageField = InputField(description="The image to encode.")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ if not isinstance(vae_info.model, (AutoencoderKLWan, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKLWan or FluxAutoEncoder for Anima VAE, got {type(vae_info.model).__name__}."
+ )
+
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="encode",
+ image_tensor=image_tensor,
+ vae=vae_info.model,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ if not isinstance(vae, (AutoencoderKLWan, FluxAutoEncoder)):
+ raise TypeError(f"Expected AutoencoderKLWan or FluxAutoEncoder, got {type(vae).__name__}.")
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+
+ with torch.inference_mode():
+ if isinstance(vae, FluxAutoEncoder):
+ # FLUX VAE handles scaling internally
+ generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
+ latents = vae.encode(image_tensor, sample=True, generator=generator)
+ else:
+ # AutoencoderKLWan expects 5D input [B, C, T, H, W]
+ if image_tensor.ndim == 4:
+ image_tensor = image_tensor.unsqueeze(2) # [B, C, H, W] -> [B, C, 1, H, W]
+
+ encoded = vae.encode(image_tensor, return_dict=False)[0]
+ latents = encoded.sample().to(dtype=vae_dtype)
+
+ # Normalize to denoiser space: (latents - mean) / std
+ # This is the inverse of the denormalization in anima_latents_to_image.py
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents)
+ latents = (latents - latents_mean) / latents_std
+
+ # Remove temporal dimension: [B, C, 1, H, W] -> [B, C, H, W]
+ if latents.ndim == 5:
+ latents = latents.squeeze(2)
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, (AutoencoderKLWan, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKLWan or FluxAutoEncoder for Anima VAE, got {type(vae_info.model).__name__}."
+ )
+
+ context.util.signal_progress("Running Anima VAE encode")
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/anima_latents_to_image.py b/invokeai/app/invocations/anima_latents_to_image.py
new file mode 100644
index 00000000000..080c101fa44
--- /dev/null
+++ b/invokeai/app/invocations/anima_latents_to_image.py
@@ -0,0 +1,108 @@
+"""Anima latents-to-image invocation.
+
+Decodes Anima latents using the QwenImage VAE (AutoencoderKLWan) or
+compatible FLUX VAE as fallback.
+
+Latents from the denoiser are in normalized space (zero-centered). Before
+VAE decode, they must be denormalized using the Wan 2.1 per-channel
+mean/std: latents = latents * std + mean (matching diffusers WanPipeline).
+
+The VAE expects 5D latents [B, C, T, H, W] — for single images, T=1.
+"""
+
+import torch
+from diffusers.models.autoencoders import AutoencoderKLWan
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder as FluxAutoEncoder
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+
+@invocation(
+ "anima_l2i",
+ title="Latents to Image - Anima",
+ tags=["latents", "image", "vae", "l2i", "anima"],
+ category="latents",
+ version="1.0.2",
+ classification=Classification.Prototype,
+)
+class AnimaLatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents using the Anima VAE.
+
+ Supports the Wan 2.1 QwenImage VAE (AutoencoderKLWan) with explicit
+ latent denormalization, and FLUX VAE as fallback.
+ """
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, (AutoencoderKLWan, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKLWan or FluxAutoEncoder for Anima VAE, got {type(vae_info.model).__name__}."
+ )
+
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="decode",
+ image_tensor=latents,
+ vae=vae_info.model,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ context.util.signal_progress("Running Anima VAE decode")
+ if not isinstance(vae, (AutoencoderKLWan, FluxAutoEncoder)):
+ raise TypeError(f"Expected AutoencoderKLWan or FluxAutoEncoder, got {type(vae).__name__}.")
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode():
+ if isinstance(vae, FluxAutoEncoder):
+ # FLUX VAE handles scaling internally, expects 4D [B, C, H, W]
+ img = vae.decode(latents)
+ else:
+ # Expects 5D latents [B, C, T, H, W]
+ if latents.ndim == 4:
+ latents = latents.unsqueeze(2) # [B, C, H, W] -> [B, C, 1, H, W]
+
+ # Denormalize from denoiser space to raw VAE space
+ # (same as diffusers WanPipeline and ComfyUI Wan21.process_out)
+ latents_mean = torch.tensor(vae.config.latents_mean).view(1, -1, 1, 1, 1).to(latents)
+ latents_std = torch.tensor(vae.config.latents_std).view(1, -1, 1, 1, 1).to(latents)
+ latents = latents * latents_std + latents_mean
+
+ decoded = vae.decode(latents, return_dict=False)[0]
+
+ # Output is 5D [B, C, T, H, W] — squeeze temporal dim
+ if decoded.ndim == 5:
+ decoded = decoded.squeeze(2)
+ img = decoded
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c")
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/anima_lora_loader.py b/invokeai/app/invocations/anima_lora_loader.py
new file mode 100644
index 00000000000..6a035b55aa6
--- /dev/null
+++ b/invokeai/app/invocations/anima_lora_loader.py
@@ -0,0 +1,162 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, Qwen3EncoderField, TransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("anima_lora_loader_output")
+class AnimaLoRALoaderOutput(BaseInvocationOutput):
+ """Anima LoRA Loader Output"""
+
+ transformer: Optional[TransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="Anima Transformer"
+ )
+ qwen3_encoder: Optional[Qwen3EncoderField] = OutputField(
+ default=None, description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder"
+ )
+
+
+@invocation(
+ "anima_lora_loader",
+ title="Apply LoRA - Anima",
+ tags=["lora", "model", "anima"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class AnimaLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA model to an Anima transformer and/or Qwen3 text encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.Anima,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ transformer: TransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Anima Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> AnimaLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
+ if self.qwen3_encoder and any(lora.lora.key == lora_key for lora in self.qwen3_encoder.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to Qwen3 encoder.')
+
+ output = AnimaLoRALoaderOutput()
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ output.transformer.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+ output.qwen3_encoder.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "anima_lora_collection_loader",
+ title="Apply LoRA Collection - Anima",
+ tags=["lora", "model", "anima"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class AnimaLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to an Anima transformer."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+
+ transformer: Optional[TransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> AnimaLoRALoaderOutput:
+ output = AnimaLoRALoaderOutput()
+
+ if self.loras is None:
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+ return output
+
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise ValueError(f"Unknown lora: {lora.lora.key}!")
+
+ if lora.lora.base is not BaseModelType.Anima:
+ raise ValueError(
+ f"LoRA '{lora.lora.key}' is for {lora.lora.base.value if lora.lora.base else 'unknown'} models, "
+ "not Anima models. Ensure you are using an Anima compatible LoRA."
+ )
+
+ added_loras.append(lora.lora.key)
+
+ if self.transformer is not None and output.transformer is not None:
+ output.transformer.loras.append(lora)
+
+ if self.qwen3_encoder is not None and output.qwen3_encoder is not None:
+ output.qwen3_encoder.loras.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/anima_model_loader.py b/invokeai/app/invocations/anima_model_loader.py
new file mode 100644
index 00000000000..0841f58bd9c
--- /dev/null
+++ b/invokeai/app/invocations/anima_model_loader.py
@@ -0,0 +1,86 @@
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ ModelIdentifierField,
+ Qwen3EncoderField,
+ TransformerField,
+ VAEField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+
+
+@invocation_output("anima_model_loader_output")
+class AnimaModelLoaderOutput(BaseInvocationOutput):
+ """Anima model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ qwen3_encoder: Qwen3EncoderField = OutputField(description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "anima_model_loader",
+ title="Main Model - Anima",
+ tags=["model", "anima"],
+ category="model",
+ version="1.4.0",
+ classification=Classification.Prototype,
+)
+class AnimaModelLoaderInvocation(BaseInvocation):
+ """Loads an Anima model, outputting its submodels.
+
+ Anima uses:
+ - Transformer: Cosmos Predict2 DiT + LLM Adapter (from single-file checkpoint)
+ - Qwen3 Encoder: Qwen3 0.6B (standalone single-file)
+ - VAE: AutoencoderKLQwenImage / Wan 2.1 VAE (standalone single-file or FLUX VAE)
+
+ The T5-XXL tokenizer needed for LLM Adapter token IDs is bundled in the package,
+ so no T5-XXL encoder model needs to be installed.
+ """
+
+ model: ModelIdentifierField = InputField(
+ description="Anima main model (transformer + LLM adapter).",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Anima,
+ ui_model_type=ModelType.Main,
+ title="Transformer",
+ )
+
+ vae_model: ModelIdentifierField = InputField(
+ description="Standalone VAE model. Anima uses a Wan 2.1 / QwenImage VAE (16-channel). "
+ "A FLUX VAE can also be used as a compatible fallback.",
+ input=Input.Direct,
+ ui_model_type=ModelType.VAE,
+ title="VAE",
+ )
+
+ qwen3_encoder_model: ModelIdentifierField = InputField(
+ description="Standalone Qwen3 0.6B Encoder model.",
+ input=Input.Direct,
+ ui_model_type=ModelType.Qwen3Encoder,
+ title="Qwen3 Encoder",
+ )
+
+ def invoke(self, context: InvocationContext) -> AnimaModelLoaderOutput:
+ # Transformer always comes from the main model
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+
+ # VAE
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+
+ # Qwen3 Encoder
+ qwen3_tokenizer = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+
+ return AnimaModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ qwen3_encoder=Qwen3EncoderField(tokenizer=qwen3_tokenizer, text_encoder=qwen3_encoder),
+ vae=VAEField(vae=vae),
+ )
diff --git a/invokeai/app/invocations/anima_text_encoder.py b/invokeai/app/invocations/anima_text_encoder.py
new file mode 100644
index 00000000000..f1d4fbff8f1
--- /dev/null
+++ b/invokeai/app/invocations/anima_text_encoder.py
@@ -0,0 +1,216 @@
+"""Anima text encoder invocation.
+
+Encodes text using the dual-conditioning pipeline:
+1. Qwen3 0.6B: Produces hidden states (last layer)
+2. T5-XXL Tokenizer: Produces token IDs only (no T5 model needed)
+
+Both outputs are stored together in AnimaConditioningInfo and used by
+the LLM Adapter inside the transformer during denoising.
+
+Key differences from Z-Image text encoder:
+- Anima uses Qwen3 0.6B (base model, NOT instruct) — no chat template
+- Anima additionally tokenizes with T5-XXL tokenizer to get token IDs
+- Qwen3 output uses all positions (including padding) for full context
+"""
+
+from contextlib import ExitStack
+from typing import Iterator, Tuple
+
+import torch
+from transformers import PreTrainedModel, PreTrainedTokenizerBase
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ AnimaConditioningField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ TensorField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import Qwen3EncoderField
+from invokeai.app.invocations.primitives import AnimaConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.anima.t5_tokenizer import load_bundled_t5_tokenizer
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.anima_lora_constants import ANIMA_LORA_QWEN3_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ AnimaConditioningInfo,
+ ConditioningFieldData,
+)
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.logging import InvokeAILogger
+
+logger = InvokeAILogger.get_logger(__name__)
+
+# T5-XXL max sequence length for token IDs
+T5_MAX_SEQ_LEN = 512
+
+# Safety cap for Qwen3 sequence length to prevent GPU OOM on extremely long prompts.
+# Qwen3 0.6B supports 32K context but the LLM Adapter doesn't need that much.
+QWEN3_MAX_SEQ_LEN = 8192
+
+
+@invocation(
+ "anima_text_encoder",
+ title="Prompt - Anima",
+ tags=["prompt", "conditioning", "anima"],
+ category="conditioning",
+ version="1.4.0",
+ classification=Classification.Prototype,
+)
+class AnimaTextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for an Anima image.
+
+ Uses Qwen3 0.6B for hidden state extraction and a bundled T5-XXL tokenizer for
+ token IDs (no T5 model weights needed). Both are combined by the
+ LLM Adapter inside the Anima transformer during denoising.
+ """
+
+ prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
+ qwen3_encoder: Qwen3EncoderField = InputField(
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+ mask: TensorField | None = InputField(
+ default=None,
+ description="A mask defining the region that this conditioning prompt applies to.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> AnimaConditioningOutput:
+ qwen3_embeds, t5xxl_ids, t5xxl_weights = self._encode_prompt(context)
+
+ # Move to CPU for storage
+ qwen3_embeds = qwen3_embeds.detach().to("cpu")
+ t5xxl_ids = t5xxl_ids.detach().to("cpu")
+ t5xxl_weights = t5xxl_weights.detach().to("cpu") if t5xxl_weights is not None else None
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[
+ AnimaConditioningInfo(
+ qwen3_embeds=qwen3_embeds,
+ t5xxl_ids=t5xxl_ids,
+ t5xxl_weights=t5xxl_weights,
+ )
+ ]
+ )
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return AnimaConditioningOutput(
+ conditioning=AnimaConditioningField(conditioning_name=conditioning_name, mask=self.mask)
+ )
+
+ def _encode_prompt(
+ self,
+ context: InvocationContext,
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
+ """Encode prompt using Qwen3 0.6B and T5-XXL tokenizer.
+
+ Returns:
+ Tuple of (qwen3_embeds, t5xxl_ids, t5xxl_weights).
+ - qwen3_embeds: Shape (max_seq_len, 1024) — includes all positions (including padding)
+ to preserve full sequence context for the LLM Adapter.
+ - t5xxl_ids: Shape (seq_len,) — T5-XXL token IDs (unpadded).
+ - t5xxl_weights: None (uniform weights for now).
+ """
+ prompt = self.prompt
+
+ # --- Step 1: Encode with Qwen3 0.6B ---
+ text_encoder_info = context.models.load(self.qwen3_encoder.text_encoder)
+ tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer)
+
+ with ExitStack() as exit_stack:
+ (_, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device())
+ (_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device())
+
+ device = text_encoder.device
+
+ # Apply LoRA models to the text encoder
+ lora_dtype = TorchDevice.choose_anima_inference_dtype(device)
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=text_encoder,
+ patches=self._lora_iterator(context),
+ prefix=ANIMA_LORA_QWEN3_PREFIX,
+ dtype=lora_dtype,
+ )
+ )
+
+ if not isinstance(text_encoder, PreTrainedModel):
+ raise TypeError(f"Expected PreTrainedModel for text encoder, got {type(text_encoder).__name__}.")
+ if not isinstance(tokenizer, PreTrainedTokenizerBase):
+ raise TypeError(f"Expected PreTrainedTokenizerBase for tokenizer, got {type(tokenizer).__name__}.")
+
+ context.util.signal_progress("Running Qwen3 0.6B text encoder")
+
+ # Anima uses base Qwen3 (not instruct) — tokenize directly, no chat template.
+ # A safety cap is applied to prevent GPU OOM on extremely long prompts.
+ text_inputs = tokenizer(
+ prompt,
+ padding=False,
+ truncation=True,
+ max_length=QWEN3_MAX_SEQ_LEN,
+ return_attention_mask=True,
+ return_tensors="pt",
+ )
+
+ text_input_ids = text_inputs.input_ids
+ attention_mask = text_inputs.attention_mask
+ if not isinstance(text_input_ids, torch.Tensor) or not isinstance(attention_mask, torch.Tensor):
+ raise TypeError("Tokenizer returned unexpected types.")
+
+ if text_input_ids.shape[-1] == QWEN3_MAX_SEQ_LEN:
+ logger.warning(
+ f"Prompt was truncated to {QWEN3_MAX_SEQ_LEN} tokens. "
+ "Consider shortening the prompt for best results."
+ )
+
+ # Ensure at least 1 token (empty prompts produce 0 tokens with padding=False)
+ if text_input_ids.shape[-1] == 0:
+ pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id
+ text_input_ids = torch.tensor([[pad_id]])
+ attention_mask = torch.tensor([[1]])
+
+ # Get last hidden state from Qwen3 (final layer output)
+ prompt_mask = attention_mask.to(device).bool()
+ outputs = text_encoder(
+ text_input_ids.to(device),
+ attention_mask=prompt_mask,
+ output_hidden_states=True,
+ )
+
+ if not hasattr(outputs, "hidden_states") or outputs.hidden_states is None:
+ raise RuntimeError("Text encoder did not return hidden_states.")
+ if len(outputs.hidden_states) < 1:
+ raise RuntimeError(f"Expected at least 1 hidden state, got {len(outputs.hidden_states)}.")
+
+ # Use last hidden state — only real tokens, no padding
+ qwen3_embeds = outputs.hidden_states[-1][0] # Shape: (seq_len, 1024)
+
+ # --- Step 2: Tokenize with bundled T5-XXL tokenizer (IDs only, no model) ---
+ context.util.signal_progress("Tokenizing with T5-XXL")
+ t5_tokenizer = load_bundled_t5_tokenizer()
+ t5_tokens = t5_tokenizer(
+ prompt,
+ padding=False,
+ truncation=True,
+ max_length=T5_MAX_SEQ_LEN,
+ return_tensors="pt",
+ )
+ t5xxl_ids = t5_tokens.input_ids[0] # Shape: (seq_len,)
+
+ return qwen3_embeds, t5xxl_ids, None
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the Qwen3 text encoder."""
+ for lora in self.qwen3_encoder.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
+ "The LoRA model may be corrupted or incompatible."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/baseinvocation.py b/invokeai/app/invocations/baseinvocation.py
new file mode 100644
index 00000000000..0546dabebb5
--- /dev/null
+++ b/invokeai/app/invocations/baseinvocation.py
@@ -0,0 +1,833 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI team
+
+from __future__ import annotations
+
+import inspect
+import re
+import sys
+import types
+import typing
+import warnings
+from abc import ABC, abstractmethod
+from enum import Enum
+from functools import lru_cache
+from inspect import signature
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ Callable,
+ ClassVar,
+ Iterable,
+ Literal,
+ Optional,
+ Type,
+ TypedDict,
+ TypeVar,
+ Union,
+ cast,
+)
+
+import semver
+from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, create_model
+from pydantic.fields import FieldInfo
+from pydantic_core import PydanticUndefined
+
+from invokeai.app.invocations.fields import (
+ FieldKind,
+ Input,
+ InputFieldJSONSchemaExtra,
+ UIType,
+ migrate_model_ui_type,
+)
+from invokeai.app.services.config.config_default import get_config
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.metaenum import MetaEnum
+from invokeai.app.util.misc import uuid_string
+from invokeai.backend.util.logging import InvokeAILogger
+
+if TYPE_CHECKING:
+ from invokeai.app.services.invocation_services import InvocationServices
+
+logger = InvokeAILogger.get_logger()
+
+
+class InvalidVersionError(ValueError):
+ pass
+
+
+class InvalidFieldError(TypeError):
+ pass
+
+
+class Classification(str, Enum, metaclass=MetaEnum):
+ """
+ The classification of an Invocation.
+ - `Stable`: The invocation, including its inputs/outputs and internal logic, is stable. You may build workflows with it, having confidence that they will not break because of a change in this invocation.
+ - `Beta`: The invocation is not yet stable, but is planned to be stable in the future. Workflows built around this invocation may break, but we are committed to supporting this invocation long-term.
+ - `Prototype`: The invocation is not yet stable and may be removed from the application at any time. Workflows built around this invocation may break, and we are *not* committed to supporting this invocation.
+ - `Deprecated`: The invocation is deprecated and may be removed in a future version.
+ - `Internal`: The invocation is not intended for use by end-users. It may be changed or removed at any time, but is exposed for users to play with.
+ - `Special`: The invocation is a special case and does not fit into any of the other classifications.
+ """
+
+ Stable = "stable"
+ Beta = "beta"
+ Prototype = "prototype"
+ Deprecated = "deprecated"
+ Internal = "internal"
+ Special = "special"
+
+
+class Bottleneck(str, Enum, metaclass=MetaEnum):
+ """
+ The bottleneck of an invocation.
+ - `Network`: The invocation's execution is network-bound.
+ - `GPU`: The invocation's execution is GPU-bound.
+ """
+
+ Network = "network"
+ GPU = "gpu"
+
+
+class UIConfigBase(BaseModel):
+ """
+ Provides additional node configuration to the UI.
+ This is used internally by the @invocation decorator logic. Do not use this directly.
+ """
+
+ tags: Optional[list[str]] = Field(default=None, description="The node's tags")
+ title: Optional[str] = Field(default=None, description="The node's display name")
+ category: Optional[str] = Field(default=None, description="The node's category")
+ version: str = Field(
+ description='The node\'s version. Should be a valid semver string e.g. "1.0.0" or "3.8.13".',
+ )
+ node_pack: str = Field(description="The node pack that this node belongs to, will be 'invokeai' for built-in nodes")
+ classification: Classification = Field(default=Classification.Stable, description="The node's classification")
+
+ model_config = ConfigDict(
+ validate_assignment=True,
+ json_schema_serialization_defaults_required=True,
+ )
+
+
+class OriginalModelField(TypedDict):
+ annotation: Any
+ field_info: FieldInfo
+
+
+class BaseInvocationOutput(BaseModel):
+ """
+ Base class for all invocation outputs.
+
+ All invocation outputs must use the `@invocation_output` decorator to provide their unique type.
+ """
+
+ output_meta: Optional[dict[str, JsonValue]] = Field(
+ default=None,
+ description="Optional dictionary of metadata for the invocation output, unrelated to the invocation's actual output value. This is not exposed as an output field.",
+ json_schema_extra={"field_kind": FieldKind.NodeAttribute},
+ )
+
+ @staticmethod
+ def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocationOutput]) -> None:
+ """Adds various UI-facing attributes to the invocation output's OpenAPI schema."""
+ # 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["class"] = "output"
+ schema["required"].extend(["type"])
+
+ @classmethod
+ def get_type(cls) -> str:
+ """Gets the invocation output's type, as provided by the `@invocation_output` decorator."""
+ return cls.model_fields["type"].default
+
+ _original_model_fields: ClassVar[dict[str, OriginalModelField]] = {}
+ """The original model fields, before any modifications were made by the @invocation_output decorator."""
+
+ model_config = ConfigDict(
+ protected_namespaces=(),
+ validate_assignment=True,
+ json_schema_serialization_defaults_required=True,
+ json_schema_extra=json_schema_extra,
+ )
+
+
+class RequiredConnectionException(Exception):
+ """Raised when an field which requires a connection did not receive a value."""
+
+ def __init__(self, node_id: str, field_name: str):
+ super().__init__(f"Node {node_id} missing connections for field {field_name}")
+
+
+class MissingInputException(Exception):
+ """Raised when an field which requires some input, but did not receive a value."""
+
+ def __init__(self, node_id: str, field_name: str):
+ super().__init__(f"Node {node_id} missing value or connection for field {field_name}")
+
+
+class BaseInvocation(ABC, BaseModel):
+ """
+ All invocations must use the `@invocation` decorator to provide their unique type.
+ """
+
+ @classmethod
+ def get_type(cls) -> str:
+ """Gets the invocation's type, as provided by the `@invocation` decorator."""
+ return cls.model_fields["type"].default
+
+ @classmethod
+ def get_output_annotation(cls) -> Type[BaseInvocationOutput]:
+ """Gets the invocation's output annotation (i.e. the return annotation of its `invoke()` method)."""
+ return signature(cls.invoke).return_annotation
+
+ @staticmethod
+ def json_schema_extra(schema: dict[str, Any], model_class: Type[BaseInvocation]) -> None:
+ """Adds various UI-facing attributes to the invocation's OpenAPI schema."""
+ if title := model_class.UIConfig.title:
+ schema["title"] = title
+ if tags := model_class.UIConfig.tags:
+ schema["tags"] = tags
+ if category := model_class.UIConfig.category:
+ schema["category"] = category
+ if node_pack := model_class.UIConfig.node_pack:
+ schema["node_pack"] = node_pack
+ schema["classification"] = model_class.UIConfig.classification
+ schema["version"] = model_class.UIConfig.version
+ if "required" not in schema or not isinstance(schema["required"], list):
+ schema["required"] = []
+ schema["class"] = "invocation"
+ schema["required"].extend(["type", "id"])
+
+ @abstractmethod
+ def invoke(self, context: InvocationContext) -> BaseInvocationOutput:
+ """Invoke with provided context and return outputs."""
+ pass
+
+ def invoke_internal(self, context: InvocationContext, services: "InvocationServices") -> BaseInvocationOutput:
+ """
+ Internal invoke method, calls `invoke()` after some prep.
+ Handles optional fields that are required to call `invoke()` and invocation cache.
+ """
+ for field_name, field in type(self).model_fields.items():
+ if not field.json_schema_extra or callable(field.json_schema_extra):
+ # something has gone terribly awry, we should always have this and it should be a dict
+ continue
+
+ # Here we handle the case where the field is optional in the pydantic class, but required
+ # in the `invoke()` method.
+
+ 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(type(self).model_fields["type"].default, field_name)
+ elif input_ == Input.Any:
+ raise MissingInputException(type(self).model_fields["type"].default, field_name)
+
+ # skip node cache codepath if it's disabled
+ if services.configuration.node_cache_size == 0:
+ return self.invoke(context)
+
+ output: BaseInvocationOutput
+ if self.use_cache:
+ key = services.invocation_cache.create_key(self)
+ cached_value = services.invocation_cache.get(key)
+ if cached_value is None:
+ services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}')
+ output = self.invoke(context)
+ services.invocation_cache.save(key, output)
+ return output
+ else:
+ services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}')
+ return cached_value
+ else:
+ services.logger.debug(f'Skipping invocation cache for "{self.get_type()}": {self.id}')
+ return self.invoke(context)
+
+ id: str = Field(
+ default_factory=uuid_string,
+ description="The id of this instance of an invocation. Must be unique among all instances of invocations.",
+ json_schema_extra={"field_kind": FieldKind.NodeAttribute},
+ )
+ is_intermediate: bool = Field(
+ default=False,
+ description="Whether or not this is an intermediate invocation.",
+ json_schema_extra=InputFieldJSONSchemaExtra(
+ input=Input.Direct, field_kind=FieldKind.NodeAttribute, ui_type=UIType._IsIntermediate
+ ).model_dump(exclude_none=True),
+ )
+ use_cache: bool = Field(
+ default=True,
+ description="Whether or not to use the cache",
+ json_schema_extra={"field_kind": FieldKind.NodeAttribute},
+ )
+
+ bottleneck: ClassVar[Bottleneck]
+
+ UIConfig: ClassVar[UIConfigBase]
+
+ model_config = ConfigDict(
+ protected_namespaces=(),
+ validate_assignment=True,
+ json_schema_extra=json_schema_extra,
+ json_schema_serialization_defaults_required=False,
+ coerce_numbers_to_str=True,
+ )
+
+ _original_model_fields: ClassVar[dict[str, OriginalModelField]] = {}
+ """The original model fields, before any modifications were made by the @invocation decorator."""
+
+
+TBaseInvocation = TypeVar("TBaseInvocation", bound=BaseInvocation)
+
+
+class InvocationRegistry:
+ _invocation_classes: ClassVar[set[type[BaseInvocation]]] = set()
+ _output_classes: ClassVar[set[type[BaseInvocationOutput]]] = set()
+
+ @classmethod
+ def register_invocation(cls, invocation: type[BaseInvocation]) -> None:
+ """Registers an invocation."""
+
+ invocation_type = invocation.get_type()
+ node_pack = invocation.UIConfig.node_pack
+
+ # Log a warning when an existing invocation is being clobbered by the one we are registering
+ clobbered_invocation = InvocationRegistry.get_invocation_for_type(invocation_type)
+ if clobbered_invocation is not None:
+ # This should always be true - we just checked if the invocation type was in the set
+ clobbered_node_pack = clobbered_invocation.UIConfig.node_pack
+
+ if clobbered_node_pack == "invokeai":
+ # The invocation being clobbered is a core invocation
+ logger.warning(f'Overriding core node "{invocation_type}" with node from "{node_pack}"')
+ else:
+ # The invocation being clobbered is a custom invocation
+ logger.warning(
+ f'Overriding node "{invocation_type}" from "{node_pack}" with node from "{clobbered_node_pack}"'
+ )
+ cls._invocation_classes.remove(clobbered_invocation)
+
+ cls._invocation_classes.add(invocation)
+ cls.invalidate_invocation_typeadapter()
+
+ @classmethod
+ @lru_cache(maxsize=1)
+ def get_invocation_typeadapter(cls) -> TypeAdapter[Any]:
+ """Gets a pydantic TypeAdapter for the union of all invocation types.
+
+ This is used to parse serialized invocations into the correct invocation class.
+
+ This method is cached to avoid rebuilding the TypeAdapter on every access. If the invocation allowlist or
+ denylist is changed, the cache should be cleared to ensure the TypeAdapter is updated and validation respects
+ the updated allowlist and denylist.
+
+ @see https://docs.pydantic.dev/latest/concepts/type_adapter/
+ """
+ return TypeAdapter(Annotated[Union[tuple(cls.get_invocation_classes())], Field(discriminator="type")])
+
+ @classmethod
+ def invalidate_invocation_typeadapter(cls) -> None:
+ """Invalidates the cached invocation type adapter."""
+ cls.get_invocation_typeadapter.cache_clear()
+
+ @classmethod
+ def unregister_pack(cls, node_pack: str) -> list[str]:
+ """Unregisters all invocations and outputs belonging to a node pack.
+
+ Returns a list of the invocation types that were removed.
+ """
+ removed_types: list[str] = []
+
+ invocations_to_remove = {inv for inv in cls._invocation_classes if inv.UIConfig.node_pack == node_pack}
+ for inv in invocations_to_remove:
+ removed_types.append(inv.get_type())
+ cls._invocation_classes.discard(inv)
+
+ if invocations_to_remove:
+ cls.invalidate_invocation_typeadapter()
+
+ # Also remove any output classes from this pack's modules
+ outputs_to_remove = {out for out in cls._output_classes if out.__module__.split(".")[0] == node_pack}
+ for out in outputs_to_remove:
+ cls._output_classes.discard(out)
+
+ if outputs_to_remove:
+ cls.invalidate_output_typeadapter()
+
+ return removed_types
+
+ @classmethod
+ def get_invocation_classes(cls) -> Iterable[type[BaseInvocation]]:
+ """Gets all invocations, respecting the allowlist and denylist."""
+ app_config = get_config()
+ allowed_invocations: set[type[BaseInvocation]] = set()
+ for sc in cls._invocation_classes:
+ invocation_type = sc.get_type()
+ is_in_allowlist = (
+ invocation_type in app_config.allow_nodes if isinstance(app_config.allow_nodes, list) else True
+ )
+ is_in_denylist = (
+ invocation_type in app_config.deny_nodes if isinstance(app_config.deny_nodes, list) else False
+ )
+ if is_in_allowlist and not is_in_denylist:
+ allowed_invocations.add(sc)
+ return allowed_invocations
+
+ @classmethod
+ def get_invocations_map(cls) -> dict[str, type[BaseInvocation]]:
+ """Gets a map of all invocation types to their invocation classes."""
+ return {i.get_type(): i for i in cls.get_invocation_classes()}
+
+ @classmethod
+ def get_invocation_types(cls) -> Iterable[str]:
+ """Gets all invocation types."""
+ return (i.get_type() for i in cls.get_invocation_classes())
+
+ @classmethod
+ def get_invocation_for_type(cls, invocation_type: str) -> type[BaseInvocation] | None:
+ """Gets the invocation class for a given invocation type."""
+ return cls.get_invocations_map().get(invocation_type)
+
+ @classmethod
+ def register_output(cls, output: "type[TBaseInvocationOutput]") -> None:
+ """Registers an invocation output."""
+ output_type = output.get_type()
+
+ # Log a warning when an existing invocation is being clobbered by the one we are registering
+ clobbered_output = InvocationRegistry.get_output_for_type(output_type)
+ if clobbered_output is not None:
+ # TODO(psyche): We do not record the node pack of the output, so we cannot log it here
+ logger.warning(f'Overriding invocation output "{output_type}"')
+ cls._output_classes.remove(clobbered_output)
+
+ cls._output_classes.add(output)
+ cls.invalidate_output_typeadapter()
+
+ @classmethod
+ def get_output_classes(cls) -> Iterable[type[BaseInvocationOutput]]:
+ """Gets all invocation outputs."""
+ return cls._output_classes
+
+ @classmethod
+ def get_outputs_map(cls) -> dict[str, type[BaseInvocationOutput]]:
+ """Gets a map of all output types to their output classes."""
+ return {i.get_type(): i for i in cls.get_output_classes()}
+
+ @classmethod
+ @lru_cache(maxsize=1)
+ def get_output_typeadapter(cls) -> TypeAdapter[Any]:
+ """Gets a pydantic TypeAdapter for the union of all invocation output types.
+
+ This is used to parse serialized invocation outputs into the correct invocation output class.
+
+ This method is cached to avoid rebuilding the TypeAdapter on every access. If the invocation allowlist or
+ denylist is changed, the cache should be cleared to ensure the TypeAdapter is updated and validation respects
+ the updated allowlist and denylist.
+
+ @see https://docs.pydantic.dev/latest/concepts/type_adapter/
+ """
+ return TypeAdapter(Annotated[Union[tuple(cls._output_classes)], Field(discriminator="type")])
+
+ @classmethod
+ def invalidate_output_typeadapter(cls) -> None:
+ """Invalidates the cached invocation output type adapter."""
+ cls.get_output_typeadapter.cache_clear()
+
+ @classmethod
+ def get_output_types(cls) -> Iterable[str]:
+ """Gets all invocation output types."""
+ return (i.get_type() for i in cls.get_output_classes())
+
+ @classmethod
+ def get_output_for_type(cls, output_type: str) -> type[BaseInvocationOutput] | None:
+ """Gets the output class for a given output type."""
+ return cls.get_outputs_map().get(output_type)
+
+
+RESERVED_NODE_ATTRIBUTE_FIELD_NAMES = {
+ "id",
+ "is_intermediate",
+ "use_cache",
+ "type",
+ "workflow",
+ "bottleneck",
+}
+
+RESERVED_INPUT_FIELD_NAMES = {"metadata", "board"}
+
+RESERVED_OUTPUT_FIELD_NAMES = {"type", "output_meta"}
+
+
+class _Model(BaseModel):
+ pass
+
+
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=DeprecationWarning)
+ # Get all pydantic model attrs, methods, etc
+ RESERVED_PYDANTIC_FIELD_NAMES = {m[0] for m in inspect.getmembers(_Model())}
+
+
+def is_enum_member(value: Any, enum_class: type[Enum]) -> bool:
+ """Checks if a value is a member of an enum class."""
+ try:
+ enum_class(value)
+ return True
+ except ValueError:
+ return False
+
+
+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 have a type annotation
+ - Must have a json_schema_extra dict
+ - Must have field_kind in json_schema_extra
+ - Field name must not be reserved, according to its field_kind
+ """
+ for name, field in model_fields.items():
+ if name in RESERVED_PYDANTIC_FIELD_NAMES:
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved by pydantic)")
+
+ if not field.annotation:
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field type (missing annotation)")
+
+ if not isinstance(field.json_schema_extra, dict):
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field definition (missing json_schema_extra dict)")
+
+ field_kind = field.json_schema_extra.get("field_kind", None)
+
+ # must have a field_kind
+ if not is_enum_member(field_kind, FieldKind):
+ raise InvalidFieldError(
+ f"{model_type}.{name}: Invalid field definition for (maybe it's not an InputField or OutputField?)"
+ )
+
+ if field_kind == FieldKind.Input.value and (
+ name in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES or name in RESERVED_INPUT_FIELD_NAMES
+ ):
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved input field name)")
+
+ if field_kind == FieldKind.Output.value and name in RESERVED_OUTPUT_FIELD_NAMES:
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved output field name)")
+
+ if field_kind == FieldKind.Internal.value and name not in RESERVED_INPUT_FIELD_NAMES:
+ raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (internal field without reserved name)")
+
+ # node attribute fields *must* be in the reserved list
+ if (
+ field_kind == FieldKind.NodeAttribute.value
+ and name not in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES
+ and name not in RESERVED_OUTPUT_FIELD_NAMES
+ ):
+ raise InvalidFieldError(
+ f"{model_type}.{name}: Invalid field name (node attribute field without reserved name)"
+ )
+
+ ui_type = field.json_schema_extra.get("ui_type", None)
+ ui_model_base = field.json_schema_extra.get("ui_model_base", None)
+ ui_model_type = field.json_schema_extra.get("ui_model_type", None)
+ ui_model_variant = field.json_schema_extra.get("ui_model_variant", None)
+ ui_model_format = field.json_schema_extra.get("ui_model_format", None)
+
+ if ui_type is not None:
+ # There are 3 cases where we may need to take action:
+ #
+ # 1. The ui_type is a migratable, deprecated value. For example, ui_type=UIType.MainModel value is
+ # deprecated and should be migrated to:
+ # - ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
+ # - ui_model_type=[ModelType.Main]
+ #
+ # 2. ui_type was set in conjunction with any of the new ui_model_[base|type|variant|format] fields, which
+ # is not allowed (they are mutually exclusive). In this case, we ignore ui_type and log a warning.
+ #
+ # 3. ui_type is a deprecated value that is not migratable. For example, ui_type=UIType.Image is deprecated;
+ # Image fields are now automatically detected based on the field's type annotation. In this case, we
+ # ignore ui_type and log a warning.
+ #
+ # The cases must be checked in this order to ensure proper handling.
+
+ # Easier to work with as an enum
+ ui_type = UIType(ui_type)
+
+ # The enum member values are not always the same as their names - we want to log the name so the user can
+ # easily review their code and see where the deprecated enum member is used.
+ human_readable_name = f"UIType.{ui_type.name}"
+
+ # Case 1: migratable deprecated value
+ did_migrate = migrate_model_ui_type(ui_type, field.json_schema_extra)
+
+ if did_migrate:
+ logger.warning(
+ f'{model_type}.{name}: Migrated deprecated "ui_type" "{human_readable_name}" to new ui_model_[base|type|variant|format] fields'
+ )
+ field.json_schema_extra.pop("ui_type")
+
+ # Case 2: mutually exclusive with new fields
+ elif (
+ ui_model_base is not None
+ or ui_model_type is not None
+ or ui_model_variant is not None
+ or ui_model_format is not None
+ ):
+ logger.warning(
+ f'{model_type}.{name}: "ui_type" is mutually exclusive with "ui_model_[base|type|format|variant]", ignoring "ui_type"'
+ )
+ field.json_schema_extra.pop("ui_type")
+
+ # Case 3: deprecated value that is not migratable
+ elif ui_type.startswith("DEPRECATED_"):
+ logger.warning(f'{model_type}.{name}: Deprecated "ui_type" "{human_readable_name}", ignoring')
+ field.json_schema_extra.pop("ui_type")
+
+ return None
+
+
+class NoDefaultSentinel:
+ pass
+
+
+def validate_field_default(
+ cls_name: str, field_name: str, invocation_type: str, annotation: Any, field_info: FieldInfo
+) -> None:
+ """Validates the default value of a field against its pydantic field definition."""
+
+ assert isinstance(field_info.json_schema_extra, dict), "json_schema_extra is not a dict"
+
+ # By the time we are doing this, we've already done some pydantic magic by overriding the original default value.
+ # We store the original default value in the json_schema_extra dict, so we can validate it here.
+ orig_default = field_info.json_schema_extra.get("orig_default", NoDefaultSentinel)
+
+ if orig_default is NoDefaultSentinel:
+ return
+
+ # To validate the default value, we can create a temporary pydantic model with the field we are validating as its
+ # only field. Then validate the default value against this temporary model.
+ TempDefaultValidator = cast(BaseModel, create_model(cls_name, **{field_name: (annotation, field_info)}))
+
+ try:
+ TempDefaultValidator.model_validate({field_name: orig_default})
+ except Exception as e:
+ raise InvalidFieldError(
+ f'Default value for field "{field_name}" on invocation "{invocation_type}" is invalid, {e}'
+ ) from e
+
+
+def is_optional(annotation: Any) -> bool:
+ """
+ Checks if the given annotation is optional (i.e. Optional[X], Union[X, None] or X | None).
+ """
+ origin = typing.get_origin(annotation)
+ # PEP 604 unions (int|None) have origin types.UnionType
+ is_union = origin is typing.Union or origin is types.UnionType
+ if not is_union:
+ return False
+ return any(arg is type(None) for arg in typing.get_args(annotation))
+
+
+def invocation(
+ invocation_type: str,
+ title: Optional[str] = None,
+ tags: Optional[list[str]] = None,
+ category: Optional[str] = None,
+ version: Optional[str] = None,
+ use_cache: Optional[bool] = True,
+ classification: Classification = Classification.Stable,
+ bottleneck: Bottleneck = Bottleneck.GPU,
+) -> Callable[[Type[TBaseInvocation]], Type[TBaseInvocation]]:
+ """
+ Registers an invocation.
+
+ :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[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] 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.
+ :param Classification classification: The classification of the invocation. Defaults to FeatureClassification.Stable. Use Beta or Prototype if the invocation is unstable.
+ :param Bottleneck bottleneck: The bottleneck of the invocation. Defaults to Bottleneck.GPU. Use Network if the invocation is network-bound.
+ """
+
+ def wrapper(cls: Type[TBaseInvocation]) -> Type[TBaseInvocation]:
+ # Validate invocation types on creation of invocation classes
+ # TODO: ensure unique?
+ if re.compile(r"^\S+$").match(invocation_type) is None:
+ raise ValueError(f'"invocation_type" must consist of non-whitespace characters, got "{invocation_type}"')
+
+ # The node pack is the module name - will be "invokeai" for built-in nodes
+ node_pack = cls.__module__.split(".")[0]
+
+ validate_fields(cls.model_fields, invocation_type)
+
+ fields: dict[str, tuple[Any, FieldInfo]] = {}
+
+ original_model_fields: dict[str, OriginalModelField] = {}
+
+ for field_name, field_info in cls.model_fields.items():
+ annotation = field_info.annotation
+ assert annotation is not None, f"{field_name} on invocation {invocation_type} has no type annotation."
+ assert isinstance(field_info.json_schema_extra, dict), (
+ f"{field_name} on invocation {invocation_type} has a non-dict json_schema_extra, did you forget to use InputField?"
+ )
+
+ original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
+
+ validate_field_default(cls.__name__, field_name, invocation_type, annotation, field_info)
+
+ if field_info.default is None and not is_optional(annotation):
+ annotation = annotation | None
+
+ fields[field_name] = (annotation, field_info)
+
+ # Add OpenAPI schema extras
+ uiconfig: dict[str, Any] = {}
+ uiconfig["title"] = title
+ uiconfig["tags"] = tags
+ uiconfig["category"] = category
+ uiconfig["classification"] = classification
+ uiconfig["node_pack"] = node_pack
+
+ if version is not None:
+ try:
+ semver.Version.parse(version)
+ except ValueError as e:
+ raise InvalidVersionError(f'Invalid version string for node "{invocation_type}": "{version}"') from e
+ uiconfig["version"] = version
+ else:
+ logger.warning(f'No version specified for node "{invocation_type}", using "1.0.0"')
+ uiconfig["version"] = "1.0.0"
+
+ cls.UIConfig = UIConfigBase(**uiconfig)
+
+ if use_cache is not None:
+ cls.model_fields["use_cache"].default = use_cache
+
+ cls.bottleneck = bottleneck
+
+ # 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.
+
+ invocation_type_annotation = Literal[invocation_type]
+
+ # Field() returns an instance of FieldInfo, but thanks to a pydantic implementation detail, it is _typed_ as Any.
+ # This cast makes the type annotation match the class's true type.
+ invocation_type_field_info = cast(
+ FieldInfo,
+ Field(title="type", default=invocation_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute}),
+ )
+
+ fields["type"] = (invocation_type_annotation, invocation_type_field_info)
+
+ # Invocation outputs must be registered using the @invocation_output decorator, but it is possible that the
+ # output is registered _after_ this invocation is registered. It depends on module import ordering.
+ #
+ # We can only confirm the output for an invocation is registered after all modules are imported. There's
+ # only really one good time to do that - during application startup, in `run_app.py`, after loading all
+ # custom nodes.
+ #
+ # We can still do some basic validation here - ensure the invoke method is defined and returns an instance
+ # of BaseInvocationOutput.
+
+ # Validate the `invoke()` method is implemented
+ if "invoke" in cls.__abstractmethods__:
+ raise ValueError(f'Invocation "{invocation_type}" must implement the "invoke" method')
+
+ # And validate that `invoke()` returns a subclass of `BaseInvocationOutput
+ invoke_return_annotation = signature(cls.invoke).return_annotation
+
+ try:
+ # TODO(psyche): If `invoke()` is not defined, `return_annotation` ends up as the string "BaseInvocationOutput"
+ # instead of the class `BaseInvocationOutput`. This may be a pydantic bug: https://github.com/pydantic/pydantic/issues/7978
+ if isinstance(invoke_return_annotation, str):
+ invoke_return_annotation = getattr(sys.modules[cls.__module__], invoke_return_annotation)
+
+ assert invoke_return_annotation is not BaseInvocationOutput
+ assert issubclass(invoke_return_annotation, BaseInvocationOutput)
+ except Exception:
+ raise ValueError(
+ f'Invocation "{invocation_type}" must have a return annotation of a subclass of BaseInvocationOutput (got "{invoke_return_annotation}")'
+ )
+
+ docstring = cls.__doc__
+ new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields) # type: ignore
+ new_class.__doc__ = docstring
+ new_class._original_model_fields = original_model_fields
+
+ InvocationRegistry.register_invocation(new_class)
+
+ return new_class
+
+ return wrapper
+
+
+TBaseInvocationOutput = TypeVar("TBaseInvocationOutput", bound=BaseInvocationOutput)
+
+
+def invocation_output(
+ output_type: str,
+) -> Callable[[Type[TBaseInvocationOutput]], Type[TBaseInvocationOutput]]:
+ """
+ Adds metadata to an invocation output.
+
+ :param str output_type: The type of the invocation output. Must be unique among all invocation outputs.
+ """
+
+ def wrapper(cls: Type[TBaseInvocationOutput]) -> Type[TBaseInvocationOutput]:
+ # Validate output types on creation of invocation output classes
+ # TODO: ensure unique?
+ if re.compile(r"^\S+$").match(output_type) is None:
+ raise ValueError(f'"output_type" must consist of non-whitespace characters, got "{output_type}"')
+
+ validate_fields(cls.model_fields, output_type)
+
+ fields: dict[str, tuple[Any, FieldInfo]] = {}
+
+ for field_name, field_info in cls.model_fields.items():
+ annotation = field_info.annotation
+ assert annotation is not None, f"{field_name} on invocation output {output_type} has no type annotation."
+ assert isinstance(field_info.json_schema_extra, dict), (
+ f"{field_name} on invocation output {output_type} has a non-dict json_schema_extra, did you forget to use InputField?"
+ )
+
+ cls._original_model_fields[field_name] = OriginalModelField(annotation=annotation, field_info=field_info)
+
+ if field_info.default is not PydanticUndefined and is_optional(annotation):
+ annotation = annotation | None
+ fields[field_name] = (annotation, field_info)
+
+ # Add the output type to the model.
+ output_type_annotation = Literal[output_type]
+
+ # Field() returns an instance of FieldInfo, but thanks to a pydantic implementation detail, it is _typed_ as Any.
+ # This cast makes the type annotation match the class's true type.
+ output_type_field_info = cast(
+ FieldInfo,
+ Field(title="type", default=output_type, json_schema_extra={"field_kind": FieldKind.NodeAttribute}),
+ )
+
+ fields["type"] = (output_type_annotation, output_type_field_info)
+
+ docstring = cls.__doc__
+ new_class = create_model(cls.__qualname__, __base__=cls, __module__=cls.__module__, **fields)
+ new_class.__doc__ = docstring
+
+ InvocationRegistry.register_output(new_class)
+
+ return new_class
+
+ return wrapper
diff --git a/invokeai/app/invocations/batch.py b/invokeai/app/invocations/batch.py
new file mode 100644
index 00000000000..f79b8816ade
--- /dev/null
+++ b/invokeai/app/invocations/batch.py
@@ -0,0 +1,270 @@
+from typing import Literal
+
+from pydantic import BaseModel
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ ImageField,
+ Input,
+ InputField,
+ OutputField,
+)
+from invokeai.app.invocations.primitives import (
+ FloatOutput,
+ ImageOutput,
+ IntegerOutput,
+ StringOutput,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+BATCH_GROUP_IDS = Literal[
+ "None",
+ "Group 1",
+ "Group 2",
+ "Group 3",
+ "Group 4",
+ "Group 5",
+]
+
+
+class NotExecutableNodeError(Exception):
+ def __init__(self, message: str = "This class should never be executed or instantiated directly."):
+ super().__init__(message)
+
+ pass
+
+
+class BaseBatchInvocation(BaseInvocation):
+ batch_group_id: BATCH_GROUP_IDS = InputField(
+ default="None",
+ description="The ID of this batch node's group. If provided, all batch nodes in with the same ID will be 'zipped' before execution, and all nodes' collections must be of the same size.",
+ input=Input.Direct,
+ title="Batch Group",
+ )
+
+ def __init__(self):
+ raise NotExecutableNodeError()
+
+
+@invocation(
+ "image_batch",
+ title="Image Batch",
+ tags=["primitives", "image", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class ImageBatchInvocation(BaseBatchInvocation):
+ """Create a batched generation, where the workflow is executed once for each image in the batch."""
+
+ images: list[ImageField] = InputField(
+ min_length=1,
+ description="The images to batch over",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation_output("image_generator_output")
+class ImageGeneratorOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of boards"""
+
+ images: list[ImageField] = OutputField(description="The generated images")
+
+
+class ImageGeneratorField(BaseModel):
+ pass
+
+
+@invocation(
+ "image_generator",
+ title="Image Generator",
+ tags=["primitives", "board", "image", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class ImageGenerator(BaseInvocation):
+ """Generated a collection of images for use in a batched generation"""
+
+ generator: ImageGeneratorField = InputField(
+ description="The image generator.",
+ input=Input.Direct,
+ title="Generator Type",
+ )
+
+ def __init__(self):
+ raise NotExecutableNodeError()
+
+ def invoke(self, context: InvocationContext) -> ImageGeneratorOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation(
+ "string_batch",
+ title="String Batch",
+ tags=["primitives", "string", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class StringBatchInvocation(BaseBatchInvocation):
+ """Create a batched generation, where the workflow is executed once for each string in the batch."""
+
+ strings: list[str] = InputField(
+ min_length=1,
+ description="The strings to batch over",
+ )
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation_output("string_generator_output")
+class StringGeneratorOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of strings"""
+
+ strings: list[str] = OutputField(description="The generated strings")
+
+
+class StringGeneratorField(BaseModel):
+ pass
+
+
+@invocation(
+ "string_generator",
+ title="String Generator",
+ tags=["primitives", "string", "number", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class StringGenerator(BaseInvocation):
+ """Generated a range of strings for use in a batched generation"""
+
+ generator: StringGeneratorField = InputField(
+ description="The string generator.",
+ input=Input.Direct,
+ title="Generator Type",
+ )
+
+ def __init__(self):
+ raise NotExecutableNodeError()
+
+ def invoke(self, context: InvocationContext) -> StringGeneratorOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation(
+ "integer_batch",
+ title="Integer Batch",
+ tags=["primitives", "integer", "number", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class IntegerBatchInvocation(BaseBatchInvocation):
+ """Create a batched generation, where the workflow is executed once for each integer in the batch."""
+
+ integers: list[int] = InputField(
+ min_length=1,
+ description="The integers to batch over",
+ )
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation_output("integer_generator_output")
+class IntegerGeneratorOutput(BaseInvocationOutput):
+ integers: list[int] = OutputField(description="The generated integers")
+
+
+class IntegerGeneratorField(BaseModel):
+ pass
+
+
+@invocation(
+ "integer_generator",
+ title="Integer Generator",
+ tags=["primitives", "int", "number", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class IntegerGenerator(BaseInvocation):
+ """Generated a range of integers for use in a batched generation"""
+
+ generator: IntegerGeneratorField = InputField(
+ description="The integer generator.",
+ input=Input.Direct,
+ title="Generator Type",
+ )
+
+ def __init__(self):
+ raise NotExecutableNodeError()
+
+ def invoke(self, context: InvocationContext) -> IntegerGeneratorOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation(
+ "float_batch",
+ title="Float Batch",
+ tags=["primitives", "float", "number", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class FloatBatchInvocation(BaseBatchInvocation):
+ """Create a batched generation, where the workflow is executed once for each float in the batch."""
+
+ floats: list[float] = InputField(
+ min_length=1,
+ description="The floats to batch over",
+ )
+
+ def invoke(self, context: InvocationContext) -> FloatOutput:
+ raise NotExecutableNodeError()
+
+
+@invocation_output("float_generator_output")
+class FloatGeneratorOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of floats"""
+
+ floats: list[float] = OutputField(description="The generated floats")
+
+
+class FloatGeneratorField(BaseModel):
+ pass
+
+
+@invocation(
+ "float_generator",
+ title="Float Generator",
+ tags=["primitives", "float", "number", "batch", "special"],
+ category="batch",
+ version="1.0.0",
+ classification=Classification.Special,
+)
+class FloatGenerator(BaseInvocation):
+ """Generated a range of floats for use in a batched generation"""
+
+ generator: FloatGeneratorField = InputField(
+ description="The float generator.",
+ input=Input.Direct,
+ title="Generator Type",
+ )
+
+ def __init__(self):
+ raise NotExecutableNodeError()
+
+ def invoke(self, context: InvocationContext) -> FloatGeneratorOutput:
+ raise NotExecutableNodeError()
diff --git a/invokeai/app/invocations/blend_latents.py b/invokeai/app/invocations/blend_latents.py
new file mode 100644
index 00000000000..9f4e0f5563c
--- /dev/null
+++ b/invokeai/app/invocations/blend_latents.py
@@ -0,0 +1,120 @@
+from typing import Optional, Union
+
+import numpy as np
+import torch
+import torchvision.transforms as T
+from PIL import Image
+from torchvision.transforms.functional import resize as tv_resize
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, LatentsField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+
+
+def slerp(
+ t: Union[float, np.ndarray],
+ v0: Union[torch.Tensor, np.ndarray],
+ v1: Union[torch.Tensor, np.ndarray],
+ device: torch.device,
+ DOT_THRESHOLD: float = 0.9995,
+):
+ """
+ Spherical linear interpolation
+ Args:
+ t (float/np.ndarray): Float value between 0.0 and 1.0
+ v0 (np.ndarray): Starting vector
+ v1 (np.ndarray): Final vector
+ DOT_THRESHOLD (float): Threshold for considering the two vectors as
+ colineal. Not recommended to alter this.
+ Returns:
+ v2 (np.ndarray): Interpolation vector between v0 and v1
+ """
+ inputs_are_torch = False
+ if not isinstance(v0, np.ndarray):
+ inputs_are_torch = True
+ v0 = v0.detach().cpu().numpy()
+ if not isinstance(v1, np.ndarray):
+ inputs_are_torch = True
+ v1 = v1.detach().cpu().numpy()
+
+ dot = np.sum(v0 * v1 / (np.linalg.norm(v0) * np.linalg.norm(v1)))
+ if np.abs(dot) > DOT_THRESHOLD:
+ v2 = (1 - t) * v0 + t * v1
+ else:
+ theta_0 = np.arccos(dot)
+ sin_theta_0 = np.sin(theta_0)
+ theta_t = theta_0 * t
+ sin_theta_t = np.sin(theta_t)
+ s0 = np.sin(theta_0 - theta_t) / sin_theta_0
+ s1 = sin_theta_t / sin_theta_0
+ v2 = s0 * v0 + s1 * v1
+
+ if inputs_are_torch:
+ v2 = torch.from_numpy(v2).to(device)
+
+ return v2
+
+
+@invocation(
+ "lblend",
+ title="Blend Latents",
+ tags=["latents", "blend", "mask"],
+ category="latents",
+ version="1.1.0",
+)
+class BlendLatentsInvocation(BaseInvocation):
+ """Blend two latents using a given alpha. If a mask is provided, the second latents will be masked before blending.
+ Latents must have same size. Masking functionality added by @dwringer."""
+
+ latents_a: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ latents_b: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ mask: Optional[ImageField] = InputField(default=None, description="Mask for blending in latents B")
+ alpha: float = InputField(ge=0, default=0.5, description=FieldDescriptions.blend_alpha)
+
+ def prep_mask_tensor(self, mask_image: Image.Image) -> torch.Tensor:
+ if mask_image.mode != "L":
+ mask_image = mask_image.convert("L")
+ mask_tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False)
+ if mask_tensor.dim() == 3:
+ mask_tensor = mask_tensor.unsqueeze(0)
+ return mask_tensor
+
+ def replace_tensor_from_masked_tensor(
+ self, tensor: torch.Tensor, other_tensor: torch.Tensor, mask_tensor: torch.Tensor
+ ):
+ output = tensor.clone()
+ mask_tensor = mask_tensor.expand(output.shape)
+ if output.dtype != torch.float16:
+ output = torch.add(output, mask_tensor * torch.sub(other_tensor, tensor))
+ else:
+ output = torch.add(output, mask_tensor.half() * torch.sub(other_tensor, tensor))
+ return output
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents_a = context.tensors.load(self.latents_a.latents_name)
+ latents_b = context.tensors.load(self.latents_b.latents_name)
+ if self.mask is None:
+ mask_tensor = torch.zeros(latents_a.shape[-2:])
+ else:
+ mask_tensor = self.prep_mask_tensor(context.images.get_pil(self.mask.image_name))
+ mask_tensor = tv_resize(mask_tensor, latents_a.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
+
+ latents_b = self.replace_tensor_from_masked_tensor(latents_b, latents_a, mask_tensor)
+
+ if latents_a.shape != latents_b.shape:
+ raise ValueError("Latents to blend must be the same size.")
+
+ device = TorchDevice.choose_torch_device()
+
+ # blend
+ blended_latents = slerp(self.alpha, latents_a, latents_b, device)
+
+ # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
+ blended_latents = blended_latents.to("cpu")
+ torch.cuda.empty_cache()
+
+ name = context.tensors.save(tensor=blended_latents)
+ return LatentsOutput.build(latents_name=name, latents=blended_latents)
diff --git a/invokeai/app/invocations/canny.py b/invokeai/app/invocations/canny.py
new file mode 100644
index 00000000000..dbfde6d3539
--- /dev/null
+++ b/invokeai/app/invocations/canny.py
@@ -0,0 +1,34 @@
+import cv2
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.util import cv2_to_pil, pil_to_cv2
+
+
+@invocation(
+ "canny_edge_detection",
+ title="Canny Edge Detection",
+ tags=["controlnet", "canny"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class CannyEdgeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Geneartes an edge map using a cv2's Canny algorithm."""
+
+ image: ImageField = InputField(description="The image to process")
+ low_threshold: int = InputField(
+ default=100, ge=0, le=255, description="The low threshold of the Canny pixel gradient (0-255)"
+ )
+ high_threshold: int = InputField(
+ default=200, ge=0, le=255, description="The high threshold of the Canny pixel gradient (0-255)"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ np_img = pil_to_cv2(image)
+ edge_map = cv2.Canny(np_img, self.low_threshold, self.high_threshold)
+ edge_map_pil = cv2_to_pil(edge_map)
+ image_dto = context.images.save(image=edge_map_pil)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/canvas.py b/invokeai/app/invocations/canvas.py
new file mode 100644
index 00000000000..cf13c3334ff
--- /dev/null
+++ b/invokeai/app/invocations/canvas.py
@@ -0,0 +1,27 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation(
+ "canvas_output",
+ title="Canvas Output",
+ tags=["canvas", "output", "image"],
+ category="canvas",
+ version="1.0.0",
+ use_cache=False,
+)
+class CanvasOutputInvocation(BaseInvocation):
+ """Outputs an image to the canvas staging area.
+
+ Use this node in workflows intended for canvas workflow integration.
+ Connect the final image of your workflow to this node to send it
+ to the canvas staging area when run via 'Run Workflow on Canvas'."""
+
+ image: ImageField = InputField(description=FieldDescriptions.image)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ image_dto = context.images.save(image=image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/cogview4_denoise.py b/invokeai/app/invocations/cogview4_denoise.py
new file mode 100644
index 00000000000..c04210401be
--- /dev/null
+++ b/invokeai/app/invocations/cogview4_denoise.py
@@ -0,0 +1,377 @@
+from typing import Callable, Optional
+
+import torch
+import torchvision.transforms as tv_transforms
+from diffusers.models.transformers.transformer_cogview4 import CogView4Transformer2DModel
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ CogView4ConditioningField,
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import TransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.sampling_utils import clip_timestep_schedule_fractional
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import CogView4ConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "cogview4_denoise",
+ title="Denoise - CogView4",
+ tags=["image", "cogview4"],
+ category="latents",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class CogView4DenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Run the denoising process with a CogView4 model."""
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.latents, input=Input.Connection
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.noise, input=Input.Connection
+ )
+ # denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None, description=FieldDescriptions.denoise_mask, input=Input.Connection
+ )
+ 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)
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.cogview4_model, input=Input.Connection, title="Transformer"
+ )
+ positive_conditioning: CogView4ConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: CogView4ConditioningField = InputField(
+ description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ cfg_scale: float | list[float] = InputField(default=3.5, description=FieldDescriptions.cfg_scale, title="CFG Scale")
+ width: int = InputField(default=1024, multiple_of=32, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=32, description="Height of the generated image.")
+ steps: int = InputField(default=25, gt=0, description=FieldDescriptions.steps)
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ """Prepare the inpaint mask.
+ - Loads the mask
+ - Resizes if necessary
+ - Casts to same device/dtype as latents
+
+ Args:
+ context (InvocationContext): The invocation context, for loading the inpaint mask.
+ latents (torch.Tensor): A latent image tensor. Used to determine the target shape, device, and dtype for the
+ inpaint mask.
+
+ Returns:
+ torch.Tensor | None: Inpaint mask. Values of 0.0 represent the regions to be fully denoised, and 1.0
+ represent the regions to be preserved.
+ """
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+
+ # The input denoise_mask contains values in [0, 1], where 0.0 represents the regions to be fully denoised, and
+ # 1.0 represents the regions to be preserved.
+ # We invert the mask so that the regions to be preserved are 0.0 and the regions to be denoised are 1.0.
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask
+
+ def _load_text_conditioning(
+ self,
+ context: InvocationContext,
+ conditioning_name: str,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> torch.Tensor:
+ # Load the conditioning data.
+ cond_data = context.conditioning.load(conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ cogview4_conditioning = cond_data.conditionings[0]
+ assert isinstance(cogview4_conditioning, CogView4ConditioningInfo)
+ cogview4_conditioning = cogview4_conditioning.to(dtype=dtype, device=device)
+
+ return cogview4_conditioning.glm_embeds
+
+ def _get_noise(
+ self,
+ batch_size: int,
+ num_channels_latents: int,
+ height: int,
+ width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ seed: int,
+ ) -> torch.Tensor:
+ # We always generate noise on the same device and dtype then cast to ensure consistency across devices/dtypes.
+ rand_device = "cpu"
+ rand_dtype = torch.float16
+
+ return torch.randn(
+ batch_size,
+ num_channels_latents,
+ int(height) // LATENT_SCALE_FACTOR,
+ int(width) // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+ def _prepare_cfg_scale(self, num_timesteps: int) -> list[float]:
+ """Prepare the CFG scale list.
+
+ Args:
+ num_timesteps (int): The number of timesteps in the scheduler. Could be different from num_steps depending
+ on the scheduler used (e.g. higher order schedulers).
+
+ Returns:
+ list[float]: _description_
+ """
+ if isinstance(self.cfg_scale, float):
+ cfg_scale = [self.cfg_scale] * num_timesteps
+ elif isinstance(self.cfg_scale, list):
+ assert len(self.cfg_scale) == num_timesteps
+ cfg_scale = self.cfg_scale
+ else:
+ raise ValueError(f"Invalid CFG scale type: {type(self.cfg_scale)}")
+
+ return cfg_scale
+
+ def _convert_timesteps_to_sigmas(self, image_seq_len: int, timesteps: torch.Tensor) -> list[float]:
+ # The logic to prepare the timestep / sigma schedule is based on:
+ # https://github.com/huggingface/diffusers/blob/b38450d5d2e5b87d5ff7088ee5798c85587b9635/src/diffusers/pipelines/cogview4/pipeline_cogview4.py#L575-L595
+ # The default FlowMatchEulerDiscreteScheduler configs are based on:
+ # https://huggingface.co/THUDM/CogView4-6B/blob/fb6f57289c73ac6d139e8d81bd5a4602d1877847/scheduler/scheduler_config.json
+ # This implementation differs slightly from the original for the sake of simplicity (differs in terminal value
+ # handling, not quantizing timesteps to integers, etc.).
+
+ def calculate_timestep_shift(
+ image_seq_len: int, base_seq_len: int = 256, base_shift: float = 0.25, max_shift: float = 0.75
+ ) -> float:
+ m = (image_seq_len / base_seq_len) ** 0.5
+ mu = m * max_shift + base_shift
+ return mu
+
+ def time_shift_linear(mu: float, sigma: float, t: torch.Tensor) -> torch.Tensor:
+ return mu / (mu + (1 / t - 1) ** sigma)
+
+ mu = calculate_timestep_shift(image_seq_len)
+ sigmas = time_shift_linear(mu, 1.0, timesteps)
+ return sigmas.tolist()
+
+ def _run_diffusion(
+ self,
+ context: InvocationContext,
+ ):
+ inference_dtype = torch.bfloat16
+ device = TorchDevice.choose_torch_device()
+
+ transformer_info = context.models.load(self.transformer.transformer)
+ assert isinstance(transformer_info.model, CogView4Transformer2DModel)
+
+ # Load/process the conditioning data.
+ # TODO(ryand): Make CFG optional.
+ do_classifier_free_guidance = True
+ pos_prompt_embeds = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.positive_conditioning.conditioning_name,
+ dtype=inference_dtype,
+ device=device,
+ )
+ neg_prompt_embeds = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.negative_conditioning.conditioning_name,
+ dtype=inference_dtype,
+ device=device,
+ )
+
+ # Prepare misc. conditioning variables.
+ # TODO(ryand): We could expose these as params (like with SDXL). But, we should experiment to see if they are
+ # useful first.
+ original_size = torch.tensor([(self.height, self.width)], dtype=pos_prompt_embeds.dtype, device=device)
+ target_size = torch.tensor([(self.height, self.width)], dtype=pos_prompt_embeds.dtype, device=device)
+ crops_coords_top_left = torch.tensor([(0, 0)], dtype=pos_prompt_embeds.dtype, device=device)
+
+ # Prepare the timestep / sigma schedule.
+ patch_size = transformer_info.model.config.patch_size # type: ignore
+ assert isinstance(patch_size, int)
+ image_seq_len = ((self.height // LATENT_SCALE_FACTOR) * (self.width // LATENT_SCALE_FACTOR)) // (patch_size**2)
+ # We add an extra step to the end to account for the final timestep of 0.0.
+ timesteps: list[float] = torch.linspace(1, 0, self.steps + 1).tolist()
+ # Clip the timesteps schedule based on denoising_start and denoising_end.
+ timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
+ sigmas = self._convert_timesteps_to_sigmas(image_seq_len, torch.tensor(timesteps))
+ total_steps = len(timesteps) - 1
+
+ # Prepare the CFG scale list.
+ cfg_scale = self._prepare_cfg_scale(total_steps)
+
+ # Load the input latents, if provided.
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+
+ # Generate initial latent noise.
+ num_channels_latents = transformer_info.model.config.in_channels # type: ignore
+ assert isinstance(num_channels_latents, int)
+ noise = self._prepare_noise_tensor(context, num_channels_latents, inference_dtype, device)
+
+ # Prepare input latent image.
+ if init_latents is not None:
+ # Noise the init_latents by the appropriate amount for the first timestep.
+ s_0 = sigmas[0]
+ latents = s_0 * noise + (1.0 - s_0) * init_latents
+ else:
+ # init_latents are not provided, so we are not doing image-to-image (i.e. we are starting from pure noise).
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ latents = noise
+
+ # If len(timesteps) == 1, then short-circuit. We are just noising the input latents, but not taking any
+ # denoising steps.
+ if len(timesteps) <= 1:
+ return latents
+
+ # Prepare inpaint extension.
+ inpaint_mask = self._prep_inpaint_mask(context, latents)
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ assert init_latents is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents,
+ inpaint_mask=inpaint_mask,
+ noise=noise,
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=0,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(timesteps[0]),
+ latents=latents,
+ ),
+ )
+
+ with transformer_info.model_on_device() as (_, transformer):
+ assert isinstance(transformer, CogView4Transformer2DModel)
+
+ # Denoising loop
+ for step_idx in tqdm(range(total_steps)):
+ t_curr = timesteps[step_idx]
+ sigma_curr = sigmas[step_idx]
+ sigma_prev = sigmas[step_idx + 1]
+
+ # Expand the timestep to match the latent model input.
+ # Multiply by 1000 to match the default FlowMatchEulerDiscreteScheduler num_train_timesteps.
+ timestep = torch.tensor([t_curr * 1000], device=device).expand(latents.shape[0])
+
+ # TODO(ryand): Support both sequential and batched CFG inference.
+ noise_pred_cond = transformer(
+ hidden_states=latents,
+ encoder_hidden_states=pos_prompt_embeds,
+ timestep=timestep,
+ original_size=original_size,
+ target_size=target_size,
+ crop_coords=crops_coords_top_left,
+ return_dict=False,
+ )[0]
+
+ # Apply CFG.
+ if do_classifier_free_guidance:
+ noise_pred_uncond = transformer(
+ hidden_states=latents,
+ encoder_hidden_states=neg_prompt_embeds,
+ timestep=timestep,
+ original_size=original_size,
+ target_size=target_size,
+ crop_coords=crops_coords_top_left,
+ return_dict=False,
+ )[0]
+
+ noise_pred = noise_pred_uncond + cfg_scale[step_idx] * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ # Compute the previous noisy sample x_t -> x_t-1.
+ latents_dtype = latents.dtype
+ # TODO(ryand): Is casting to float32 necessary for precision/stability? I copied this from SD3.
+ latents = latents.to(dtype=torch.float32)
+ latents = latents + (sigma_prev - sigma_curr) * noise_pred
+ latents = latents.to(dtype=latents_dtype)
+
+ if inpaint_extension is not None:
+ latents = inpaint_extension.merge_intermediate_latents_with_init_latents(latents, sigma_prev)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(t_curr),
+ latents=latents,
+ ),
+ )
+
+ return latents
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, num_channels_latents: int, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "CogView4", self.width, self.height)
+ return noise
+
+ return self._get_noise(
+ batch_size=1,
+ num_channels_latents=num_channels_latents,
+ height=self.height,
+ width=self.width,
+ dtype=inference_dtype,
+ device=device,
+ seed=self.seed,
+ )
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.CogView4)
+
+ return step_callback
diff --git a/invokeai/app/invocations/cogview4_image_to_latents.py b/invokeai/app/invocations/cogview4_image_to_latents.py
new file mode 100644
index 00000000000..facbc38dd42
--- /dev/null
+++ b/invokeai/app/invocations/cogview4_image_to_latents.py
@@ -0,0 +1,76 @@
+import einops
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
+
+# TODO(ryand): This is effectively a copy of SD3ImageToLatentsInvocation and a subset of ImageToLatentsInvocation. We
+# should refactor to avoid this duplication.
+
+
+@invocation(
+ "cogview4_i2l",
+ title="Image to Latents - CogView4",
+ tags=["image", "latents", "vae", "i2l", "cogview4"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates latents from an image."""
+
+ image: ImageField = InputField(description="The image to encode.")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ assert isinstance(vae_info.model, AutoencoderKL)
+ estimated_working_memory = estimate_vae_working_memory_cogview4(
+ operation="encode", image_tensor=image_tensor, vae=vae_info.model
+ )
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoencoderKL)
+
+ vae.disable_tiling()
+
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype)
+ with torch.inference_mode():
+ image_tensor_dist = vae.encode(image_tensor).latent_dist
+ # TODO: Use seed to make sampling reproducible.
+ latents: torch.Tensor = image_tensor_dist.sample().to(dtype=vae.dtype)
+
+ latents = vae.config.scaling_factor * latents
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, AutoencoderKL)
+
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/cogview4_latents_to_image.py b/invokeai/app/invocations/cogview4_latents_to_image.py
new file mode 100644
index 00000000000..1b77ed8a1f8
--- /dev/null
+++ b/invokeai/app/invocations/cogview4_latents_to_image.py
@@ -0,0 +1,79 @@
+from contextlib import nullcontext
+
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
+
+# TODO(ryand): This is effectively a copy of SD3LatentsToImageInvocation and a subset of LatentsToImageInvocation. We
+# should refactor to avoid this duplication.
+
+
+@invocation(
+ "cogview4_l2i",
+ title="Latents to Image - CogView4",
+ tags=["latents", "image", "vae", "l2i", "cogview4"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class CogView4LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents."""
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, (AutoencoderKL))
+ estimated_working_memory = estimate_vae_working_memory_cogview4(
+ operation="decode", image_tensor=latents, vae=vae_info.model
+ )
+ with (
+ SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
+ vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),
+ ):
+ context.util.signal_progress("Running VAE")
+ assert isinstance(vae, (AutoencoderKL))
+ latents = latents.to(TorchDevice.choose_torch_device())
+
+ vae.disable_tiling()
+
+ tiling_context = nullcontext()
+
+ # clear memory as vae decode can request a lot
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode(), tiling_context:
+ # copied from diffusers pipeline
+ latents = latents / vae.config.scaling_factor
+ img = vae.decode(latents, return_dict=False)[0]
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c") # noqa: F821
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/cogview4_model_loader.py b/invokeai/app/invocations/cogview4_model_loader.py
new file mode 100644
index 00000000000..fbafcd345fd
--- /dev/null
+++ b/invokeai/app/invocations/cogview4_model_loader.py
@@ -0,0 +1,56 @@
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ GlmEncoderField,
+ ModelIdentifierField,
+ TransformerField,
+ VAEField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+
+
+@invocation_output("cogview4_model_loader_output")
+class CogView4ModelLoaderOutput(BaseInvocationOutput):
+ """CogView4 base model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ glm_encoder: GlmEncoderField = OutputField(description=FieldDescriptions.glm_encoder, title="GLM Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "cogview4_model_loader",
+ title="Main Model - CogView4",
+ tags=["model", "cogview4"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class CogView4ModelLoaderInvocation(BaseInvocation):
+ """Loads a CogView4 base model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.cogview4_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.CogView4,
+ ui_model_type=ModelType.Main,
+ )
+
+ def invoke(self, context: InvocationContext) -> CogView4ModelLoaderOutput:
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ glm_tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ glm_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+
+ return CogView4ModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ glm_encoder=GlmEncoderField(tokenizer=glm_tokenizer, text_encoder=glm_encoder),
+ vae=VAEField(vae=vae),
+ )
diff --git a/invokeai/app/invocations/cogview4_text_encoder.py b/invokeai/app/invocations/cogview4_text_encoder.py
new file mode 100644
index 00000000000..13234889fba
--- /dev/null
+++ b/invokeai/app/invocations/cogview4_text_encoder.py
@@ -0,0 +1,100 @@
+import torch
+from transformers import GlmModel, PreTrainedTokenizerFast
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, UIComponent
+from invokeai.app.invocations.model import GlmEncoderField
+from invokeai.app.invocations.primitives import CogView4ConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ CogView4ConditioningInfo,
+ ConditioningFieldData,
+)
+
+# The CogView4 GLM Text Encoder max sequence length set based on the default in diffusers.
+COGVIEW4_GLM_MAX_SEQ_LEN = 1024
+
+
+@invocation(
+ "cogview4_text_encoder",
+ title="Prompt - CogView4",
+ tags=["prompt", "conditioning", "cogview4"],
+ category="prompt",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class CogView4TextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for a cogview4 image."""
+
+ prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
+ glm_encoder: GlmEncoderField = InputField(
+ title="GLM Encoder",
+ description=FieldDescriptions.glm_encoder,
+ input=Input.Connection,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> CogView4ConditioningOutput:
+ glm_embeds = self._glm_encode(context, max_seq_len=COGVIEW4_GLM_MAX_SEQ_LEN)
+ # Move embeddings to CPU for storage to save VRAM
+ glm_embeds = glm_embeds.detach().to("cpu")
+ conditioning_data = ConditioningFieldData(conditionings=[CogView4ConditioningInfo(glm_embeds=glm_embeds)])
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return CogView4ConditioningOutput.build(conditioning_name)
+
+ def _glm_encode(self, context: InvocationContext, max_seq_len: int) -> torch.Tensor:
+ prompt = [self.prompt]
+
+ # TODO(ryand): Add model inputs to the invocation rather than hard-coding.
+ glm_text_encoder_info = context.models.load(self.glm_encoder.text_encoder)
+ with (
+ glm_text_encoder_info.model_on_device() as (_, glm_text_encoder),
+ context.models.load(self.glm_encoder.tokenizer).model_on_device() as (_, glm_tokenizer),
+ ):
+ repaired_tensors = glm_text_encoder_info.repair_required_tensors_on_device()
+ device = get_effective_device(glm_text_encoder)
+ if repaired_tensors > 0:
+ context.logger.warning(
+ f"Recovered {repaired_tensors} required GLM tensor(s) onto {device} after a partial device mismatch."
+ )
+
+ context.util.signal_progress("Running GLM text encoder")
+ assert isinstance(glm_text_encoder, GlmModel)
+ assert isinstance(glm_tokenizer, PreTrainedTokenizerFast)
+
+ text_inputs = glm_tokenizer(
+ prompt,
+ padding="longest",
+ max_length=max_seq_len,
+ truncation=True,
+ add_special_tokens=True,
+ return_tensors="pt",
+ )
+ text_input_ids = text_inputs.input_ids
+ untruncated_ids = glm_tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
+ assert isinstance(text_input_ids, torch.Tensor)
+ assert isinstance(untruncated_ids, torch.Tensor)
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
+ text_input_ids, untruncated_ids
+ ):
+ removed_text = glm_tokenizer.batch_decode(untruncated_ids[:, max_seq_len - 1 : -1])
+ context.logger.warning(
+ "The following part of your input was truncated because `max_sequence_length` is set to "
+ f" {max_seq_len} tokens: {removed_text}"
+ )
+
+ current_length = text_input_ids.shape[1]
+ pad_length = (16 - (current_length % 16)) % 16
+ if pad_length > 0:
+ pad_ids = torch.full(
+ (text_input_ids.shape[0], pad_length),
+ fill_value=glm_tokenizer.pad_token_id,
+ dtype=text_input_ids.dtype,
+ device=text_input_ids.device,
+ )
+ text_input_ids = torch.cat([pad_ids, text_input_ids], dim=1)
+ prompt_embeds = glm_text_encoder(text_input_ids.to(device), output_hidden_states=True).hidden_states[-2]
+
+ assert isinstance(prompt_embeds, torch.Tensor)
+ return prompt_embeds
diff --git a/invokeai/app/invocations/collections.py b/invokeai/app/invocations/collections.py
new file mode 100644
index 00000000000..39e77f5b637
--- /dev/null
+++ b/invokeai/app/invocations/collections.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
+
+
+import numpy as np
+from pydantic import ValidationInfo, field_validator
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import InputField
+from invokeai.app.invocations.primitives import IntegerCollectionOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.misc import SEED_MAX
+
+
+@invocation("range", title="Integer Range", tags=["collection", "integer", "range"], category="batch", version="1.0.0")
+class RangeInvocation(BaseInvocation):
+ """Creates a range of numbers from start to stop with step"""
+
+ start: int = InputField(default=0, description="The start 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")
+
+ @field_validator("stop")
+ def stop_gt_start(cls, v: int, info: ValidationInfo):
+ if "start" in info.data and v <= info.data["start"]:
+ raise ValueError("stop must be greater than start")
+ return v
+
+ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
+ return IntegerCollectionOutput(collection=list(range(self.start, self.stop, self.step)))
+
+
+@invocation(
+ "range_of_size",
+ title="Integer Range of Size",
+ tags=["collection", "integer", "size", "range"],
+ category="batch",
+ version="1.0.0",
+)
+class RangeOfSizeInvocation(BaseInvocation):
+ """Creates a range from start to start + (size * step) incremented by step"""
+
+ start: int = InputField(default=0, description="The start of the range")
+ size: int = InputField(default=1, gt=0, description="The number of values")
+ step: int = InputField(default=1, description="The step of the range")
+
+ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
+ return IntegerCollectionOutput(
+ collection=list(range(self.start, self.start + (self.step * self.size), self.step))
+ )
+
+
+@invocation(
+ "random_range",
+ title="Random Range",
+ tags=["range", "integer", "random", "collection"],
+ category="batch",
+ version="1.0.1",
+ use_cache=False,
+)
+class RandomRangeInvocation(BaseInvocation):
+ """Creates a collection of random numbers"""
+
+ low: int = InputField(default=0, description="The inclusive low value")
+ high: int = InputField(default=np.iinfo(np.int32).max, description="The exclusive high value")
+ size: int = InputField(default=1, description="The number of values to generate")
+ seed: int = InputField(
+ default=0,
+ ge=0,
+ le=SEED_MAX,
+ description="The seed for the RNG (omit for random)",
+ )
+
+ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
+ rng = np.random.default_rng(self.seed)
+ return IntegerCollectionOutput(collection=list(rng.integers(low=self.low, high=self.high, size=self.size)))
diff --git a/invokeai/app/invocations/color_map.py b/invokeai/app/invocations/color_map.py
new file mode 100644
index 00000000000..ec95acfffd3
--- /dev/null
+++ b/invokeai/app/invocations/color_map.py
@@ -0,0 +1,41 @@
+import cv2
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.util import np_to_pil, pil_to_np
+
+
+@invocation(
+ "color_map",
+ title="Color Map",
+ tags=["controlnet"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class ColorMapInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates a color map from the provided image."""
+
+ image: ImageField = InputField(description="The image to process")
+ tile_size: int = InputField(default=64, ge=1, description=FieldDescriptions.tile_size)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+
+ np_image = pil_to_np(image)
+ height, width = np_image.shape[:2]
+
+ width_tile_size = min(self.tile_size, width)
+ height_tile_size = min(self.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_pil = np_to_pil(color_map)
+
+ image_dto = context.images.save(image=color_map_pil)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/compel.py b/invokeai/app/invocations/compel.py
new file mode 100644
index 00000000000..99373531d8e
--- /dev/null
+++ b/invokeai/app/invocations/compel.py
@@ -0,0 +1,534 @@
+from typing import Iterator, List, Optional, Tuple, Union, cast
+
+import torch
+from compel import Compel, ReturnedEmbeddingsType, SplitLongTextMode
+from compel.prompt_parser import Blend, Conjunction, CrossAttentionControlSubstitute, FlattenedPrompt, Fragment
+from transformers import CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import (
+ ConditioningField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ OutputField,
+ TensorField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import CLIPField
+from invokeai.app.invocations.primitives import ConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.ti_utils import generate_ti_list
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.model_patcher import ModelPatcher
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ BasicConditioningInfo,
+ ConditioningFieldData,
+ SDXLConditioningInfo,
+)
+from invokeai.backend.util.devices import TorchDevice
+
+# unconditioned: Optional[torch.Tensor]
+
+
+# class ConditioningAlgo(str, Enum):
+# Compose = "compose"
+# ComposeEx = "compose_ex"
+# PerpNeg = "perp_neg"
+
+
+@invocation(
+ "compel",
+ title="Prompt - SD1.5",
+ tags=["prompt", "compel"],
+ category="prompt",
+ version="1.2.1",
+)
+class CompelInvocation(BaseInvocation):
+ """Parse prompt using compel package to conditioning."""
+
+ prompt: str = InputField(
+ default="",
+ description=FieldDescriptions.compel_prompt,
+ ui_component=UIComponent.Textarea,
+ )
+ clip: CLIPField = InputField(
+ title="CLIP",
+ description=FieldDescriptions.clip,
+ )
+ mask: Optional[TensorField] = InputField(
+ default=None, description="A mask defining the region that this conditioning prompt applies to."
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ConditioningOutput:
+ def _lora_loader() -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in self.clip.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+ return
+
+ # loras = [(context.models.get(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras]
+
+ text_encoder_info = context.models.load(self.clip.text_encoder)
+ ti_list = generate_ti_list(self.prompt, text_encoder_info.config.base, context)
+
+ with (
+ # apply all patches while the model is on the target device
+ text_encoder_info.model_on_device() as (cached_weights, text_encoder),
+ context.models.load(self.clip.tokenizer) as tokenizer,
+ LayerPatcher.apply_smart_model_patches(
+ model=text_encoder,
+ patches=_lora_loader(),
+ prefix="lora_te_",
+ dtype=text_encoder.dtype,
+ cached_weights=cached_weights,
+ ),
+ # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers.
+ ModelPatcher.apply_clip_skip(text_encoder, self.clip.skipped_layers),
+ ModelPatcher.apply_ti(tokenizer, text_encoder, ti_list) as (
+ patched_tokenizer,
+ ti_manager,
+ ),
+ ):
+ context.util.signal_progress("Building conditioning")
+ assert isinstance(text_encoder, CLIPTextModel)
+ assert isinstance(tokenizer, CLIPTokenizer)
+ compel = Compel(
+ tokenizer=patched_tokenizer,
+ text_encoder=text_encoder,
+ textual_inversion_manager=ti_manager,
+ dtype_for_device_getter=TorchDevice.choose_torch_dtype,
+ truncate_long_prompts=False,
+ device=get_effective_device(text_encoder),
+ split_long_text_mode=SplitLongTextMode.SENTENCES,
+ )
+
+ conjunction = Compel.parse_prompt_string(self.prompt)
+
+ if context.config.get().log_tokenization:
+ log_tokenization_for_conjunction(conjunction, patched_tokenizer)
+
+ c, _options = compel.build_conditioning_tensor_for_conjunction(conjunction)
+
+ del compel
+ del patched_tokenizer
+ del tokenizer
+ del ti_manager
+ del text_encoder
+ del text_encoder_info
+
+ c = c.detach().to("cpu")
+
+ conditioning_data = ConditioningFieldData(conditionings=[BasicConditioningInfo(embeds=c)])
+
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return ConditioningOutput(
+ conditioning=ConditioningField(
+ conditioning_name=conditioning_name,
+ mask=self.mask,
+ )
+ )
+
+
+class SDXLPromptInvocationBase:
+ """Prompt processor for SDXL models."""
+
+ def run_clip_compel(
+ self,
+ context: InvocationContext,
+ clip_field: CLIPField,
+ prompt: str,
+ get_pooled: bool,
+ lora_prefix: str,
+ zero_on_empty: bool,
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
+ text_encoder_info = context.models.load(clip_field.text_encoder)
+ # return zero on empty
+ if prompt == "" and zero_on_empty:
+ cpu_text_encoder = text_encoder_info.model
+ assert isinstance(cpu_text_encoder, torch.nn.Module)
+ c = torch.zeros(
+ (
+ 1,
+ cpu_text_encoder.config.max_position_embeddings,
+ cpu_text_encoder.config.hidden_size,
+ ),
+ dtype=cpu_text_encoder.dtype,
+ )
+ if get_pooled:
+ c_pooled = torch.zeros(
+ (1, cpu_text_encoder.config.hidden_size),
+ dtype=c.dtype,
+ )
+ else:
+ c_pooled = None
+ return c, c_pooled
+
+ def _lora_loader() -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in clip_field.loras:
+ lora_info = context.models.load(lora.lora)
+ lora_model = lora_info.model
+ assert isinstance(lora_model, ModelPatchRaw)
+ yield (lora_model, lora.weight)
+ del lora_info
+ return
+
+ # loras = [(context.models.get(**lora.dict(exclude={"weight"})).context.model, lora.weight) for lora in self.clip.loras]
+
+ ti_list = generate_ti_list(prompt, text_encoder_info.config.base, context)
+
+ with (
+ # apply all patches while the model is on the target device
+ text_encoder_info.model_on_device() as (cached_weights, text_encoder),
+ context.models.load(clip_field.tokenizer) as tokenizer,
+ LayerPatcher.apply_smart_model_patches(
+ model=text_encoder,
+ patches=_lora_loader(),
+ prefix=lora_prefix,
+ dtype=text_encoder.dtype,
+ cached_weights=cached_weights,
+ ),
+ # Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers.
+ ModelPatcher.apply_clip_skip(text_encoder, clip_field.skipped_layers),
+ ModelPatcher.apply_ti(tokenizer, text_encoder, ti_list) as (
+ patched_tokenizer,
+ ti_manager,
+ ),
+ ):
+ context.util.signal_progress("Building conditioning")
+ assert isinstance(text_encoder, (CLIPTextModel, CLIPTextModelWithProjection))
+ assert isinstance(tokenizer, CLIPTokenizer)
+
+ text_encoder = cast(CLIPTextModel, text_encoder)
+ compel = Compel(
+ tokenizer=patched_tokenizer,
+ text_encoder=text_encoder,
+ textual_inversion_manager=ti_manager,
+ dtype_for_device_getter=TorchDevice.choose_torch_dtype,
+ truncate_long_prompts=False, # TODO:
+ returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED, # TODO: clip skip
+ requires_pooled=get_pooled,
+ device=get_effective_device(text_encoder),
+ split_long_text_mode=SplitLongTextMode.SENTENCES,
+ )
+
+ conjunction = Compel.parse_prompt_string(prompt)
+
+ if context.config.get().log_tokenization:
+ # TODO: better logging for and syntax
+ log_tokenization_for_conjunction(conjunction, patched_tokenizer)
+
+ # TODO: ask for optimizations? to not run text_encoder twice
+ c, _options = compel.build_conditioning_tensor_for_conjunction(conjunction)
+ if get_pooled:
+ c_pooled = compel.conditioning_provider.get_pooled_embeddings([prompt])
+ else:
+ c_pooled = None
+
+ del compel
+ del patched_tokenizer
+ del tokenizer
+ del ti_manager
+ del text_encoder
+ del text_encoder_info
+
+ c = c.detach().to("cpu")
+ if c_pooled is not None:
+ c_pooled = c_pooled.detach().to("cpu")
+
+ return c, c_pooled
+
+
+@invocation(
+ "sdxl_compel_prompt",
+ title="Prompt - SDXL",
+ tags=["sdxl", "compel", "prompt"],
+ category="prompt",
+ version="1.2.1",
+)
+class SDXLCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
+ """Parse prompt using compel package to conditioning."""
+
+ prompt: str = InputField(
+ default="",
+ 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_height: int = InputField(default=1024, description="")
+ crop_top: int = InputField(default=0, description="")
+ crop_left: int = InputField(default=0, description="")
+ target_width: int = InputField(default=1024, description="")
+ target_height: int = InputField(default=1024, description="")
+ clip: CLIPField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 1")
+ clip2: CLIPField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP 2")
+ mask: Optional[TensorField] = InputField(
+ default=None, description="A mask defining the region that this conditioning prompt applies to."
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ConditioningOutput:
+ c1, c1_pooled = self.run_clip_compel(context, self.clip, self.prompt, False, "lora_te1_", zero_on_empty=True)
+ if self.style.strip() == "":
+ c2, c2_pooled = self.run_clip_compel(
+ context, self.clip2, self.prompt, True, "lora_te2_", zero_on_empty=True
+ )
+ else:
+ c2, c2_pooled = self.run_clip_compel(context, self.clip2, self.style, True, "lora_te2_", zero_on_empty=True)
+
+ original_size = (self.original_height, self.original_width)
+ crop_coords = (self.crop_top, self.crop_left)
+ target_size = (self.target_height, self.target_width)
+
+ add_time_ids = torch.tensor([original_size + crop_coords + target_size])
+
+ # [1, 77, 768], [1, 154, 1280]
+ if c1.shape[1] < c2.shape[1]:
+ c1 = torch.cat(
+ [
+ c1,
+ torch.zeros(
+ (c1.shape[0], c2.shape[1] - c1.shape[1], c1.shape[2]),
+ device=c1.device,
+ dtype=c1.dtype,
+ ),
+ ],
+ dim=1,
+ )
+
+ elif c1.shape[1] > c2.shape[1]:
+ c2 = torch.cat(
+ [
+ c2,
+ torch.zeros(
+ (c2.shape[0], c1.shape[1] - c2.shape[1], c2.shape[2]),
+ device=c2.device,
+ dtype=c2.dtype,
+ ),
+ ],
+ dim=1,
+ )
+
+ assert c2_pooled is not None
+ conditioning_data = ConditioningFieldData(
+ conditionings=[
+ SDXLConditioningInfo(
+ embeds=torch.cat([c1, c2], dim=-1), pooled_embeds=c2_pooled, add_time_ids=add_time_ids
+ )
+ ]
+ )
+
+ conditioning_name = context.conditioning.save(conditioning_data)
+
+ return ConditioningOutput(
+ conditioning=ConditioningField(
+ conditioning_name=conditioning_name,
+ mask=self.mask,
+ )
+ )
+
+
+@invocation(
+ "sdxl_refiner_compel_prompt",
+ title="Prompt - SDXL Refiner",
+ tags=["sdxl", "compel", "prompt"],
+ category="prompt",
+ version="1.1.2",
+)
+class SDXLRefinerCompelPromptInvocation(BaseInvocation, SDXLPromptInvocationBase):
+ """Parse prompt using compel package to conditioning."""
+
+ style: str = InputField(
+ default="",
+ description=FieldDescriptions.compel_prompt,
+ ui_component=UIComponent.Textarea,
+ ) # TODO: ?
+ original_width: int = InputField(default=1024, description="")
+ original_height: int = InputField(default=1024, description="")
+ crop_top: int = InputField(default=0, description="")
+ crop_left: int = InputField(default=0, description="")
+ aesthetic_score: float = InputField(default=6.0, description=FieldDescriptions.sdxl_aesthetic)
+ clip2: CLIPField = InputField(description=FieldDescriptions.clip, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ConditioningOutput:
+ # TODO: if there will appear lora for refiner - write proper prefix
+ c2, c2_pooled = self.run_clip_compel(context, self.clip2, self.style, True, "", zero_on_empty=False)
+
+ original_size = (self.original_height, self.original_width)
+ crop_coords = (self.crop_top, self.crop_left)
+
+ add_time_ids = torch.tensor([original_size + crop_coords + (self.aesthetic_score,)])
+
+ assert c2_pooled is not None
+ conditioning_data = ConditioningFieldData(
+ conditionings=[SDXLConditioningInfo(embeds=c2, pooled_embeds=c2_pooled, add_time_ids=add_time_ids)]
+ )
+
+ conditioning_name = context.conditioning.save(conditioning_data)
+
+ return ConditioningOutput.build(conditioning_name)
+
+
+@invocation_output("clip_skip_output")
+class CLIPSkipInvocationOutput(BaseInvocationOutput):
+ """CLIP skip node output"""
+
+ clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
+
+
+@invocation(
+ "clip_skip",
+ title="Apply CLIP Skip - SD1.5, SDXL",
+ tags=["clipskip", "clip", "skip"],
+ category="prompt",
+ version="1.1.1",
+)
+class CLIPSkipInvocation(BaseInvocation):
+ """Skip layers in clip text_encoder model."""
+
+ clip: CLIPField = InputField(description=FieldDescriptions.clip, input=Input.Connection, title="CLIP")
+ skipped_layers: int = InputField(default=0, ge=0, description=FieldDescriptions.skipped_layers)
+
+ def invoke(self, context: InvocationContext) -> CLIPSkipInvocationOutput:
+ self.clip.skipped_layers += self.skipped_layers
+ return CLIPSkipInvocationOutput(
+ clip=self.clip,
+ )
+
+
+def get_max_token_count(
+ tokenizer: CLIPTokenizer,
+ prompt: Union[FlattenedPrompt, Blend, Conjunction],
+ truncate_if_too_long: bool = False,
+) -> int:
+ if type(prompt) is Blend:
+ blend: Blend = prompt
+ return max([get_max_token_count(tokenizer, p, truncate_if_too_long) for p in blend.prompts])
+ elif type(prompt) is Conjunction:
+ conjunction: Conjunction = prompt
+ return sum([get_max_token_count(tokenizer, p, truncate_if_too_long) for p in conjunction.prompts])
+ else:
+ return len(get_tokens_for_prompt_object(tokenizer, prompt, truncate_if_too_long))
+
+
+def get_tokens_for_prompt_object(
+ tokenizer: CLIPTokenizer, parsed_prompt: FlattenedPrompt, truncate_if_too_long: bool = True
+) -> List[str]:
+ if type(parsed_prompt) is Blend:
+ raise ValueError("Blend is not supported here - you need to get tokens for each of its .children")
+
+ text_fragments = [
+ (
+ x.text
+ if type(x) is Fragment
+ else (" ".join([f.text for f in x.original]) if type(x) is CrossAttentionControlSubstitute else str(x))
+ )
+ for x in parsed_prompt.children
+ ]
+ text = " ".join(text_fragments)
+ tokens: List[str] = tokenizer.tokenize(text)
+ if truncate_if_too_long:
+ max_tokens_length = tokenizer.model_max_length - 2 # typically 75
+ tokens = tokens[0:max_tokens_length]
+ return tokens
+
+
+def log_tokenization_for_conjunction(
+ c: Conjunction, tokenizer: CLIPTokenizer, display_label_prefix: Optional[str] = None
+) -> None:
+ display_label_prefix = display_label_prefix or ""
+ for i, p in enumerate(c.prompts):
+ if len(c.prompts) > 1:
+ this_display_label_prefix = f"{display_label_prefix}(conjunction part {i + 1}, weight={c.weights[i]})"
+ else:
+ assert display_label_prefix is not None
+ this_display_label_prefix = display_label_prefix
+ log_tokenization_for_prompt_object(p, tokenizer, display_label_prefix=this_display_label_prefix)
+
+
+def log_tokenization_for_prompt_object(
+ p: Union[Blend, FlattenedPrompt], tokenizer: CLIPTokenizer, display_label_prefix: Optional[str] = None
+) -> None:
+ display_label_prefix = display_label_prefix or ""
+ if type(p) is Blend:
+ blend: Blend = p
+ for i, c in enumerate(blend.prompts):
+ log_tokenization_for_prompt_object(
+ c,
+ tokenizer,
+ display_label_prefix=f"{display_label_prefix}(blend part {i + 1}, weight={blend.weights[i]})",
+ )
+ elif type(p) is FlattenedPrompt:
+ flattened_prompt: FlattenedPrompt = p
+ if flattened_prompt.wants_cross_attention_control:
+ original_fragments = []
+ edited_fragments = []
+ for f in flattened_prompt.children:
+ if type(f) is CrossAttentionControlSubstitute:
+ original_fragments += f.original
+ edited_fragments += f.edited
+ else:
+ original_fragments.append(f)
+ edited_fragments.append(f)
+
+ original_text = " ".join([x.text for x in original_fragments])
+ log_tokenization_for_text(
+ original_text,
+ tokenizer,
+ display_label=f"{display_label_prefix}(.swap originals)",
+ )
+ edited_text = " ".join([x.text for x in edited_fragments])
+ log_tokenization_for_text(
+ edited_text,
+ tokenizer,
+ display_label=f"{display_label_prefix}(.swap replacements)",
+ )
+ else:
+ text = " ".join([x.text for x in flattened_prompt.children])
+ log_tokenization_for_text(text, tokenizer, display_label=display_label_prefix)
+
+
+def log_tokenization_for_text(
+ text: str,
+ tokenizer: CLIPTokenizer,
+ display_label: Optional[str] = None,
+ truncate_if_too_long: Optional[bool] = False,
+) -> None:
+ """shows how the prompt is tokenized
+ # usually tokens have '' to indicate end-of-word,
+ # but for readability it has been replaced with ' '
+ """
+ tokens = tokenizer.tokenize(text)
+ tokenized = ""
+ discarded = ""
+ usedTokens = 0
+ totalTokens = len(tokens)
+
+ for i in range(0, totalTokens):
+ token = tokens[i].replace("", " ")
+ # alternate color
+ s = (usedTokens % 6) + 1
+ if truncate_if_too_long and i >= tokenizer.model_max_length:
+ discarded = discarded + f"\x1b[0;3{s};40m{token}"
+ else:
+ tokenized = tokenized + f"\x1b[0;3{s};40m{token}"
+ usedTokens += 1
+
+ if usedTokens > 0:
+ print(f"\n>> [TOKENLOG] Tokens {display_label or ''} ({usedTokens}):")
+ print(f"{tokenized}\x1b[0m")
+
+ if discarded != "":
+ print(f"\n>> [TOKENLOG] Tokens Discarded ({totalTokens - usedTokens}):")
+ print(f"{discarded}\x1b[0m")
diff --git a/invokeai/app/invocations/composition-nodes.py b/invokeai/app/invocations/composition-nodes.py
new file mode 100644
index 00000000000..babbf29151a
--- /dev/null
+++ b/invokeai/app/invocations/composition-nodes.py
@@ -0,0 +1,1545 @@
+# All nodes in this file are originally pulled from https://github.com/dwringer/composition-nodes
+
+import os
+from ast import literal_eval as tuple_from_string
+from functools import reduce
+from io import BytesIO
+from math import pi as PI
+from typing import Literal, Optional
+
+import cv2
+import numpy
+import torch
+from PIL import Image, ImageChops, ImageCms, ImageColor, ImageDraw, ImageEnhance, ImageOps
+from torchvision.transforms.functional import to_pil_image as pil_image_from_tensor
+
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.backend.image_util.color_conversion import (
+ hsl_from_srgb,
+ linear_srgb_from_oklab,
+ linear_srgb_from_oklch,
+ linear_srgb_from_srgb,
+ okhsl_from_srgb,
+ okhsv_from_srgb,
+ oklab_from_linear_srgb,
+ oklab_from_oklch,
+ oklch_from_oklab,
+ srgb_from_hsl,
+ srgb_from_okhsl,
+ srgb_from_okhsv,
+)
+from invokeai.backend.image_util.composition import (
+ CIELAB_TO_UPLAB_ICC_PATH,
+ MAX_FLOAT,
+ equivalent_achromatic_lightness,
+ gamut_clip_tensor,
+ remove_nans,
+ srgb_from_linear_srgb,
+ tensor_from_pil_image,
+)
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.invocation_api import (
+ BaseInvocation,
+ ImageField,
+ InputField,
+ InvocationContext,
+ WithBoard,
+ WithMetadata,
+ invocation,
+)
+
+HUE_COLOR_SPACES = Literal[
+ "HSV / HSL / RGB",
+ "Okhsl",
+ "Okhsv",
+ "*Oklch / Oklab",
+ "*LCh / CIELab",
+ "*UPLab (w/CIELab_to_UPLab.icc)",
+]
+
+
+@invocation(
+ "invokeai_img_hue_adjust_plus",
+ title="Adjust Image Hue Plus",
+ tags=["image", "hue", "oklab", "cielab", "uplab", "lch", "hsv", "hsl", "lab"],
+ category="image",
+ version="1.2.0",
+)
+class InvokeAdjustImageHuePlusInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Adjusts the Hue of an image by rotating it in the selected color space. Originally created by @dwringer"""
+
+ image: ImageField = InputField(description="The image to adjust")
+ space: HUE_COLOR_SPACES = InputField(
+ default="HSV / HSL / RGB",
+ description="Color space in which to rotate hue by polar coords (*: non-invertible)",
+ )
+ degrees: float = InputField(default=0.0, description="Degrees by which to rotate image hue")
+ preserve_lightness: bool = InputField(default=False, description="Whether to preserve CIELAB lightness values")
+ ok_adaptive_gamut: float = InputField(
+ ge=0, default=0.05, description="Higher preserves chroma at the expense of lightness (Oklab)"
+ )
+ ok_high_precision: bool = InputField(
+ default=True, description="Use more steps in computing gamut (Oklab/Okhsv/Okhsl)"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_in = context.images.get_pil(self.image.image_name)
+ image_out = None
+ space = self.space.split()[0].lower().strip("*")
+
+ # Keep the mode and alpha channel for restoration after shifting the hue:
+ image_mode = image_in.mode
+ original_mode = image_mode
+ alpha_channel = None
+ if (image_mode == "RGBA") or (image_mode == "LA") or (image_mode == "PA"):
+ alpha_channel = image_in.getchannel("A")
+ elif (image_mode == "RGBa") or (image_mode == "La") or (image_mode == "Pa"):
+ alpha_channel = image_in.getchannel("a")
+ if (image_mode == "RGBA") or (image_mode == "RGBa"):
+ image_mode = "RGB"
+ elif (image_mode == "LA") or (image_mode == "La"):
+ image_mode = "L"
+ elif image_mode == "PA":
+ image_mode = "P"
+
+ image_in = image_in.convert("RGB")
+
+ # Keep the CIELAB L* lightness channel for restoration if Preserve Lightness is selected:
+ (channel_l, channel_a, channel_b, profile_srgb, profile_lab, profile_uplab, lab_transform, uplab_transform) = (
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ if self.preserve_lightness or (space == "lch") or (space == "uplab"):
+ profile_srgb = ImageCms.createProfile("sRGB")
+ if space == "uplab":
+ with open(CIELAB_TO_UPLAB_ICC_PATH, "rb") as f:
+ profile_uplab = ImageCms.getOpenProfile(f)
+ if profile_uplab is None:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=6500)
+ else:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=5000)
+
+ lab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_srgb, profile_lab, "RGB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_in, lab_transform)
+ if profile_uplab is not None:
+ uplab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_uplab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, uplab_transform)
+
+ channel_l = image_out.getchannel("L")
+ channel_a = image_out.getchannel("A")
+ channel_b = image_out.getchannel("B")
+
+ if space == "hsv":
+ hsv_tensor = image_resized_to_grid_as_tensor(image_in.convert("HSV"), normalize=False, multiple_of=1)
+ hsv_tensor[0, :, :] = torch.remainder(torch.add(hsv_tensor[0, :, :] * 360.0, self.degrees), 360.0) / 360.0
+ image_out = pil_image_from_tensor(hsv_tensor, mode="HSV").convert("RGB")
+
+ elif space == "okhsl":
+ rgb_tensor = image_resized_to_grid_as_tensor(image_in.convert("RGB"), normalize=False, multiple_of=1)
+ hsl_tensor = okhsl_from_srgb(rgb_tensor, steps=(3 if self.ok_high_precision else 1))
+ hsl_tensor[0, :, :] = torch.remainder(torch.add(hsl_tensor[0, :, :], self.degrees), 360.0)
+ rgb_tensor = srgb_from_okhsl(hsl_tensor, alpha=0.0)
+ image_out = pil_image_from_tensor(rgb_tensor, mode="RGB")
+
+ elif space == "okhsv":
+ rgb_tensor = image_resized_to_grid_as_tensor(image_in.convert("RGB"), normalize=False, multiple_of=1)
+ hsv_tensor = okhsv_from_srgb(rgb_tensor, steps=(3 if self.ok_high_precision else 1))
+ hsv_tensor[0, :, :] = torch.remainder(torch.add(hsv_tensor[0, :, :], self.degrees), 360.0)
+ rgb_tensor = srgb_from_okhsv(hsv_tensor, alpha=0.0)
+ image_out = pil_image_from_tensor(rgb_tensor, mode="RGB")
+
+ elif (space == "lch") or (space == "uplab"):
+ #
+
+ a_tensor = image_resized_to_grid_as_tensor(channel_a, normalize=True, multiple_of=1)
+ b_tensor = image_resized_to_grid_as_tensor(channel_b, normalize=True, multiple_of=1)
+
+ # L*a*b* to L*C*h
+ c_tensor = torch.sqrt(torch.add(torch.pow(a_tensor, 2.0), torch.pow(b_tensor, 2.0)))
+ h_tensor = torch.atan2(b_tensor, a_tensor)
+
+ # Rotate h
+ rot_rads = (self.degrees / 180.0) * PI
+
+ h_rot = torch.add(h_tensor, rot_rads)
+ h_rot = torch.sub(torch.remainder(torch.add(h_rot, PI), 2 * PI), PI)
+
+ # L*C*h to L*a*b*
+ a_tensor = torch.mul(c_tensor, torch.cos(h_rot))
+ b_tensor = torch.mul(c_tensor, torch.sin(h_rot))
+
+ # -1..1 -> 0..1 for all elts of a, b
+ a_tensor = torch.div(torch.add(a_tensor, 1.0), 2.0)
+ b_tensor = torch.div(torch.add(b_tensor, 1.0), 2.0)
+
+ a_img = pil_image_from_tensor(a_tensor)
+ b_img = pil_image_from_tensor(b_tensor)
+
+ image_out = Image.merge("LAB", (channel_l, a_img, b_img))
+
+ if profile_uplab is not None:
+ deuplab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_uplab, profile_lab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, deuplab_transform)
+
+ rgb_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_srgb, "LAB", "RGB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, rgb_transform)
+
+ elif space == "oklch":
+ rgb_tensor = image_resized_to_grid_as_tensor(image_in.convert("RGB"), normalize=False, multiple_of=1)
+
+ linear_srgb_tensor = linear_srgb_from_srgb(rgb_tensor)
+ oklch_tensor = oklch_from_oklab(oklab_from_linear_srgb(linear_srgb_tensor))
+ oklch_tensor[2, :, :] = torch.remainder(torch.add(oklch_tensor[2, :, :], self.degrees), 360.0)
+ linear_srgb_tensor = linear_srgb_from_oklch(oklch_tensor)
+
+ rgb_tensor = srgb_from_linear_srgb(
+ linear_srgb_tensor, alpha=self.ok_adaptive_gamut, steps=(3 if self.ok_high_precision else 1)
+ )
+
+ image_out = pil_image_from_tensor(rgb_tensor, mode="RGB")
+
+ # Not all modes can convert directly to LAB using pillow:
+ # image_out = image_out.convert("RGB")
+
+ # Restore the L* channel if required:
+ if self.preserve_lightness and (not ((space == "lch") or (space == "uplab"))):
+ if profile_uplab is None:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=6500)
+ else:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=5000)
+
+ lab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_srgb, profile_lab, "RGB", "LAB", renderingIntent=2, flags=0x2400
+ )
+
+ image_out = ImageCms.applyTransform(image_out, lab_transform)
+
+ if profile_uplab is not None:
+ uplab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_uplab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, uplab_transform)
+
+ image_out = Image.merge("LAB", tuple([channel_l] + [image_out.getchannel(c) for c in "AB"]))
+
+ if profile_uplab is not None:
+ deuplab_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_uplab, profile_lab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, deuplab_transform)
+
+ rgb_transform = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_srgb, "LAB", "RGB", renderingIntent=2, flags=0x2400
+ )
+ image_out = ImageCms.applyTransform(image_out, rgb_transform)
+
+ # Restore the original image mode, with alpha channel if required:
+ image_out = image_out.convert(image_mode)
+ if "a" in original_mode.lower():
+ image_out = Image.merge(
+ original_mode, tuple([image_out.getchannel(c) for c in image_mode] + [alpha_channel])
+ )
+
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "invokeai_img_enhance",
+ title="Enhance Image",
+ tags=["enhance", "image"],
+ category="image",
+ version="1.2.1",
+)
+class InvokeImageEnhanceInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Applies processing from PIL's ImageEnhance module. Originally created by @dwringer"""
+
+ image: ImageField = InputField(description="The image for which to apply processing")
+ invert: bool = InputField(default=False, description="Whether to invert the image colors")
+ color: float = InputField(ge=0, default=1.0, description="Color enhancement factor")
+ contrast: float = InputField(ge=0, default=1.0, description="Contrast enhancement factor")
+ brightness: float = InputField(ge=0, default=1.0, description="Brightness enhancement factor")
+ sharpness: float = InputField(ge=0, default=1.0, description="Sharpness enhancement factor")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_out = context.images.get_pil(self.image.image_name)
+ if self.invert:
+ if image_out.mode not in ("L", "RGB"):
+ image_out = image_out.convert("RGB")
+ image_out = ImageOps.invert(image_out)
+ if self.color != 1.0:
+ color_enhancer = ImageEnhance.Color(image_out)
+ image_out = color_enhancer.enhance(self.color)
+ if self.contrast != 1.0:
+ contrast_enhancer = ImageEnhance.Contrast(image_out)
+ image_out = contrast_enhancer.enhance(self.contrast)
+ if self.brightness != 1.0:
+ brightness_enhancer = ImageEnhance.Brightness(image_out)
+ image_out = brightness_enhancer.enhance(self.brightness)
+ if self.sharpness != 1.0:
+ sharpness_enhancer = ImageEnhance.Sharpness(image_out)
+ image_out = sharpness_enhancer.enhance(self.sharpness)
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "invokeai_ealightness",
+ title="Equivalent Achromatic Lightness",
+ tags=["image", "channel", "mask", "cielab", "lab"],
+ category="image",
+ version="1.2.0",
+)
+class InvokeEquivalentAchromaticLightnessInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Calculate Equivalent Achromatic Lightness from image. Originally created by @dwringer"""
+
+ image: ImageField = InputField(description="Image from which to get channel")
+
+ # The chroma, C*
+ # , and the hue, h, in the CIELAB color space are obtained by C*=sqrt((a*)^2+(b*)^2)
+ # and h=arctan(b*/a*)
+ # k 0.1644 0.0603 0.1307 0.0060
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_in = context.images.get_pil(self.image.image_name)
+
+ if image_in.mode == "L":
+ image_in = image_in.convert("RGB")
+
+ image_out = image_in.convert("LAB")
+ channel_l = image_out.getchannel("L")
+ channel_a = image_out.getchannel("A")
+ channel_b = image_out.getchannel("B")
+
+ l_tensor = image_resized_to_grid_as_tensor(channel_l, normalize=False, multiple_of=1)
+ l_max = torch.ones(l_tensor.shape)
+ l_min = torch.zeros(l_tensor.shape)
+ a_tensor = image_resized_to_grid_as_tensor(channel_a, normalize=True, multiple_of=1)
+ b_tensor = image_resized_to_grid_as_tensor(channel_b, normalize=True, multiple_of=1)
+
+ c_tensor = torch.sqrt(torch.add(torch.pow(a_tensor, 2.0), torch.pow(b_tensor, 2.0)))
+ h_tensor = torch.atan2(b_tensor, a_tensor)
+
+ k = [0.1644, 0.0603, 0.1307, 0.0060]
+
+ h_minus_90 = torch.sub(h_tensor, PI / 2.0)
+ h_minus_90 = torch.sub(torch.remainder(torch.add(h_minus_90, 3 * PI), 2 * PI), PI)
+
+ f_by = torch.add(k[0] * torch.abs(torch.sin(torch.div(h_minus_90, 2.0))), k[1])
+ f_r_0 = torch.add(k[2] * torch.abs(torch.cos(h_tensor)), k[3])
+
+ f_r = torch.zeros(l_tensor.shape)
+ mask_hi = torch.ge(h_tensor, -1 * (PI / 2.0))
+ mask_lo = torch.le(h_tensor, PI / 2.0)
+ mask = torch.logical_and(mask_hi, mask_lo)
+ f_r[mask] = f_r_0[mask]
+
+ l_adjustment = torch.tensordot(torch.add(f_by, f_r), c_tensor, dims=([1, 2], [1, 2]))
+ l_max = torch.add(l_max, l_adjustment)
+ l_min = torch.add(l_min, l_adjustment)
+ image_tensor = torch.add(l_tensor, l_adjustment)
+
+ image_tensor = torch.div(torch.sub(image_tensor, l_min.min()), l_max.max() - l_min.min())
+
+ image_out = pil_image_from_tensor(image_tensor)
+
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
+
+
+BLEND_MODES = Literal[
+ "Normal",
+ "Lighten Only",
+ "Darken Only",
+ "Lighten Only (EAL)",
+ "Darken Only (EAL)",
+ "Hue",
+ "Saturation",
+ "Color",
+ "Luminosity",
+ "Linear Dodge (Add)",
+ "Subtract",
+ "Multiply",
+ "Divide",
+ "Screen",
+ "Overlay",
+ "Linear Burn",
+ "Difference",
+ "Hard Light",
+ "Soft Light",
+ "Vivid Light",
+ "Linear Light",
+ "Color Burn",
+ "Color Dodge",
+]
+
+BLEND_COLOR_SPACES = Literal[
+ "RGB", "Linear RGB", "HSL (RGB)", "HSV (RGB)", "Okhsl", "Okhsv", "Oklch (Oklab)", "LCh (CIELab)"
+]
+
+
+@invocation(
+ "invokeai_img_blend",
+ title="Image Layer Blend",
+ tags=["image", "blend", "layer", "alpha", "composite", "dodge", "burn"],
+ category="image",
+ version="1.2.0",
+)
+class InvokeImageBlendInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Blend two images together, with optional opacity, mask, and blend modes. Originally created by @dwringer"""
+
+ layer_upper: ImageField = InputField(description="The top image to blend", ui_order=1)
+ blend_mode: BLEND_MODES = InputField(default="Normal", description="Available blend modes", ui_order=2)
+ opacity: float = InputField(ge=0, default=1.0, description="Desired opacity of the upper layer", ui_order=3)
+ mask: Optional[ImageField] = InputField(
+ default=None, description="Optional mask, used to restrict areas from blending", ui_order=4
+ )
+ fit_to_width: bool = InputField(default=False, description="Scale upper layer to fit base width", ui_order=5)
+ fit_to_height: bool = InputField(default=True, description="Scale upper layer to fit base height", ui_order=6)
+ layer_base: ImageField = InputField(description="The bottom image to blend", ui_order=7)
+ color_space: BLEND_COLOR_SPACES = InputField(
+ default="RGB", description="Available color spaces for blend computations", ui_order=8
+ )
+ adaptive_gamut: float = InputField(
+ ge=0,
+ default=0.0,
+ description="Adaptive gamut clipping (0=off). Higher prioritizes chroma over lightness",
+ ui_order=9,
+ )
+ high_precision: bool = InputField(
+ default=True, description="Use more steps in computing gamut when possible", ui_order=10
+ )
+
+ def scale_and_pad_or_crop_to_base(self, image_upper: Image.Image, image_base: Image.Image):
+ """Rescale upper image based on self.fill_x and self.fill_y params"""
+
+ aspect_base = image_base.width / image_base.height
+ aspect_upper = image_upper.width / image_upper.height
+ if self.fit_to_width and self.fit_to_height:
+ image_upper = image_upper.resize((image_base.width, image_base.height))
+ elif (self.fit_to_width and (aspect_base < aspect_upper)) or (
+ self.fit_to_height and (aspect_upper <= aspect_base)
+ ):
+ image_upper = ImageOps.pad(
+ image_upper, (image_base.width, image_base.height), color=tuple([0 for band in image_upper.getbands()])
+ )
+ elif (self.fit_to_width and (aspect_upper <= aspect_base)) or (
+ self.fit_to_height and (aspect_base < aspect_upper)
+ ):
+ image_upper = ImageOps.fit(image_upper, (image_base.width, image_base.height))
+ return image_upper
+
+ def image_convert_with_xform(self, image_in: Image.Image, from_mode: str, to_mode: str):
+ """Use PIL ImageCms color management to convert 3-channel image from one mode to another"""
+
+ def fixed_mode(mode: str):
+ if mode.lower() == "srgb":
+ return "rgb"
+ elif mode.lower() == "cielab":
+ return "lab"
+ else:
+ return mode.lower()
+
+ from_mode, to_mode = fixed_mode(from_mode), fixed_mode(to_mode)
+
+ profile_srgb = None
+ profile_uplab = None
+ profile_lab = None
+ if (from_mode.lower() == "rgb") or (to_mode.lower() == "rgb"):
+ profile_srgb = ImageCms.createProfile("sRGB")
+ if (from_mode.lower() == "uplab") or (to_mode.lower() == "uplab"):
+ if os.path.isfile("CIELab_to_UPLab.icc"):
+ profile_uplab = ImageCms.getOpenProfile("CIELab_to_UPLab.icc")
+ if (from_mode.lower() in ["lab", "cielab", "uplab"]) or (to_mode.lower() in ["lab", "cielab", "uplab"]):
+ if profile_uplab is None:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=6500)
+ else:
+ profile_lab = ImageCms.createProfile("LAB", colorTemp=5000)
+
+ xform_rgb_to_lab = None
+ xform_uplab_to_lab = None
+ xform_lab_to_uplab = None
+ xform_lab_to_rgb = None
+ if from_mode == "rgb":
+ xform_rgb_to_lab = ImageCms.buildTransformFromOpenProfiles(
+ profile_srgb, profile_lab, "RGB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ elif from_mode == "uplab":
+ xform_uplab_to_lab = ImageCms.buildTransformFromOpenProfiles(
+ profile_uplab, profile_lab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ if to_mode == "uplab":
+ xform_lab_to_uplab = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_uplab, "LAB", "LAB", renderingIntent=2, flags=0x2400
+ )
+ elif to_mode == "rgb":
+ xform_lab_to_rgb = ImageCms.buildTransformFromOpenProfiles(
+ profile_lab, profile_srgb, "LAB", "RGB", renderingIntent=2, flags=0x2400
+ )
+
+ image_out = None
+ if (from_mode == "rgb") and (to_mode == "lab"):
+ image_out = ImageCms.applyTransform(image_in, xform_rgb_to_lab)
+ elif (from_mode == "rgb") and (to_mode == "uplab"):
+ image_out = ImageCms.applyTransform(image_in, xform_rgb_to_lab)
+ image_out = ImageCms.applyTransform(image_out, xform_lab_to_uplab)
+ elif (from_mode == "lab") and (to_mode == "uplab"):
+ image_out = ImageCms.applyTransform(image_in, xform_lab_to_uplab)
+ elif (from_mode == "lab") and (to_mode == "rgb"):
+ image_out = ImageCms.applyTransform(image_in, xform_lab_to_rgb)
+ elif (from_mode == "uplab") and (to_mode == "lab"):
+ image_out = ImageCms.applyTransform(image_in, xform_uplab_to_lab)
+ elif (from_mode == "uplab") and (to_mode == "rgb"):
+ image_out = ImageCms.applyTransform(image_in, xform_uplab_to_lab)
+ image_out = ImageCms.applyTransform(image_out, xform_lab_to_rgb)
+
+ return image_out
+
+ def prepare_tensors_from_images(
+ self,
+ image_upper: Image.Image,
+ image_lower: Image.Image,
+ mask_image: Optional[Image.Image] = None,
+ required: Optional[list[str]] = None,
+ ):
+ """Convert image to the necessary image space representations for blend calculations"""
+ required = required or ["hsv", "hsl", "lch", "oklch", "okhsl", "okhsv", "l_eal"]
+ alpha_upper, alpha_lower = None, None
+ if image_upper.mode == "RGBA":
+ # Prepare tensors to compute blend
+ image_rgba_upper = image_upper.convert("RGBA")
+ alpha_upper = image_rgba_upper.getchannel("A")
+ image_upper = image_upper.convert("RGB")
+ else:
+ if not (image_upper.mode == "RGB"):
+ image_upper = image_upper.convert("RGB")
+ if image_lower.mode == "RGBA":
+ # Prepare tensors to compute blend
+ image_rgba_lower = image_lower.convert("RGBA")
+ alpha_lower = image_rgba_lower.getchannel("A")
+ image_lower = image_lower.convert("RGB")
+ else:
+ if not (image_lower.mode == "RGB"):
+ image_lower = image_lower.convert("RGB")
+
+ image_lab_upper, image_lab_lower = None, None
+ upper_lab_tensor, lower_lab_tensor = None, None
+ upper_lch_tensor, lower_lch_tensor = None, None
+ if "lch" in required:
+ image_lab_upper, image_lab_lower = (
+ self.image_convert_with_xform(image_upper, "rgb", "lab"),
+ self.image_convert_with_xform(image_lower, "rgb", "lab"),
+ )
+
+ upper_lab_tensor = torch.stack(
+ [
+ tensor_from_pil_image(image_lab_upper.getchannel("L"), normalize=False)[0, :, :],
+ tensor_from_pil_image(image_lab_upper.getchannel("A"), normalize=True)[0, :, :],
+ tensor_from_pil_image(image_lab_upper.getchannel("B"), normalize=True)[0, :, :],
+ ]
+ )
+ lower_lab_tensor = torch.stack(
+ [
+ tensor_from_pil_image(image_lab_lower.getchannel("L"), normalize=False)[0, :, :],
+ tensor_from_pil_image(image_lab_lower.getchannel("A"), normalize=True)[0, :, :],
+ tensor_from_pil_image(image_lab_lower.getchannel("B"), normalize=True)[0, :, :],
+ ]
+ )
+ upper_lch_tensor = torch.stack(
+ [
+ upper_lab_tensor[0, :, :],
+ torch.sqrt(
+ torch.add(torch.pow(upper_lab_tensor[1, :, :], 2.0), torch.pow(upper_lab_tensor[2, :, :], 2.0))
+ ),
+ torch.atan2(upper_lab_tensor[2, :, :], upper_lab_tensor[1, :, :]),
+ ]
+ )
+ lower_lch_tensor = torch.stack(
+ [
+ lower_lab_tensor[0, :, :],
+ torch.sqrt(
+ torch.add(torch.pow(lower_lab_tensor[1, :, :], 2.0), torch.pow(lower_lab_tensor[2, :, :], 2.0))
+ ),
+ torch.atan2(lower_lab_tensor[2, :, :], lower_lab_tensor[1, :, :]),
+ ]
+ )
+
+ upper_l_eal_tensor, lower_l_eal_tensor = None, None
+ if "l_eal" in required:
+ upper_l_eal_tensor = equivalent_achromatic_lightness(upper_lch_tensor)
+ lower_l_eal_tensor = equivalent_achromatic_lightness(lower_lch_tensor)
+
+ image_hsv_upper, image_hsv_lower = None, None
+ upper_hsv_tensor, lower_hsv_tensor = None, None
+ if "hsv" in required:
+ image_hsv_upper, image_hsv_lower = image_upper.convert("HSV"), image_lower.convert("HSV")
+ upper_hsv_tensor = torch.stack(
+ [
+ tensor_from_pil_image(image_hsv_upper.getchannel("H"), normalize=False)[0, :, :] * 360.0,
+ tensor_from_pil_image(image_hsv_upper.getchannel("S"), normalize=False)[0, :, :],
+ tensor_from_pil_image(image_hsv_upper.getchannel("V"), normalize=False)[0, :, :],
+ ]
+ )
+ lower_hsv_tensor = torch.stack(
+ [
+ tensor_from_pil_image(image_hsv_lower.getchannel("H"), normalize=False)[0, :, :] * 360.0,
+ tensor_from_pil_image(image_hsv_lower.getchannel("S"), normalize=False)[0, :, :],
+ tensor_from_pil_image(image_hsv_lower.getchannel("V"), normalize=False)[0, :, :],
+ ]
+ )
+
+ upper_rgb_tensor = tensor_from_pil_image(image_upper, normalize=False)
+ lower_rgb_tensor = tensor_from_pil_image(image_lower, normalize=False)
+
+ alpha_upper_tensor, alpha_lower_tensor = None, None
+ if alpha_upper is None:
+ alpha_upper_tensor = torch.ones(upper_rgb_tensor[0, :, :].shape)
+ else:
+ alpha_upper_tensor = tensor_from_pil_image(alpha_upper, normalize=False)[0, :, :]
+ if alpha_lower is None:
+ alpha_lower_tensor = torch.ones(lower_rgb_tensor[0, :, :].shape)
+ else:
+ alpha_lower_tensor = tensor_from_pil_image(alpha_lower, normalize=False)[0, :, :]
+
+ mask_tensor = None
+ if mask_image is not None:
+ mask_tensor = tensor_from_pil_image(mask_image.convert("L"), normalize=False)[0, :, :]
+
+ upper_hsl_tensor, lower_hsl_tensor = None, None
+ if "hsl" in required:
+ upper_hsl_tensor = hsl_from_srgb(upper_rgb_tensor)
+ lower_hsl_tensor = hsl_from_srgb(lower_rgb_tensor)
+
+ upper_okhsl_tensor, lower_okhsl_tensor = None, None
+ if "okhsl" in required:
+ upper_okhsl_tensor = okhsl_from_srgb(upper_rgb_tensor, steps=(3 if self.high_precision else 1))
+ lower_okhsl_tensor = okhsl_from_srgb(lower_rgb_tensor, steps=(3 if self.high_precision else 1))
+
+ upper_okhsv_tensor, lower_okhsv_tensor = None, None
+ if "okhsv" in required:
+ upper_okhsv_tensor = okhsv_from_srgb(upper_rgb_tensor, steps=(3 if self.high_precision else 1))
+ lower_okhsv_tensor = okhsv_from_srgb(lower_rgb_tensor, steps=(3 if self.high_precision else 1))
+
+ upper_rgb_l_tensor = linear_srgb_from_srgb(upper_rgb_tensor)
+ lower_rgb_l_tensor = linear_srgb_from_srgb(lower_rgb_tensor)
+
+ upper_oklab_tensor, lower_oklab_tensor = None, None
+ upper_oklch_tensor, lower_oklch_tensor = None, None
+ if "oklch" in required:
+ upper_oklab_tensor = oklab_from_linear_srgb(upper_rgb_l_tensor)
+ lower_oklab_tensor = oklab_from_linear_srgb(lower_rgb_l_tensor)
+ upper_oklch_tensor = oklch_from_oklab(upper_oklab_tensor)
+ lower_oklch_tensor = oklch_from_oklab(lower_oklab_tensor)
+
+ return (
+ upper_rgb_l_tensor,
+ lower_rgb_l_tensor,
+ upper_rgb_tensor,
+ lower_rgb_tensor,
+ alpha_upper_tensor,
+ alpha_lower_tensor,
+ mask_tensor,
+ upper_hsv_tensor,
+ lower_hsv_tensor,
+ upper_hsl_tensor,
+ lower_hsl_tensor,
+ upper_lab_tensor,
+ lower_lab_tensor,
+ upper_lch_tensor,
+ lower_lch_tensor,
+ upper_l_eal_tensor,
+ lower_l_eal_tensor,
+ upper_oklab_tensor,
+ lower_oklab_tensor,
+ upper_oklch_tensor,
+ lower_oklch_tensor,
+ upper_okhsv_tensor,
+ lower_okhsv_tensor,
+ upper_okhsl_tensor,
+ lower_okhsl_tensor,
+ )
+
+ def apply_blend(self, image_tensors: torch.Tensor):
+ """Apply the selected blend mode using the appropriate color space representations"""
+
+ blend_mode = self.blend_mode
+ color_space = self.color_space.split()[0]
+ if (color_space in ["RGB", "Linear"]) and (blend_mode in ["Hue", "Saturation", "Luminosity", "Color"]):
+ color_space = "HSL"
+
+ def adaptive_clipped(rgb_tensor: torch.Tensor, clamp: bool = True, replace_with: float = MAX_FLOAT):
+ """Keep elements of the tensor finite"""
+
+ rgb_tensor = remove_nans(rgb_tensor, replace_with=replace_with)
+
+ if 0 < self.adaptive_gamut:
+ rgb_tensor = gamut_clip_tensor(
+ rgb_tensor, alpha=self.adaptive_gamut, steps=(3 if self.high_precision else 1)
+ )
+ rgb_tensor = remove_nans(rgb_tensor, replace_with=replace_with)
+ if clamp: # Use of MAX_FLOAT seems to lead to NaN's coming back in some cases:
+ rgb_tensor = rgb_tensor.clamp(0.0, 1.0)
+
+ return rgb_tensor
+
+ reassembly_function = {
+ "RGB": lambda t: linear_srgb_from_srgb(t),
+ "Linear": lambda t: t,
+ "HSL": lambda t: linear_srgb_from_srgb(srgb_from_hsl(t)),
+ "HSV": lambda t: linear_srgb_from_srgb(
+ tensor_from_pil_image(
+ pil_image_from_tensor(
+ torch.stack(
+ [
+ torch.remainder(t[0, :, :], 360.0) / 360.0,
+ t[1, :, :].clamp(0.0, 1.0),
+ t[2, :, :].clamp(0.0, 1.0),
+ ]
+ ),
+ mode="HSV",
+ ).convert("RGB"),
+ normalize=False,
+ )
+ ),
+ "Okhsl": lambda t: linear_srgb_from_srgb(
+ srgb_from_okhsl(t, alpha=self.adaptive_gamut, steps=(3 if self.high_precision else 1))
+ ),
+ "Okhsv": lambda t: linear_srgb_from_srgb(
+ srgb_from_okhsv(t, alpha=self.adaptive_gamut, steps=(3 if self.high_precision else 1))
+ ),
+ "Oklch": lambda t: linear_srgb_from_oklab(oklab_from_oklch(t)),
+ "LCh": lambda t: linear_srgb_from_srgb(
+ tensor_from_pil_image(
+ self.image_convert_with_xform(
+ Image.merge(
+ "LAB",
+ tuple(
+ pil_image_from_tensor(u)
+ for u in [
+ t[0, :, :].clamp(0.0, 1.0),
+ torch.div(torch.add(torch.mul(t[1, :, :], torch.cos(t[2, :, :])), 1.0), 2.0),
+ torch.div(torch.add(torch.mul(t[1, :, :], torch.sin(t[2, :, :])), 1.0), 2.0),
+ ]
+ ),
+ ),
+ "lab",
+ "rgb",
+ ),
+ normalize=False,
+ )
+ ),
+ }[color_space]
+
+ (
+ upper_rgb_l_tensor, # linear-light sRGB
+ lower_rgb_l_tensor, # linear-light sRGB
+ upper_rgb_tensor,
+ lower_rgb_tensor,
+ alpha_upper_tensor,
+ alpha_lower_tensor,
+ mask_tensor,
+ upper_hsv_tensor, # h_hsv_degrees, s_hsv, v_hsv
+ lower_hsv_tensor,
+ upper_hsl_tensor, # h_hsl_degrees, s_hsl, l_hsl
+ lower_hsl_tensor,
+ upper_lab_tensor, # l_lab, a_lab, b_lab
+ lower_lab_tensor,
+ upper_lch_tensor, # , c_lab, h_lab
+ lower_lch_tensor,
+ upper_l_eal_tensor, # l_eal
+ lower_l_eal_tensor,
+ upper_oklab_tensor, # l_oklab, a_oklab, b_oklab
+ lower_oklab_tensor,
+ upper_oklch_tensor, # l_oklab, c_oklab, h_oklab_degrees
+ lower_oklch_tensor,
+ upper_okhsv_tensor, # h_okhsv_degrees, s_okhsv, v_okhsv
+ lower_okhsv_tensor,
+ upper_okhsl_tensor, # h_okhsl_degrees, s_okhsl, l_r_oklab
+ lower_okhsl_tensor,
+ ) = image_tensors
+
+ current_space_tensors = {
+ "RGB": [upper_rgb_tensor, lower_rgb_tensor],
+ "Linear": [upper_rgb_l_tensor, lower_rgb_l_tensor],
+ "HSL": [upper_hsl_tensor, lower_hsl_tensor],
+ "HSV": [upper_hsv_tensor, lower_hsv_tensor],
+ "Okhsl": [upper_okhsl_tensor, lower_okhsl_tensor],
+ "Okhsv": [upper_okhsv_tensor, lower_okhsv_tensor],
+ "Oklch": [upper_oklch_tensor, lower_oklch_tensor],
+ "LCh": [upper_lch_tensor, lower_lch_tensor],
+ }[color_space]
+ upper_space_tensor = current_space_tensors[0]
+ lower_space_tensor = current_space_tensors[1]
+
+ lightness_index = {
+ "RGB": None,
+ "Linear": None,
+ "HSL": 2,
+ "HSV": 2,
+ "Okhsl": 2,
+ "Okhsv": 2,
+ "Oklch": 0,
+ "LCh": 0,
+ }[color_space]
+
+ saturation_index = {
+ "RGB": None,
+ "Linear": None,
+ "HSL": 1,
+ "HSV": 1,
+ "Okhsl": 1,
+ "Okhsv": 1,
+ "Oklch": 1,
+ "LCh": 1,
+ }[color_space]
+
+ hue_index = {
+ "RGB": None,
+ "Linear": None,
+ "HSL": 0,
+ "HSV": 0,
+ "Okhsl": 0,
+ "Okhsv": 0,
+ "Oklch": 2,
+ "LCh": 2,
+ }[color_space]
+
+ hue_period = {
+ "RGB": None,
+ "Linear": None,
+ "HSL": 360.0,
+ "HSV": 360.0,
+ "Okhsl": 360.0,
+ "Okhsv": 360.0,
+ "Oklch": 360.0,
+ "LCh": 2.0 * PI,
+ }[color_space]
+
+ if blend_mode == "Normal":
+ upper_rgb_l_tensor = reassembly_function(upper_space_tensor)
+
+ elif blend_mode == "Multiply":
+ upper_rgb_l_tensor = reassembly_function(torch.mul(lower_space_tensor, upper_space_tensor))
+
+ elif blend_mode == "Screen":
+ upper_rgb_l_tensor = reassembly_function(
+ torch.add(
+ torch.mul(
+ torch.mul(
+ torch.add(torch.mul(upper_space_tensor, -1.0), 1.0),
+ torch.add(torch.mul(lower_space_tensor, -1.0), 1.0),
+ ),
+ -1.0,
+ ),
+ 1.0,
+ )
+ )
+
+ elif (blend_mode == "Overlay") or (blend_mode == "Hard Light"):
+ subject_of_cond_tensor = lower_space_tensor if (blend_mode == "Overlay") else upper_space_tensor
+ if lightness_index is None:
+ upper_space_tensor = torch.where(
+ torch.lt(subject_of_cond_tensor, 0.5),
+ torch.mul(torch.mul(lower_space_tensor, upper_space_tensor), 2.0),
+ torch.add(
+ torch.mul(
+ torch.mul(
+ torch.mul(
+ torch.add(torch.mul(lower_space_tensor, -1.0), 1.0),
+ torch.add(torch.mul(upper_space_tensor, -1.0), 1.0),
+ ),
+ 2.0,
+ ),
+ -1.0,
+ ),
+ 1.0,
+ ),
+ )
+ else: # TODO: Currently blending only the lightness channel, not really ideal.
+ upper_space_tensor[lightness_index, :, :] = torch.where(
+ torch.lt(subject_of_cond_tensor[lightness_index, :, :], 0.5),
+ torch.mul(
+ torch.mul(lower_space_tensor[lightness_index, :, :], upper_space_tensor[lightness_index, :, :]),
+ 2.0,
+ ),
+ torch.add(
+ torch.mul(
+ torch.mul(
+ torch.mul(
+ torch.add(torch.mul(lower_space_tensor[lightness_index, :, :], -1.0), 1.0),
+ torch.add(torch.mul(upper_space_tensor[lightness_index, :, :], -1.0), 1.0),
+ ),
+ 2.0,
+ ),
+ -1.0,
+ ),
+ 1.0,
+ ),
+ )
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(upper_space_tensor))
+
+ elif blend_mode == "Soft Light":
+ if lightness_index is None:
+ g_tensor = torch.where(
+ torch.le(lower_space_tensor, 0.25),
+ torch.mul(
+ torch.add(
+ torch.mul(torch.sub(torch.mul(lower_space_tensor, 16.0), 12.0), lower_space_tensor), 4.0
+ ),
+ lower_space_tensor,
+ ),
+ torch.sqrt(lower_space_tensor),
+ )
+ lower_space_tensor = torch.where(
+ torch.le(upper_space_tensor, 0.5),
+ torch.sub(
+ lower_space_tensor,
+ torch.mul(
+ torch.mul(torch.add(torch.mul(lower_space_tensor, -1.0), 1.0), lower_space_tensor),
+ torch.add(torch.mul(torch.mul(upper_space_tensor, 2.0), -1.0), 1.0),
+ ),
+ ),
+ torch.add(
+ lower_space_tensor,
+ torch.mul(
+ torch.sub(torch.mul(upper_space_tensor, 2.0), 1.0), torch.sub(g_tensor, lower_space_tensor)
+ ),
+ ),
+ )
+ else:
+ print(
+ "\r\nCOND SHAPE:"
+ + str(torch.le(lower_space_tensor[lightness_index, :, :], 0.25).unsqueeze(0).shape)
+ + "\r\n"
+ )
+ g_tensor = torch.where( # Calculates all 3 channels but only one is currently used
+ torch.le(lower_space_tensor[lightness_index, :, :], 0.25).expand(upper_space_tensor.shape),
+ torch.mul(
+ torch.add(
+ torch.mul(torch.sub(torch.mul(lower_space_tensor, 16.0), 12.0), lower_space_tensor), 4.0
+ ),
+ lower_space_tensor,
+ ),
+ torch.sqrt(lower_space_tensor),
+ )
+ lower_space_tensor[lightness_index, :, :] = torch.where(
+ torch.le(upper_space_tensor[lightness_index, :, :], 0.5),
+ torch.sub(
+ lower_space_tensor[lightness_index, :, :],
+ torch.mul(
+ torch.mul(
+ torch.add(torch.mul(lower_space_tensor[lightness_index, :, :], -1.0), 1.0),
+ lower_space_tensor[lightness_index, :, :],
+ ),
+ torch.add(torch.mul(torch.mul(upper_space_tensor[lightness_index, :, :], 2.0), -1.0), 1.0),
+ ),
+ ),
+ torch.add(
+ lower_space_tensor[lightness_index, :, :],
+ torch.mul(
+ torch.sub(torch.mul(upper_space_tensor[lightness_index, :, :], 2.0), 1.0),
+ torch.sub(g_tensor[lightness_index, :, :], lower_space_tensor[lightness_index, :, :]),
+ ),
+ ),
+ )
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Linear Dodge (Add)":
+ lower_space_tensor = torch.add(lower_space_tensor, upper_space_tensor)
+ if hue_index is not None:
+ lower_space_tensor[hue_index, :, :] = torch.remainder(lower_space_tensor[hue_index, :, :], hue_period)
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Color Dodge":
+ lower_space_tensor = torch.div(lower_space_tensor, torch.add(torch.mul(upper_space_tensor, -1.0), 1.0))
+ if hue_index is not None:
+ lower_space_tensor[hue_index, :, :] = torch.remainder(lower_space_tensor[hue_index, :, :], hue_period)
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Divide":
+ lower_space_tensor = torch.div(lower_space_tensor, upper_space_tensor)
+ if hue_index is not None:
+ lower_space_tensor[hue_index, :, :] = torch.remainder(lower_space_tensor[hue_index, :, :], hue_period)
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Linear Burn":
+ # We compute the result in the lower image's current space tensor and return that:
+ if lightness_index is None: # Elementwise
+ lower_space_tensor = torch.sub(torch.add(lower_space_tensor, upper_space_tensor), 1.0)
+ else: # Operate only on the selected lightness channel
+ lower_space_tensor[lightness_index, :, :] = torch.sub(
+ torch.add(lower_space_tensor[lightness_index, :, :], upper_space_tensor[lightness_index, :, :]), 1.0
+ )
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Color Burn":
+ upper_rgb_l_tensor = adaptive_clipped(
+ reassembly_function(
+ torch.add(
+ torch.mul(
+ torch.min(
+ torch.div(torch.add(torch.mul(lower_space_tensor, -1.0), 1.0), upper_space_tensor),
+ torch.ones(lower_space_tensor.shape),
+ ),
+ -1.0,
+ ),
+ 1.0,
+ )
+ )
+ )
+ elif blend_mode == "Vivid Light":
+ if lightness_index is None:
+ lower_space_tensor = adaptive_clipped(
+ reassembly_function(
+ torch.where(
+ torch.lt(upper_space_tensor, 0.5),
+ torch.div(
+ torch.add(
+ torch.mul(
+ torch.div(
+ torch.add(torch.mul(lower_space_tensor, -1.0), 1.0), upper_space_tensor
+ ),
+ -1.0,
+ ),
+ 1.0,
+ ),
+ 2.0,
+ ),
+ torch.div(
+ torch.div(lower_space_tensor, torch.add(torch.mul(upper_space_tensor, -1.0), 1.0)), 2.0
+ ),
+ )
+ )
+ )
+ else:
+ lower_space_tensor[lightness_index, :, :] = torch.where(
+ torch.lt(upper_space_tensor[lightness_index, :, :], 0.5),
+ torch.div(
+ torch.add(
+ torch.mul(
+ torch.div(
+ torch.add(torch.mul(lower_space_tensor[lightness_index, :, :], -1.0), 1.0),
+ upper_space_tensor[lightness_index, :, :],
+ ),
+ -1.0,
+ ),
+ 1.0,
+ ),
+ 2.0,
+ ),
+ torch.div(
+ torch.div(
+ lower_space_tensor[lightness_index, :, :],
+ torch.add(torch.mul(upper_space_tensor[lightness_index, :, :], -1.0), 1.0),
+ ),
+ 2.0,
+ ),
+ )
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Linear Light":
+ if lightness_index is None:
+ lower_space_tensor = torch.sub(torch.add(lower_space_tensor, torch.mul(upper_space_tensor, 2.0)), 1.0)
+ else:
+ lower_space_tensor[lightness_index, :, :] = torch.sub(
+ torch.add(
+ lower_space_tensor[lightness_index, :, :],
+ torch.mul(upper_space_tensor[lightness_index, :, :], 2.0),
+ ),
+ 1.0,
+ )
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Subtract":
+ lower_space_tensor = torch.sub(lower_space_tensor, upper_space_tensor)
+ if hue_index is not None:
+ lower_space_tensor[hue_index, :, :] = torch.remainder(lower_space_tensor[hue_index, :, :], hue_period)
+ upper_rgb_l_tensor = adaptive_clipped(reassembly_function(lower_space_tensor))
+
+ elif blend_mode == "Difference":
+ upper_rgb_l_tensor = adaptive_clipped(
+ reassembly_function(torch.abs(torch.sub(lower_space_tensor, upper_space_tensor)))
+ )
+
+ elif (blend_mode == "Darken Only") or (blend_mode == "Lighten Only"):
+ extrema_fn = torch.min if (blend_mode == "Darken Only") else torch.max
+ comparator_fn = torch.ge if (blend_mode == "Darken Only") else torch.lt
+ if lightness_index is None:
+ upper_space_tensor = torch.stack(
+ [
+ extrema_fn(upper_space_tensor[0, :, :], lower_space_tensor[0, :, :]),
+ extrema_fn(upper_space_tensor[1, :, :], lower_space_tensor[1, :, :]),
+ extrema_fn(upper_space_tensor[2, :, :], lower_space_tensor[2, :, :]),
+ ]
+ )
+ else:
+ upper_space_tensor = torch.where(
+ comparator_fn(
+ upper_space_tensor[lightness_index, :, :], lower_space_tensor[lightness_index, :, :]
+ ).expand(upper_space_tensor.shape),
+ lower_space_tensor,
+ upper_space_tensor,
+ )
+ upper_rgb_l_tensor = reassembly_function(upper_space_tensor)
+
+ elif blend_mode in [
+ "Hue",
+ "Saturation",
+ "Color",
+ "Luminosity",
+ ]:
+ if blend_mode == "Hue": # l, c: lower / h: upper
+ upper_space_tensor[lightness_index, :, :] = lower_space_tensor[lightness_index, :, :]
+ upper_space_tensor[saturation_index, :, :] = lower_space_tensor[saturation_index, :, :]
+ elif blend_mode == "Saturation": # l, h: lower / c: upper
+ upper_space_tensor[lightness_index, :, :] = lower_space_tensor[lightness_index, :, :]
+ upper_space_tensor[hue_index, :, :] = lower_space_tensor[hue_index, :, :]
+ elif blend_mode == "Color": # l: lower / c, h: upper
+ upper_space_tensor[lightness_index, :, :] = lower_space_tensor[lightness_index, :, :]
+ elif blend_mode == "Luminosity": # h, c: lower / l: upper
+ upper_space_tensor[saturation_index, :, :] = lower_space_tensor[saturation_index, :, :]
+ upper_space_tensor[hue_index, :, :] = lower_space_tensor[hue_index, :, :]
+ upper_rgb_l_tensor = reassembly_function(upper_space_tensor)
+
+ elif blend_mode in ["Lighten Only (EAL)", "Darken Only (EAL)"]:
+ comparator_fn = torch.lt if (blend_mode == "Lighten Only (EAL)") else torch.ge
+ upper_space_tensor = torch.where(
+ comparator_fn(upper_l_eal_tensor, lower_l_eal_tensor).expand(upper_space_tensor.shape),
+ lower_space_tensor,
+ upper_space_tensor,
+ )
+ upper_rgb_l_tensor = reassembly_function(upper_space_tensor)
+
+ return upper_rgb_l_tensor
+
+ def alpha_composite(
+ self,
+ upper_tensor: torch.Tensor,
+ alpha_upper_tensor: torch.Tensor,
+ lower_tensor: torch.Tensor,
+ alpha_lower_tensor: torch.Tensor,
+ mask_tensor: Optional[torch.Tensor] = None,
+ ):
+ """Alpha compositing of upper on lower tensor with alpha channels, mask and scalar"""
+
+ upper_tensor = remove_nans(upper_tensor)
+
+ alpha_upper_tensor = torch.mul(alpha_upper_tensor, self.opacity)
+ if mask_tensor is not None:
+ alpha_upper_tensor = torch.mul(alpha_upper_tensor, torch.add(torch.mul(mask_tensor, -1.0), 1.0))
+
+ alpha_tensor = torch.add(
+ alpha_upper_tensor, torch.mul(alpha_lower_tensor, torch.add(torch.mul(alpha_upper_tensor, -1.0), 1.0))
+ )
+
+ return (
+ torch.div(
+ torch.add(
+ torch.mul(upper_tensor, alpha_upper_tensor),
+ torch.mul(
+ torch.mul(lower_tensor, alpha_lower_tensor), torch.add(torch.mul(alpha_upper_tensor, -1.0), 1.0)
+ ),
+ ),
+ alpha_tensor,
+ ),
+ alpha_tensor,
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ """Main execution of the ImageBlendInvocation node"""
+
+ image_upper = context.images.get_pil(self.layer_upper.image_name)
+ image_base = context.images.get_pil(self.layer_base.image_name)
+
+ # Keep the modes for restoration after processing:
+ image_mode_base = image_base.mode
+
+ # Get rid of ICC profiles by converting to sRGB, but save for restoration:
+ cms_profile_srgb = None
+ if "icc_profile" in image_upper.info:
+ cms_profile_upper = BytesIO(image_upper.info["icc_profile"])
+ cms_profile_srgb = ImageCms.createProfile("sRGB")
+ cms_xform = ImageCms.buildTransformFromOpenProfiles(
+ cms_profile_upper, cms_profile_srgb, image_upper.mode, "RGBA"
+ )
+ image_upper = ImageCms.applyTransform(image_upper, cms_xform)
+
+ cms_profile_base = None
+ icc_profile_bytes = None
+ if "icc_profile" in image_base.info:
+ icc_profile_bytes = image_base.info["icc_profile"]
+ cms_profile_base = BytesIO(icc_profile_bytes)
+ if cms_profile_srgb is None:
+ cms_profile_srgb = ImageCms.createProfile("sRGB")
+ cms_xform = ImageCms.buildTransformFromOpenProfiles(
+ cms_profile_base, cms_profile_srgb, image_base.mode, "RGBA"
+ )
+ image_base = ImageCms.applyTransform(image_base, cms_xform)
+
+ image_mask = None
+ if self.mask is not None:
+ image_mask = context.images.get_pil(self.mask.image_name)
+ color_space = self.color_space.split()[0]
+
+ image_upper = self.scale_and_pad_or_crop_to_base(image_upper, image_base)
+ if image_mask is not None:
+ image_mask = self.scale_and_pad_or_crop_to_base(image_mask, image_base)
+
+ tensor_requirements = []
+
+ # Hue, Saturation, Color, and Luminosity won't work in sRGB, require HSL
+ if self.blend_mode in ["Hue", "Saturation", "Color", "Luminosity"] and self.color_space in [
+ "RGB",
+ "Linear RGB",
+ ]:
+ tensor_requirements = ["hsl"]
+
+ if self.blend_mode in ["Lighten Only (EAL)", "Darken Only (EAL)"]:
+ tensor_requirements = tensor_requirements + ["lch", "l_eal"]
+
+ tensor_requirements += {
+ "Linear": [],
+ "RGB": [],
+ "HSL": ["hsl"],
+ "HSV": ["hsv"],
+ "Okhsl": ["okhsl"],
+ "Okhsv": ["okhsv"],
+ "Oklch": ["oklch"],
+ "LCh": ["lch"],
+ }[color_space]
+
+ image_tensors = (
+ upper_rgb_l_tensor, # linear-light sRGB
+ lower_rgb_l_tensor, # linear-light sRGB
+ upper_rgb_tensor,
+ lower_rgb_tensor,
+ alpha_upper_tensor,
+ alpha_lower_tensor,
+ mask_tensor,
+ upper_hsv_tensor,
+ lower_hsv_tensor,
+ upper_hsl_tensor,
+ lower_hsl_tensor,
+ upper_lab_tensor,
+ lower_lab_tensor,
+ upper_lch_tensor,
+ lower_lch_tensor,
+ upper_l_eal_tensor,
+ lower_l_eal_tensor,
+ upper_oklab_tensor,
+ lower_oklab_tensor,
+ upper_oklch_tensor,
+ lower_oklch_tensor,
+ upper_okhsv_tensor,
+ lower_okhsv_tensor,
+ upper_okhsl_tensor,
+ lower_okhsl_tensor,
+ ) = self.prepare_tensors_from_images(
+ image_upper, image_base, mask_image=image_mask, required=tensor_requirements
+ )
+
+ # if not (self.blend_mode == "Normal"):
+ upper_rgb_l_tensor = self.apply_blend(image_tensors)
+
+ output_tensor, alpha_tensor = self.alpha_composite(
+ srgb_from_linear_srgb(
+ upper_rgb_l_tensor, alpha=self.adaptive_gamut, steps=(3 if self.high_precision else 1)
+ ),
+ alpha_upper_tensor,
+ lower_rgb_tensor,
+ alpha_lower_tensor,
+ mask_tensor=mask_tensor,
+ )
+
+ # Restore alpha channel and base mode:
+ output_tensor = torch.stack(
+ [output_tensor[0, :, :], output_tensor[1, :, :], output_tensor[2, :, :], alpha_tensor]
+ )
+ image_out = pil_image_from_tensor(output_tensor, mode="RGBA")
+
+ # Restore ICC profile if base image had one:
+ if cms_profile_base is not None:
+ cms_xform = ImageCms.buildTransformFromOpenProfiles(
+ cms_profile_srgb, BytesIO(icc_profile_bytes), "RGBA", image_out.mode
+ )
+ image_out = ImageCms.applyTransform(image_out, cms_xform)
+ else:
+ image_out = image_out.convert(image_mode_base)
+
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "invokeai_img_composite",
+ title="Image Compositor",
+ tags=["image", "compose", "chroma", "key"],
+ category="image",
+ version="1.2.0",
+)
+class InvokeImageCompositorInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Removes backdrop from subject image then overlays subject on background image. Originally created by @dwringer"""
+
+ image_subject: ImageField = InputField(description="Image of the subject on a plain monochrome background")
+ image_background: ImageField = InputField(description="Image of a background scene")
+ chroma_key: str = InputField(
+ default="", description="Can be empty for corner flood select, or CSS-3 color or tuple"
+ )
+ threshold: int = InputField(ge=0, default=50, description="Subject isolation flood-fill threshold")
+ fill_x: bool = InputField(default=False, description="Scale base subject image to fit background width")
+ fill_y: bool = InputField(default=True, description="Scale base subject image to fit background height")
+ x_offset: int = InputField(default=0, description="x-offset for the subject")
+ y_offset: int = InputField(default=0, description="y-offset for the subject")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_background = context.images.get_pil(self.image_background.image_name).convert(mode="RGBA")
+ image_subject = context.images.get_pil(self.image_subject.image_name).convert(mode="RGBA")
+
+ if image_subject.height == 0 or image_subject.width == 0:
+ raise ValueError("The subject image has zero height or width")
+ if image_background.height == 0 or image_background.width == 0:
+ raise ValueError("The subject image has zero height or width")
+
+ # Handle backdrop removal:
+ chroma_key = self.chroma_key.strip()
+ if 0 < len(chroma_key):
+ # Remove pixels by chroma key:
+ if chroma_key[0] == "(":
+ chroma_key = tuple_from_string(chroma_key)
+ while len(chroma_key) < 3:
+ chroma_key = tuple(list(chroma_key) + [0])
+ if len(chroma_key) == 3:
+ chroma_key = tuple(list(chroma_key) + [255])
+ else:
+ chroma_key = ImageColor.getcolor(chroma_key, "RGBA")
+ threshold = self.threshold**2.0 # to compare vs squared color distance from key
+ pixels = image_subject.load()
+ if pixels is None:
+ raise ValueError("Unable to load pixels from subject image")
+ for i in range(image_subject.width):
+ for j in range(image_subject.height):
+ if (
+ reduce(
+ lambda a, b: a + b, [(pixels[i, j][k] - chroma_key[k]) ** 2 for k in range(len(chroma_key))]
+ )
+ < threshold
+ ):
+ pixels[i, j] = tuple([0 for k in range(len(chroma_key))])
+ else:
+ # Remove pixels by flood select from corners:
+ ImageDraw.floodfill(image_subject, (0, 0), (0, 0, 0, 0), thresh=self.threshold)
+ ImageDraw.floodfill(image_subject, (0, image_subject.height - 1), (0, 0, 0, 0), thresh=self.threshold)
+ ImageDraw.floodfill(image_subject, (image_subject.width - 1, 0), (0, 0, 0, 0), thresh=self.threshold)
+ ImageDraw.floodfill(
+ image_subject, (image_subject.width - 1, image_subject.height - 1), (0, 0, 0, 0), thresh=self.threshold
+ )
+
+ # Scale and position the subject:
+ aspect_background = image_background.width / image_background.height
+ aspect_subject = image_subject.width / image_subject.height
+ if self.fill_x and self.fill_y:
+ image_subject = image_subject.resize((image_background.width, image_background.height))
+ elif (self.fill_x and (aspect_background < aspect_subject)) or (
+ self.fill_y and (aspect_subject <= aspect_background)
+ ):
+ image_subject = ImageOps.pad(
+ image_subject, (image_background.width, image_background.height), color=(0, 0, 0, 0)
+ )
+ elif (self.fill_x and (aspect_subject <= aspect_background)) or (
+ self.fill_y and (aspect_background < aspect_subject)
+ ):
+ image_subject = ImageOps.fit(image_subject, (image_background.width, image_background.height))
+ if (self.x_offset != 0) or (self.y_offset != 0):
+ image_subject = ImageChops.offset(image_subject, self.x_offset, yoffset=-1 * self.y_offset)
+
+ new_image = Image.alpha_composite(image_background, image_subject)
+ new_image.convert(mode="RGB")
+ image_dto = context.images.save(new_image)
+
+ return ImageOutput.build(image_dto)
+
+
+DILATE_ERODE_MODES = Literal[
+ "Dilate",
+ "Erode",
+]
+
+
+@invocation(
+ "invokeai_img_dilate_erode",
+ title="Image Dilate or Erode",
+ tags=["image", "mask", "dilate", "erode", "expand", "contract", "mask"],
+ category="image",
+ version="1.3.0",
+)
+class InvokeImageDilateOrErodeInvocation(BaseInvocation, WithMetadata):
+ """Dilate (expand) or erode (contract) an image. Originally created by @dwringer"""
+
+ image: ImageField = InputField(description="The image from which to create a mask")
+ lightness_only: bool = InputField(default=False, description="If true, only applies to image lightness (CIELa*b*)")
+ radius_w: int = InputField(
+ ge=0, default=4, description="Width (in pixels) by which to dilate(expand) or erode (contract) the image"
+ )
+ radius_h: int = InputField(
+ ge=0, default=4, description="Height (in pixels) by which to dilate(expand) or erode (contract) the image"
+ )
+ mode: DILATE_ERODE_MODES = InputField(default="Dilate", description="How to operate on the image")
+
+ def expand_or_contract(self, image_in: Image.Image):
+ image_out = numpy.array(image_in)
+ expand_radius_w = self.radius_w
+ expand_radius_h = self.radius_h
+
+ expand_fn = None
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (expand_radius_w * 2 + 1, expand_radius_h * 2 + 1))
+ if self.mode == "Dilate":
+ expand_fn = cv2.dilate
+ elif self.mode == "Erode":
+ expand_fn = cv2.erode
+ else:
+ raise ValueError("Invalid mode selected")
+ image_out = expand_fn(image_out, kernel, iterations=1)
+ return Image.fromarray(image_out, mode=image_in.mode)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_in = context.images.get_pil(self.image.image_name)
+ image_out = image_in
+
+ if self.lightness_only:
+ image_mode = image_in.mode
+ alpha_channel = None
+ if (image_mode == "RGBA") or (image_mode == "LA") or (image_mode == "PA"):
+ alpha_channel = image_in.getchannel("A")
+ elif (image_mode == "RGBa") or (image_mode == "La") or (image_mode == "Pa"):
+ alpha_channel = image_in.getchannel("a")
+ if (image_mode == "RGBA") or (image_mode == "RGBa"):
+ image_mode = "RGB"
+ elif (image_mode == "LA") or (image_mode == "La"):
+ image_mode = "L"
+ elif image_mode == "PA":
+ image_mode = "P"
+ image_out = image_out.convert("RGB")
+ image_out = image_out.convert("LAB")
+ l_channel = self.expand_or_contract(image_out.getchannel("L"))
+ image_out = Image.merge("LAB", (l_channel, image_out.getchannel("A"), image_out.getchannel("B")))
+ if (image_mode == "L") or (image_mode == "P"):
+ image_out = image_out.convert("RGB")
+ image_out = image_out.convert(image_mode)
+ if "a" in image_in.mode.lower():
+ image_out = Image.merge(
+ image_in.mode, tuple([image_out.getchannel(c) for c in image_mode] + [alpha_channel])
+ )
+ else:
+ image_out = self.expand_or_contract(image_out)
+
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "invokeai_img_val_thresholds",
+ title="Image Value Thresholds",
+ tags=["image", "mask", "value", "threshold"],
+ category="image",
+ version="1.2.0",
+)
+class InvokeImageValueThresholdsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Clip image to pure black/white past specified thresholds. Originally created by @dwringer"""
+
+ image: ImageField = InputField(description="The image from which to create a mask")
+ invert_output: bool = InputField(default=False, description="Make light areas dark and vice versa")
+ renormalize_values: bool = InputField(default=False, description="Rescale remaining values from minimum to maximum")
+ lightness_only: bool = InputField(default=False, description="If true, only applies to image lightness (CIELa*b*)")
+ threshold_upper: float = InputField(default=0.5, description="Threshold above which will be set to full value")
+ threshold_lower: float = InputField(default=0.5, description="Threshold below which will be set to minimum value")
+
+ def get_threshold_mask(self, image_tensor: torch.Tensor):
+ img_tensor = image_tensor.clone()
+ threshold_h, threshold_s = self.threshold_upper, self.threshold_lower
+ ones_tensor = torch.ones(img_tensor.shape)
+ zeros_tensor = torch.zeros(img_tensor.shape)
+
+ zeros_mask, ones_mask = None, None
+ if self.invert_output:
+ zeros_mask, ones_mask = torch.ge(img_tensor, threshold_h), torch.lt(img_tensor, threshold_s)
+ else:
+ ones_mask, zeros_mask = torch.ge(img_tensor, threshold_h), torch.lt(img_tensor, threshold_s)
+
+ if not (threshold_h == threshold_s):
+ mask_hi = torch.ge(img_tensor, threshold_s)
+ mask_lo = torch.lt(img_tensor, threshold_h)
+ mask = torch.logical_and(mask_hi, mask_lo)
+ masked = img_tensor[mask]
+ if 0 < masked.numel():
+ if self.renormalize_values:
+ vmax, vmin = max(threshold_h, threshold_s), min(threshold_h, threshold_s)
+ if vmax == vmin:
+ img_tensor[mask] = vmin * ones_tensor[mask]
+ elif self.invert_output:
+ img_tensor[mask] = torch.sub(1.0, (img_tensor[mask] - vmin) / (vmax - vmin))
+ else:
+ img_tensor[mask] = (img_tensor[mask] - vmin) / (vmax - vmin)
+
+ img_tensor[ones_mask] = ones_tensor[ones_mask]
+ img_tensor[zeros_mask] = zeros_tensor[zeros_mask]
+
+ return img_tensor
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_in = context.images.get_pil(self.image.image_name)
+
+ if self.lightness_only:
+ image_mode = image_in.mode
+ alpha_channel = None
+ if (image_mode == "RGBA") or (image_mode == "LA") or (image_mode == "PA"):
+ alpha_channel = image_in.getchannel("A")
+ elif (image_mode == "RGBa") or (image_mode == "La") or (image_mode == "Pa"):
+ alpha_channel = image_in.getchannel("a")
+ if (image_mode == "RGBA") or (image_mode == "RGBa"):
+ image_mode = "RGB"
+ elif (image_mode == "LA") or (image_mode == "La"):
+ image_mode = "L"
+ elif image_mode == "PA":
+ image_mode = "P"
+ image_out = image_in.convert("RGB")
+ image_out = image_out.convert("LAB")
+
+ l_channel = image_resized_to_grid_as_tensor(image_out.getchannel("L"), normalize=False)
+ l_channel = self.get_threshold_mask(l_channel)
+ l_channel = pil_image_from_tensor(l_channel)
+
+ image_out = Image.merge("LAB", (l_channel, image_out.getchannel("A"), image_out.getchannel("B")))
+ if (image_mode == "L") or (image_mode == "P"):
+ image_out = image_out.convert("RGB")
+ image_out = image_out.convert(image_mode)
+ if "a" in image_in.mode.lower():
+ image_out = Image.merge(
+ image_in.mode, tuple([image_out.getchannel(c) for c in image_mode] + [alpha_channel])
+ )
+ else:
+ image_out = image_resized_to_grid_as_tensor(image_in, normalize=False)
+ image_out = self.get_threshold_mask(image_out)
+ image_out = pil_image_from_tensor(image_out)
+
+ image_dto = context.images.save(image_out)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/constants.py b/invokeai/app/invocations/constants.py
new file mode 100644
index 00000000000..314890a0f8f
--- /dev/null
+++ b/invokeai/app/invocations/constants.py
@@ -0,0 +1,12 @@
+from typing import Literal
+
+LATENT_SCALE_FACTOR = 8
+"""
+HACK: Many nodes are currently hard-coded to use a fixed latent scale factor of 8. This is fragile, and will need to
+be addressed if future models use a different latent scale factor. Also, note that there may be places where the scale
+factor is hard-coded to a literal '8' rather than using this constant.
+The ratio of image:latent dimensions is LATENT_SCALE_FACTOR:1, or 8:1.
+"""
+
+IMAGE_MODES = Literal["L", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"]
+"""A literal type for PIL image modes supported by Invoke"""
diff --git a/invokeai/app/invocations/content_shuffle.py b/invokeai/app/invocations/content_shuffle.py
new file mode 100644
index 00000000000..6fd35b53eb2
--- /dev/null
+++ b/invokeai/app/invocations/content_shuffle.py
@@ -0,0 +1,25 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.content_shuffle import content_shuffle
+
+
+@invocation(
+ "content_shuffle",
+ title="Content Shuffle",
+ tags=["controlnet", "normal"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class ContentShuffleInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Shuffles the image, similar to a 'liquify' filter."""
+
+ image: ImageField = InputField(description="The image to process")
+ scale_factor: int = InputField(default=256, ge=0, description="The scale factor used for the shuffle")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ output_image = content_shuffle(input_image=image, scale_factor=self.scale_factor)
+ image_dto = context.images.save(image=output_image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/controlnet.py b/invokeai/app/invocations/controlnet.py
new file mode 100644
index 00000000000..9b0fc8219b2
--- /dev/null
+++ b/invokeai/app/invocations/controlnet.py
@@ -0,0 +1,136 @@
+# Invocations for ControlNet image preprocessors
+# initial implementation by Gregg Helt, 2023
+from typing import List, Union
+
+from pydantic import BaseModel, Field, field_validator, model_validator
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ OutputField,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.controlnet_utils import (
+ CONTROLNET_MODE_VALUES,
+ CONTROLNET_RESIZE_VALUES,
+ heuristic_resize_fast,
+)
+from invokeai.backend.image_util.util import np_to_pil, pil_to_np
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+class ControlField(BaseModel):
+ image: ImageField = Field(description="The control image")
+ control_model: ModelIdentifierField = Field(description="The ControlNet model to use")
+ control_weight: Union[float, List[float]] = Field(default=1, description="The weight given to the ControlNet")
+ begin_step_percent: float = Field(
+ default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)"
+ )
+ end_step_percent: float = Field(
+ default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
+ )
+ control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode to use")
+ resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
+
+ @field_validator("control_weight")
+ @classmethod
+ def validate_control_weight(cls, v):
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self):
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+
+@invocation_output("control_output")
+class ControlOutput(BaseInvocationOutput):
+ """node output for ControlNet info"""
+
+ # Outputs
+ control: ControlField = OutputField(description=FieldDescriptions.control)
+
+
+@invocation(
+ "controlnet", title="ControlNet - SD1.5, SD2, SDXL", tags=["controlnet"], category="conditioning", version="1.1.3"
+)
+class ControlNetInvocation(BaseInvocation):
+ """Collects ControlNet info to pass to other nodes"""
+
+ image: ImageField = InputField(description="The control image")
+ control_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.controlnet_model,
+ ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2, BaseModelType.StableDiffusionXL],
+ ui_model_type=ModelType.ControlNet,
+ )
+ control_weight: Union[float, List[float]] = InputField(
+ default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"
+ )
+ begin_step_percent: float = InputField(
+ default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)"
+ )
+ end_step_percent: float = InputField(
+ default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
+ )
+ control_mode: CONTROLNET_MODE_VALUES = InputField(default="balanced", description="The control mode used")
+ resize_mode: CONTROLNET_RESIZE_VALUES = InputField(default="just_resize", description="The resize mode used")
+
+ @field_validator("control_weight")
+ @classmethod
+ def validate_control_weight(cls, v):
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self) -> "ControlNetInvocation":
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+ def invoke(self, context: InvocationContext) -> ControlOutput:
+ return ControlOutput(
+ control=ControlField(
+ image=self.image,
+ control_model=self.control_model,
+ control_weight=self.control_weight,
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ control_mode=self.control_mode,
+ resize_mode=self.resize_mode,
+ ),
+ )
+
+
+@invocation(
+ "heuristic_resize",
+ title="Heuristic Resize",
+ tags=["image, controlnet"],
+ category="controlnet_preprocessors",
+ version="1.1.1",
+ classification=Classification.Prototype,
+)
+class HeuristicResizeInvocation(BaseInvocation):
+ """Resize an image using a heuristic method. Preserves edge maps."""
+
+ image: ImageField = InputField(description="The image to resize")
+ width: int = InputField(default=512, ge=1, description="The width to resize to (px)")
+ height: int = InputField(default=512, ge=1, description="The height to resize to (px)")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ np_img = pil_to_np(image)
+ np_resized = heuristic_resize_fast(np_img, (self.width, self.height))
+ resized = np_to_pil(np_resized)
+ image_dto = context.images.save(image=resized)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/create_denoise_mask.py b/invokeai/app/invocations/create_denoise_mask.py
new file mode 100644
index 00000000000..419a516bcdc
--- /dev/null
+++ b/invokeai/app/invocations/create_denoise_mask.py
@@ -0,0 +1,76 @@
+from typing import Optional
+
+import torch
+import torchvision.transforms as T
+from PIL import Image
+from torchvision.transforms.functional import resize as tv_resize
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField
+from invokeai.app.invocations.image_to_latents import ImageToLatentsInvocation
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import DenoiseMaskOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+
+
+@invocation(
+ "create_denoise_mask",
+ title="Create Denoise Mask",
+ tags=["mask", "denoise"],
+ category="mask",
+ version="1.0.2",
+)
+class CreateDenoiseMaskInvocation(BaseInvocation):
+ """Creates mask for denoising model run."""
+
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection, ui_order=0)
+ 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)
+ tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=3)
+ fp32: bool = InputField(default=False, description=FieldDescriptions.fp32, ui_order=4)
+
+ def prep_mask_tensor(self, mask_image: Image.Image) -> torch.Tensor:
+ if mask_image.mode != "L":
+ mask_image = mask_image.convert("L")
+ mask_tensor: torch.Tensor = image_resized_to_grid_as_tensor(mask_image, normalize=False)
+ if mask_tensor.dim() == 3:
+ mask_tensor = mask_tensor.unsqueeze(0)
+ # if shape is not None:
+ # mask_tensor = tv_resize(mask_tensor, shape, T.InterpolationMode.BILINEAR)
+ return mask_tensor
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> DenoiseMaskOutput:
+ if self.image is not None:
+ image = context.images.get_pil(self.image.image_name)
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = image_tensor.unsqueeze(0)
+ else:
+ image_tensor = None
+
+ mask = self.prep_mask_tensor(
+ context.images.get_pil(self.mask.image_name),
+ )
+
+ if image_tensor is not None:
+ vae_info = context.models.load(self.vae.vae)
+
+ img_mask = tv_resize(mask, image_tensor.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
+ masked_image = image_tensor * torch.where(img_mask < 0.5, 0.0, 1.0)
+ # TODO:
+ context.util.signal_progress("Running VAE encoder")
+ masked_latents = ImageToLatentsInvocation.vae_encode(vae_info, self.fp32, self.tiled, masked_image.clone())
+
+ masked_latents_name = context.tensors.save(tensor=masked_latents)
+ else:
+ masked_latents_name = None
+
+ mask_name = context.tensors.save(tensor=mask)
+
+ return DenoiseMaskOutput.build(
+ mask_name=mask_name,
+ masked_latents_name=masked_latents_name,
+ gradient=False,
+ )
diff --git a/invokeai/app/invocations/create_gradient_mask.py b/invokeai/app/invocations/create_gradient_mask.py
new file mode 100644
index 00000000000..08826cc5efc
--- /dev/null
+++ b/invokeai/app/invocations/create_gradient_mask.py
@@ -0,0 +1,229 @@
+from typing import Literal, Optional
+
+import cv2
+import numpy as np
+import torch
+import torchvision.transforms as T
+from PIL import Image
+from torchvision.transforms.functional import resize as tv_resize
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ OutputField,
+)
+from invokeai.app.invocations.image_to_latents import ImageToLatentsInvocation
+from invokeai.app.invocations.model import UNetField, VAEField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import FluxVariantType, ModelType, ModelVariantType
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+
+
+@invocation_output("gradient_mask_output")
+class GradientMaskOutput(BaseInvocationOutput):
+ """Outputs a denoise mask and an image representing the total gradient of the mask."""
+
+ denoise_mask: DenoiseMaskField = OutputField(
+ description="Mask for denoise model run. Values of 0.0 represent the regions to be fully denoised, and 1.0 "
+ + "represent the regions to be preserved."
+ )
+ expanded_mask_area: ImageField = OutputField(
+ description="Image representing the total gradient area of the mask. For paste-back purposes."
+ )
+
+
+@invocation(
+ "create_gradient_mask",
+ title="Create Gradient Mask",
+ tags=["mask", "denoise"],
+ category="mask",
+ version="1.3.0",
+)
+class CreateGradientMaskInvocation(BaseInvocation):
+ """Creates mask for denoising."""
+
+ mask: ImageField = InputField(description="Image which will be masked", ui_order=1)
+ edge_radius: int = InputField(default=16, ge=0, description="How far to expand the edges of the mask", ui_order=2)
+ coherence_mode: Literal["Gaussian Blur", "Box Blur", "Staged"] = InputField(default="Gaussian Blur", ui_order=3)
+ minimum_denoise: float = InputField(
+ default=0.0, ge=0, le=1, description="Minimum denoise level for the coherence region", ui_order=4
+ )
+ image: Optional[ImageField] = InputField(
+ default=None,
+ description="OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE",
+ title="[OPTIONAL] Image",
+ ui_order=6,
+ )
+ unet: Optional[UNetField] = InputField(
+ description="OPTIONAL: If the Unet is a specialized Inpainting model, masked_latents will be generated from the image with the VAE",
+ default=None,
+ input=Input.Connection,
+ title="[OPTIONAL] UNet",
+ ui_order=5,
+ )
+ vae: Optional[VAEField] = InputField(
+ default=None,
+ description="OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE",
+ title="[OPTIONAL] VAE",
+ input=Input.Connection,
+ ui_order=7,
+ )
+ tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=8)
+ fp32: bool = InputField(default=False, description=FieldDescriptions.fp32, ui_order=9)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> GradientMaskOutput:
+ mask_image = context.images.get_pil(self.mask.image_name, mode="L")
+
+ # Resize the mask_image. Makes the filter 64x faster and doesn't hurt quality in latent scale anyway
+ mask_image = mask_image.resize(
+ (
+ mask_image.width // LATENT_SCALE_FACTOR,
+ mask_image.height // LATENT_SCALE_FACTOR,
+ ),
+ resample=Image.Resampling.BILINEAR,
+ )
+
+ mask_np_orig = np.array(mask_image, dtype=np.float32)
+
+ self.edge_radius = self.edge_radius // LATENT_SCALE_FACTOR # scale the edge radius to match the mask size
+
+ if self.edge_radius > 0:
+ mask_np = 255 - mask_np_orig # invert so 0 is unmasked (higher values = higher denoise strength)
+ dilated_mask = mask_np.copy()
+
+ # Create kernel based on coherence mode
+ if self.coherence_mode == "Box Blur":
+ # Create a circular distance kernel that fades from center outward
+ kernel_size = self.edge_radius * 2 + 1
+ center = self.edge_radius
+ kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32)
+ for i in range(kernel_size):
+ for j in range(kernel_size):
+ dist = np.sqrt((i - center) ** 2 + (j - center) ** 2)
+ if dist <= self.edge_radius:
+ kernel[i, j] = 1.0 - (dist / self.edge_radius)
+ else: # Gaussian Blur or Staged
+ # Create a Gaussian kernel
+ kernel_size = self.edge_radius * 2 + 1
+ kernel = cv2.getGaussianKernel(
+ kernel_size, self.edge_radius / 2.5
+ ) # 2.5 is a magic number (standard deviation capturing)
+ kernel = kernel * kernel.T # Make 2D gaussian kernel
+ kernel = kernel / np.max(kernel) # Normalize center to 1.0
+
+ # Ensure values outside radius are 0
+ center = self.edge_radius
+ for i in range(kernel_size):
+ for j in range(kernel_size):
+ dist = np.sqrt((i - center) ** 2 + (j - center) ** 2)
+ if dist > self.edge_radius:
+ kernel[i, j] = 0
+
+ # 2D max filter
+ mask_tensor = torch.tensor(mask_np)
+ kernel_tensor = torch.tensor(kernel)
+ dilated_mask = 255 - self.max_filter2D_torch(mask_tensor, kernel_tensor).cpu()
+ dilated_mask = dilated_mask.numpy()
+
+ threshold = (1 - self.minimum_denoise) * 255
+
+ if self.coherence_mode == "Staged":
+ # wherever expanded mask is darker than the original mask but original was above threshhold, set it to the threshold
+ # makes any expansion areas drop to threshhold. Raising minimum across the image happen outside of this if
+ threshold_mask = (dilated_mask < mask_np_orig) & (mask_np_orig > threshold)
+ dilated_mask = np.where(threshold_mask, threshold, mask_np_orig)
+
+ # wherever expanded mask is less than 255 but greater than threshold, drop it to threshold (minimum denoise)
+ threshold_mask = (dilated_mask > threshold) & (dilated_mask < 255)
+ dilated_mask = np.where(threshold_mask, threshold, dilated_mask)
+
+ else:
+ dilated_mask = mask_np_orig.copy()
+
+ # convert to tensor
+ dilated_mask = np.clip(dilated_mask, 0, 255).astype(np.uint8)
+ mask_tensor = torch.tensor(dilated_mask, device=torch.device("cpu"))
+
+ # binary mask for compositing
+ expanded_mask = np.where((dilated_mask < 255), 0, 255)
+ expanded_mask_image = Image.fromarray(expanded_mask.astype(np.uint8), mode="L")
+ expanded_mask_image = expanded_mask_image.resize(
+ (
+ mask_image.width * LATENT_SCALE_FACTOR,
+ mask_image.height * LATENT_SCALE_FACTOR,
+ ),
+ resample=Image.Resampling.NEAREST,
+ )
+ expanded_image_dto = context.images.save(expanded_mask_image)
+
+ # restore the original mask size
+ dilated_mask = Image.fromarray(dilated_mask.astype(np.uint8))
+ dilated_mask = dilated_mask.resize(
+ (
+ mask_image.width * LATENT_SCALE_FACTOR,
+ mask_image.height * LATENT_SCALE_FACTOR,
+ ),
+ resample=Image.Resampling.NEAREST,
+ )
+
+ # stack the mask as a tensor, repeating 4 times on dimmension 1
+ dilated_mask_tensor = image_resized_to_grid_as_tensor(dilated_mask, normalize=False)
+ mask_name = context.tensors.save(tensor=dilated_mask_tensor.unsqueeze(0))
+
+ masked_latents_name = None
+ if self.unet is not None and self.vae is not None and self.image is not None:
+ # all three fields must be present at the same time
+ main_model_config = context.models.get_config(self.unet.unet.key)
+ assert main_model_config.type is ModelType.Main
+ variant = getattr(main_model_config, "variant", None)
+ if variant is ModelVariantType.Inpaint or variant is FluxVariantType.DevFill:
+ mask = dilated_mask_tensor
+ vae_info = context.models.load(self.vae.vae)
+ image = context.images.get_pil(self.image.image_name)
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = image_tensor.unsqueeze(0)
+ img_mask = tv_resize(mask, image_tensor.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
+ masked_image = image_tensor * torch.where(img_mask < 0.5, 0.0, 1.0)
+ context.util.signal_progress("Running VAE encoder")
+ masked_latents = ImageToLatentsInvocation.vae_encode(
+ vae_info, self.fp32, self.tiled, masked_image.clone()
+ )
+ masked_latents_name = context.tensors.save(tensor=masked_latents)
+
+ return GradientMaskOutput(
+ denoise_mask=DenoiseMaskField(mask_name=mask_name, masked_latents_name=masked_latents_name, gradient=True),
+ expanded_mask_area=ImageField(image_name=expanded_image_dto.image_name),
+ )
+
+ def max_filter2D_torch(self, image: torch.Tensor, kernel: torch.Tensor) -> torch.Tensor:
+ """
+ This morphological operation is much faster in torch than numpy or opencv
+ For reasonable kernel sizes, the overhead of copying the data to the GPU is not worth it.
+ """
+ h, w = kernel.shape
+ pad_h, pad_w = h // 2, w // 2
+
+ padded = torch.nn.functional.pad(image, (pad_w, pad_w, pad_h, pad_h), mode="constant", value=0)
+ result = torch.zeros_like(image)
+
+ # This looks like it's inside out, but it does the same thing and is more efficient
+ for i in range(h):
+ for j in range(w):
+ weight = kernel[i, j]
+ if weight <= 0:
+ continue
+
+ # Extract the region from padded tensor
+ region = padded[i : i + image.shape[0], j : j + image.shape[1]]
+
+ # Apply weight and update max
+ result = torch.maximum(result, region * weight)
+
+ return result
diff --git a/invokeai/app/invocations/crop_latents.py b/invokeai/app/invocations/crop_latents.py
new file mode 100644
index 00000000000..258049fd2c1
--- /dev/null
+++ b/invokeai/app/invocations/crop_latents.py
@@ -0,0 +1,61 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, LatentsField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+# The Crop Latents node was copied from @skunkworxdark's implementation here:
+# https://github.com/skunkworxdark/XYGrid_nodes/blob/74647fa9c1fa57d317a94bd43ca689af7f0aae5e/images_to_grids.py#L1117C1-L1167C80
+@invocation(
+ "crop_latents",
+ title="Crop Latents",
+ tags=["latents", "crop"],
+ category="latents",
+ version="1.0.2",
+)
+# TODO(ryand): Named `CropLatentsCoreInvocation` to prevent a conflict with custom node `CropLatentsInvocation`.
+# Currently, if the class names conflict then 'GET /openapi.json' fails.
+class CropLatentsCoreInvocation(BaseInvocation):
+ """Crops a latent-space tensor to a box specified in image-space. The box dimensions and coordinates must be
+ divisible by the latent scale factor of 8.
+ """
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ x: int = InputField(
+ ge=0,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description="The left x coordinate (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.",
+ )
+ y: int = InputField(
+ ge=0,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description="The top y coordinate (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.",
+ )
+ width: int = InputField(
+ ge=1,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description="The width (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.",
+ )
+ height: int = InputField(
+ ge=1,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description="The height (in px) of the crop rectangle in image space. This value will be converted to a dimension in latent space.",
+ )
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ x1 = self.x // LATENT_SCALE_FACTOR
+ y1 = self.y // LATENT_SCALE_FACTOR
+ x2 = x1 + (self.width // LATENT_SCALE_FACTOR)
+ y2 = y1 + (self.height // LATENT_SCALE_FACTOR)
+
+ cropped_latents = latents[..., y1:y2, x1:x2]
+
+ name = context.tensors.save(tensor=cropped_latents)
+
+ return LatentsOutput.build(latents_name=name, latents=cropped_latents)
diff --git a/invokeai/app/invocations/custom_nodes/README.md b/invokeai/app/invocations/custom_nodes/README.md
new file mode 100644
index 00000000000..d93bb65539c
--- /dev/null
+++ b/invokeai/app/invocations/custom_nodes/README.md
@@ -0,0 +1,51 @@
+# 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.
diff --git a/invokeai/app/invocations/cv.py b/invokeai/app/invocations/cv.py
new file mode 100644
index 00000000000..f7951ccfeb2
--- /dev/null
+++ b/invokeai/app/invocations/cv.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+
+import cv2 as cv
+import numpy
+from PIL import Image, ImageOps
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation("cv_inpaint", title="OpenCV Inpaint", tags=["opencv", "inpaint"], category="inpaint", version="1.3.1")
+class CvInpaintInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Simple inpaint using opencv."""
+
+ image: ImageField = InputField(description="The image to inpaint")
+ mask: ImageField = InputField(description="The mask to use when inpainting")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ mask = context.images.get_pil(self.mask.image_name)
+
+ # Convert to cv image/mask
+ # TODO: consider making these utility functions
+ cv_image = cv.cvtColor(numpy.array(image.convert("RGB")), cv.COLOR_RGB2BGR)
+ cv_mask = numpy.array(ImageOps.invert(mask.convert("L")))
+
+ # Inpaint
+ cv_inpainted = cv.inpaint(cv_image, cv_mask, 3, cv.INPAINT_TELEA)
+
+ # Convert back to Pillow
+ # TODO: consider making a utility function
+ image_inpainted = Image.fromarray(cv.cvtColor(cv_inpainted, cv.COLOR_BGR2RGB))
+
+ image_dto = context.images.save(image=image_inpainted)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/denoise_latents.py b/invokeai/app/invocations/denoise_latents.py
new file mode 100644
index 00000000000..bb114263e23
--- /dev/null
+++ b/invokeai/app/invocations/denoise_latents.py
@@ -0,0 +1,1110 @@
+# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
+import inspect
+import os
+from contextlib import ExitStack
+from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
+
+import torch
+import torchvision
+import torchvision.transforms as T
+from diffusers.configuration_utils import ConfigMixin
+from diffusers.models.adapter import T2IAdapter
+from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
+from diffusers.schedulers.scheduling_dpmsolver_multistep import DPMSolverMultistepScheduler
+from diffusers.schedulers.scheduling_dpmsolver_sde import DPMSolverSDEScheduler
+from diffusers.schedulers.scheduling_dpmsolver_singlestep import DPMSolverSinglestepScheduler
+from diffusers.schedulers.scheduling_tcd import TCDScheduler
+from diffusers.schedulers.scheduling_utils import SchedulerMixin as Scheduler
+from PIL import Image
+from pydantic import field_validator
+from torchvision.transforms.functional import resize as tv_resize
+from transformers import CLIPVisionModelWithProjection
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.controlnet import ControlField
+from invokeai.app.invocations.fields import (
+ ConditioningField,
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ UIType,
+)
+from invokeai.app.invocations.ip_adapter import IPAdapterField
+from invokeai.app.invocations.model import ModelIdentifierField, UNetField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.invocations.t2i_adapter import T2IAdapterField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.controlnet_utils import prepare_control_image
+from invokeai.backend.ip_adapter.ip_adapter import IPAdapter
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelVariantType
+from invokeai.backend.model_patcher import ModelPatcher
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.denoise_context import DenoiseContext, DenoiseInputs
+from invokeai.backend.stable_diffusion.diffusers_pipeline import (
+ ControlNetData,
+ StableDiffusionGeneratorPipeline,
+ T2IAdapterData,
+)
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ BasicConditioningInfo,
+ IPAdapterConditioningInfo,
+ IPAdapterData,
+ Range,
+ SDXLConditioningInfo,
+ TextConditioningData,
+ TextConditioningRegions,
+)
+from invokeai.backend.stable_diffusion.diffusion.custom_atttention import CustomAttnProcessor2_0
+from invokeai.backend.stable_diffusion.diffusion_backend import StableDiffusionBackend
+from invokeai.backend.stable_diffusion.extension_callback_type import ExtensionCallbackType
+from invokeai.backend.stable_diffusion.extensions.controlnet import ControlNetExt
+from invokeai.backend.stable_diffusion.extensions.freeu import FreeUExt
+from invokeai.backend.stable_diffusion.extensions.inpaint import InpaintExt
+from invokeai.backend.stable_diffusion.extensions.inpaint_model import InpaintModelExt
+from invokeai.backend.stable_diffusion.extensions.lora import LoRAExt
+from invokeai.backend.stable_diffusion.extensions.preview import PreviewExt
+from invokeai.backend.stable_diffusion.extensions.rescale_cfg import RescaleCFGExt
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.stable_diffusion.extensions.t2i_adapter import T2IAdapterExt
+from invokeai.backend.stable_diffusion.extensions_manager import ExtensionsManager
+from invokeai.backend.stable_diffusion.schedulers import SCHEDULER_MAP
+from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.hotfixes import ControlNetModel
+from invokeai.backend.util.mask import to_standard_float_mask
+from invokeai.backend.util.silence_warnings import SilenceWarnings
+
+
+def get_scheduler(
+ context: InvocationContext,
+ scheduler_info: ModelIdentifierField,
+ scheduler_name: str,
+ seed: int,
+ unet_config: AnyModelConfig,
+) -> Scheduler:
+ """Load a scheduler and apply some scheduler-specific overrides."""
+ # TODO(ryand): Silently falling back to ddim seems like a bad idea. Look into why this was added and remove if
+ # possible.
+ scheduler_class, scheduler_extra_config = SCHEDULER_MAP.get(scheduler_name, SCHEDULER_MAP["ddim"])
+ orig_scheduler_info = context.models.load(scheduler_info)
+
+ with orig_scheduler_info as orig_scheduler:
+ scheduler_config = orig_scheduler.config
+
+ if "_backup" in scheduler_config:
+ scheduler_config = scheduler_config["_backup"]
+ scheduler_config = {
+ **scheduler_config,
+ **scheduler_extra_config, # FIXME
+ "_backup": scheduler_config,
+ }
+
+ if hasattr(unet_config, "prediction_type"):
+ scheduler_config["prediction_type"] = unet_config.prediction_type
+
+ # make dpmpp_sde reproducable(seed can be passed only in initializer)
+ if scheduler_class is DPMSolverSDEScheduler:
+ scheduler_config["noise_sampler_seed"] = seed
+
+ if scheduler_class is DPMSolverMultistepScheduler or scheduler_class is DPMSolverSinglestepScheduler:
+ if scheduler_config["_class_name"] == "DEISMultistepScheduler" and scheduler_config["algorithm_type"] == "deis":
+ scheduler_config["algorithm_type"] = "dpmsolver++"
+
+ scheduler = scheduler_class.from_config(scheduler_config)
+
+ # hack copied over from generate.py
+ if not hasattr(scheduler, "uses_inpainting_model"):
+ scheduler.uses_inpainting_model = lambda: False
+ assert isinstance(scheduler, Scheduler)
+ return scheduler
+
+
+@invocation(
+ "denoise_latents",
+ title="Denoise - SD1.5, SDXL",
+ tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
+ category="latents",
+ version="1.5.4",
+)
+class DenoiseLatentsInvocation(BaseInvocation):
+ """Denoises noisy latents to decodable images"""
+
+ positive_conditioning: Union[ConditioningField, list[ConditioningField]] = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection, ui_order=0
+ )
+ negative_conditioning: Union[ConditioningField, list[ConditioningField]] = InputField(
+ description=FieldDescriptions.negative_cond, input=Input.Connection, ui_order=1
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.noise,
+ input=Input.Connection,
+ ui_order=3,
+ )
+ steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
+ cfg_scale: Union[float, List[float]] = InputField(
+ default=7.5, description=FieldDescriptions.cfg_scale, title="CFG Scale"
+ )
+ 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)
+ scheduler: SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description=FieldDescriptions.scheduler,
+ ui_type=UIType.Scheduler,
+ )
+ unet: UNetField = InputField(
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ ui_order=2,
+ )
+ control: Optional[Union[ControlField, list[ControlField]]] = InputField(
+ default=None,
+ input=Input.Connection,
+ ui_order=5,
+ )
+ ip_adapter: Optional[Union[IPAdapterField, list[IPAdapterField]]] = InputField(
+ 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,
+ )
+ cfg_rescale_multiplier: float = InputField(
+ title="CFG Rescale Multiplier", default=0, ge=0, lt=1, description=FieldDescriptions.cfg_rescale_multiplier
+ )
+ latents: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ ui_order=4,
+ )
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None,
+ description=FieldDescriptions.denoise_mask,
+ input=Input.Connection,
+ ui_order=8,
+ )
+
+ @field_validator("cfg_scale")
+ def ge_one(cls, v: Union[List[float], float]) -> Union[List[float], float]:
+ """validate that all cfg_scale values are >= 1"""
+ if isinstance(v, list):
+ for i in v:
+ if i < 1:
+ raise ValueError("cfg_scale must be greater than 1")
+ else:
+ if v < 1:
+ raise ValueError("cfg_scale must be greater than 1")
+ return v
+
+ @staticmethod
+ def _get_text_embeddings_and_masks(
+ cond_list: list[ConditioningField],
+ context: InvocationContext,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> tuple[Union[list[BasicConditioningInfo], list[SDXLConditioningInfo]], list[Optional[torch.Tensor]]]:
+ """Get the text embeddings and masks from the input conditioning fields."""
+ text_embeddings: Union[list[BasicConditioningInfo], list[SDXLConditioningInfo]] = []
+ text_embeddings_masks: list[Optional[torch.Tensor]] = []
+ for cond in cond_list:
+ cond_data = context.conditioning.load(cond.conditioning_name)
+ text_embeddings.append(cond_data.conditionings[0].to(device=device, dtype=dtype))
+
+ mask = cond.mask
+ if mask is not None:
+ mask = context.tensors.load(mask.tensor_name)
+ text_embeddings_masks.append(mask)
+
+ return text_embeddings, text_embeddings_masks
+
+ @staticmethod
+ def _preprocess_regional_prompt_mask(
+ mask: Optional[torch.Tensor], target_height: int, target_width: int, dtype: torch.dtype
+ ) -> torch.Tensor:
+ """Preprocess a regional prompt mask to match the target height and width.
+ If mask is None, returns a mask of all ones with the target height and width.
+ If mask is not None, resizes the mask to the target height and width using 'nearest' interpolation.
+
+ Returns:
+ torch.Tensor: The processed mask. shape: (1, 1, target_height, target_width).
+ """
+
+ if mask is None:
+ return torch.ones((1, 1, target_height, target_width), dtype=dtype)
+
+ mask = to_standard_float_mask(mask, out_dtype=dtype)
+
+ tf = torchvision.transforms.Resize(
+ (target_height, target_width), interpolation=torchvision.transforms.InterpolationMode.NEAREST
+ )
+
+ # Add a batch dimension to the mask, because torchvision expects shape (batch, channels, h, w).
+ mask = mask.unsqueeze(0) # Shape: (1, h, w) -> (1, 1, h, w)
+ resized_mask = tf(mask)
+ return resized_mask
+
+ @staticmethod
+ def _concat_regional_text_embeddings(
+ text_conditionings: Union[list[BasicConditioningInfo], list[SDXLConditioningInfo]],
+ masks: Optional[list[Optional[torch.Tensor]]],
+ latent_height: int,
+ latent_width: int,
+ dtype: torch.dtype,
+ ) -> tuple[Union[BasicConditioningInfo, SDXLConditioningInfo], Optional[TextConditioningRegions]]:
+ """Concatenate regional text embeddings into a single embedding and track the region masks accordingly."""
+ if masks is None:
+ masks = [None] * len(text_conditionings)
+ assert len(text_conditionings) == len(masks)
+
+ is_sdxl = type(text_conditionings[0]) is SDXLConditioningInfo
+
+ all_masks_are_none = all(mask is None for mask in masks)
+
+ text_embedding = []
+ pooled_embedding = None
+ add_time_ids = None
+ cur_text_embedding_len = 0
+ processed_masks = []
+ embedding_ranges = []
+
+ for prompt_idx, text_embedding_info in enumerate(text_conditionings):
+ mask = masks[prompt_idx]
+
+ if is_sdxl:
+ # We choose a random SDXLConditioningInfo's pooled_embeds and add_time_ids here, with a preference for
+ # prompts without a mask. We prefer prompts without a mask, because they are more likely to contain
+ # global prompt information. In an ideal case, there should be exactly one global prompt without a
+ # mask, but we don't enforce this.
+
+ # HACK(ryand): The fact that we have to choose a single pooled_embedding and add_time_ids here is a
+ # fundamental interface issue. The SDXL Compel nodes are not designed to be used in the way that we use
+ # them for regional prompting. Ideally, the DenoiseLatents invocation should accept a single
+ # pooled_embeds tensor and a list of standard text embeds with region masks. This change would be a
+ # pretty major breaking change to a popular node, so for now we use this hack.
+ if pooled_embedding is None or mask is None:
+ pooled_embedding = text_embedding_info.pooled_embeds
+ if add_time_ids is None or mask is None:
+ add_time_ids = text_embedding_info.add_time_ids
+
+ text_embedding.append(text_embedding_info.embeds)
+ if not all_masks_are_none:
+ embedding_ranges.append(
+ Range(
+ start=cur_text_embedding_len, end=cur_text_embedding_len + text_embedding_info.embeds.shape[1]
+ )
+ )
+ processed_masks.append(
+ DenoiseLatentsInvocation._preprocess_regional_prompt_mask(
+ mask, latent_height, latent_width, dtype=dtype
+ )
+ )
+
+ cur_text_embedding_len += text_embedding_info.embeds.shape[1]
+
+ text_embedding = torch.cat(text_embedding, dim=1)
+ assert len(text_embedding.shape) == 3 # batch_size, seq_len, token_len
+
+ regions = None
+ if not all_masks_are_none:
+ regions = TextConditioningRegions(
+ masks=torch.cat(processed_masks, dim=1),
+ ranges=embedding_ranges,
+ )
+
+ if is_sdxl:
+ return (
+ SDXLConditioningInfo(embeds=text_embedding, pooled_embeds=pooled_embedding, add_time_ids=add_time_ids),
+ regions,
+ )
+ return BasicConditioningInfo(embeds=text_embedding), regions
+
+ @staticmethod
+ def get_conditioning_data(
+ context: InvocationContext,
+ positive_conditioning_field: Union[ConditioningField, list[ConditioningField]],
+ negative_conditioning_field: Union[ConditioningField, list[ConditioningField]],
+ latent_height: int,
+ latent_width: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ cfg_scale: float | list[float],
+ steps: int,
+ cfg_rescale_multiplier: float,
+ ) -> TextConditioningData:
+ # Normalize positive_conditioning_field and negative_conditioning_field to lists.
+ cond_list = positive_conditioning_field
+ if not isinstance(cond_list, list):
+ cond_list = [cond_list]
+ uncond_list = negative_conditioning_field
+ if not isinstance(uncond_list, list):
+ uncond_list = [uncond_list]
+
+ cond_text_embeddings, cond_text_embedding_masks = DenoiseLatentsInvocation._get_text_embeddings_and_masks(
+ cond_list, context, device, dtype
+ )
+ uncond_text_embeddings, uncond_text_embedding_masks = DenoiseLatentsInvocation._get_text_embeddings_and_masks(
+ uncond_list, context, device, dtype
+ )
+
+ cond_text_embedding, cond_regions = DenoiseLatentsInvocation._concat_regional_text_embeddings(
+ text_conditionings=cond_text_embeddings,
+ masks=cond_text_embedding_masks,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ dtype=dtype,
+ )
+ uncond_text_embedding, uncond_regions = DenoiseLatentsInvocation._concat_regional_text_embeddings(
+ text_conditionings=uncond_text_embeddings,
+ masks=uncond_text_embedding_masks,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ dtype=dtype,
+ )
+
+ if isinstance(cfg_scale, list):
+ assert len(cfg_scale) == steps, "cfg_scale (list) must have the same length as the number of steps"
+
+ conditioning_data = TextConditioningData(
+ uncond_text=uncond_text_embedding,
+ cond_text=cond_text_embedding,
+ uncond_regions=uncond_regions,
+ cond_regions=cond_regions,
+ guidance_scale=cfg_scale,
+ guidance_rescale_multiplier=cfg_rescale_multiplier,
+ )
+ return conditioning_data
+
+ @staticmethod
+ def create_pipeline(
+ unet: UNet2DConditionModel,
+ scheduler: Scheduler,
+ ) -> StableDiffusionGeneratorPipeline:
+ class FakeVae:
+ class FakeVaeConfig:
+ def __init__(self) -> None:
+ self.block_out_channels = [0]
+
+ def __init__(self) -> None:
+ self.config = FakeVae.FakeVaeConfig()
+
+ return StableDiffusionGeneratorPipeline(
+ vae=FakeVae(), # TODO: oh...
+ text_encoder=None,
+ tokenizer=None,
+ unet=unet,
+ scheduler=scheduler,
+ safety_checker=None,
+ feature_extractor=None,
+ requires_safety_checker=False,
+ )
+
+ @staticmethod
+ def prep_control_data(
+ context: InvocationContext,
+ control_input: ControlField | list[ControlField] | None,
+ latents_shape: List[int],
+ device: torch.device,
+ exit_stack: ExitStack,
+ do_classifier_free_guidance: bool = True,
+ ) -> list[ControlNetData] | None:
+ # Normalize control_input to a list.
+ control_list: list[ControlField]
+ if isinstance(control_input, ControlField):
+ control_list = [control_input]
+ elif isinstance(control_input, list):
+ control_list = control_input
+ elif control_input is None:
+ control_list = []
+ else:
+ raise ValueError(f"Unexpected control_input type: {type(control_input)}")
+
+ if len(control_list) == 0:
+ return None
+
+ # Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR.
+ _, _, latent_height, latent_width = latents_shape
+ control_height_resize = latent_height * LATENT_SCALE_FACTOR
+ control_width_resize = latent_width * LATENT_SCALE_FACTOR
+
+ controlnet_data: list[ControlNetData] = []
+ for control_info in control_list:
+ control_model = exit_stack.enter_context(context.models.load(control_info.control_model))
+ assert isinstance(control_model, ControlNetModel)
+
+ control_image_field = control_info.image
+ input_image = context.images.get_pil(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=device,
+ dtype=control_model.dtype,
+ control_mode=control_info.control_mode,
+ resize_mode=control_info.resize_mode,
+ )
+ control_item = ControlNetData(
+ model=control_model,
+ 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
+
+ @staticmethod
+ def parse_controlnet_field(
+ exit_stack: ExitStack,
+ context: InvocationContext,
+ control_input: ControlField | list[ControlField] | None,
+ ext_manager: ExtensionsManager,
+ ) -> None:
+ # Normalize control_input to a list.
+ control_list: list[ControlField]
+ if isinstance(control_input, ControlField):
+ control_list = [control_input]
+ elif isinstance(control_input, list):
+ control_list = control_input
+ elif control_input is None:
+ control_list = []
+ else:
+ raise ValueError(f"Unexpected control_input type: {type(control_input)}")
+
+ for control_info in control_list:
+ model = exit_stack.enter_context(context.models.load(control_info.control_model))
+ ext_manager.add_extension(
+ ControlNetExt(
+ model=model,
+ image=context.images.get_pil(control_info.image.image_name),
+ 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,
+ resize_mode=control_info.resize_mode,
+ )
+ )
+
+ @staticmethod
+ def parse_t2i_adapter_field(
+ exit_stack: ExitStack,
+ context: InvocationContext,
+ t2i_adapters: Optional[Union[T2IAdapterField, list[T2IAdapterField]]],
+ ext_manager: ExtensionsManager,
+ bgr_mode: bool = False,
+ ) -> None:
+ if t2i_adapters is None:
+ return
+
+ # Handle the possibility that t2i_adapters could be a list or a single T2IAdapterField.
+ if isinstance(t2i_adapters, T2IAdapterField):
+ t2i_adapters = [t2i_adapters]
+
+ for t2i_adapter_field in t2i_adapters:
+ image = context.images.get_pil(t2i_adapter_field.image.image_name)
+ if bgr_mode: # SDXL t2i trained on cv2's BGR outputs, but PIL won't convert straight to BGR
+ r, g, b = image.split()
+ image = Image.merge("RGB", (b, g, r))
+ ext_manager.add_extension(
+ T2IAdapterExt(
+ node_context=context,
+ model_id=t2i_adapter_field.t2i_adapter_model,
+ image=context.images.get_pil(t2i_adapter_field.image.image_name),
+ weight=t2i_adapter_field.weight,
+ begin_step_percent=t2i_adapter_field.begin_step_percent,
+ end_step_percent=t2i_adapter_field.end_step_percent,
+ resize_mode=t2i_adapter_field.resize_mode,
+ )
+ )
+
+ def prep_ip_adapter_image_prompts(
+ self,
+ context: InvocationContext,
+ ip_adapters: List[IPAdapterField],
+ ) -> List[Tuple[torch.Tensor, torch.Tensor]]:
+ """Run the IPAdapter CLIPVisionModel, returning image prompt embeddings."""
+ image_prompts = []
+ for single_ip_adapter in ip_adapters:
+ with context.models.load(single_ip_adapter.ip_adapter_model) as ip_adapter_model:
+ assert isinstance(ip_adapter_model, IPAdapter)
+ # `single_ip_adapter.image` could be a list or a single ImageField. Normalize to a list here.
+ single_ipa_image_fields = single_ip_adapter.image
+ if not isinstance(single_ipa_image_fields, list):
+ single_ipa_image_fields = [single_ipa_image_fields]
+
+ single_ipa_images = [
+ context.images.get_pil(image.image_name, mode="RGB") for image in single_ipa_image_fields
+ ]
+ with context.models.load(single_ip_adapter.image_encoder_model) as image_encoder_model:
+ assert isinstance(image_encoder_model, CLIPVisionModelWithProjection)
+ # 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
+ )
+ image_prompts.append((image_prompt_embeds, uncond_image_prompt_embeds))
+
+ return image_prompts
+
+ def prep_ip_adapter_data(
+ self,
+ context: InvocationContext,
+ ip_adapters: List[IPAdapterField],
+ image_prompts: List[Tuple[torch.Tensor, torch.Tensor]],
+ exit_stack: ExitStack,
+ latent_height: int,
+ latent_width: int,
+ dtype: torch.dtype,
+ ) -> Optional[List[IPAdapterData]]:
+ """If IP-Adapter is enabled, then this function loads the requisite models and adds the image prompt conditioning data."""
+ ip_adapter_data_list = []
+ for single_ip_adapter, (image_prompt_embeds, uncond_image_prompt_embeds) in zip(
+ ip_adapters, image_prompts, strict=True
+ ):
+ ip_adapter_model = exit_stack.enter_context(context.models.load(single_ip_adapter.ip_adapter_model))
+
+ mask_field = single_ip_adapter.mask
+ mask = context.tensors.load(mask_field.tensor_name) if mask_field is not None else None
+ mask = self._preprocess_regional_prompt_mask(mask, latent_height, latent_width, dtype=dtype)
+
+ ip_adapter_data_list.append(
+ IPAdapterData(
+ ip_adapter_model=ip_adapter_model,
+ weight=single_ip_adapter.weight,
+ target_blocks=single_ip_adapter.target_blocks,
+ begin_step_percent=single_ip_adapter.begin_step_percent,
+ end_step_percent=single_ip_adapter.end_step_percent,
+ ip_adapter_conditioning=IPAdapterConditioningInfo(image_prompt_embeds, uncond_image_prompt_embeds),
+ mask=mask,
+ method=single_ip_adapter.method,
+ )
+ )
+
+ return ip_adapter_data_list if len(ip_adapter_data_list) > 0 else None
+
+ def run_t2i_adapters(
+ self,
+ context: InvocationContext,
+ t2i_adapter: Optional[Union[T2IAdapterField, list[T2IAdapterField]]],
+ latents_shape: list[int],
+ device: torch.device,
+ 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_config = context.models.get_config(t2i_adapter_field.t2i_adapter_model.key)
+ image = context.images.get_pil(t2i_adapter_field.image.image_name, mode="RGB")
+
+ # The max_unet_downscale is the maximum amount that the UNet model downscales the latent image internally.
+ if t2i_adapter_model_config.base == BaseModelType.StableDiffusion1:
+ max_unet_downscale = 8
+ elif t2i_adapter_model_config.base == BaseModelType.StableDiffusionXL:
+ max_unet_downscale = 4
+
+ # SDXL adapters are trained on cv2's BGR outputs
+ r, g, b = image.split()
+ image = Image.merge("RGB", (b, g, r))
+ else:
+ raise ValueError(f"Unexpected T2I-Adapter base model type: '{t2i_adapter_model_config.base}'.")
+
+ t2i_adapter_model: T2IAdapter
+ with context.models.load(t2i_adapter_field.t2i_adapter_model) as t2i_adapter_model:
+ total_downscale_factor = t2i_adapter_model.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).
+
+ # Assuming fixed dimensional scaling of LATENT_SCALE_FACTOR.
+ _, _, latent_height, latent_width = latents_shape
+ control_height_resize = latent_height * LATENT_SCALE_FACTOR
+ control_width_resize = latent_width * LATENT_SCALE_FACTOR
+ t2i_image = prepare_control_image(
+ image=image,
+ do_classifier_free_guidance=False,
+ width=control_width_resize,
+ height=control_height_resize,
+ num_channels=t2i_adapter_model.config["in_channels"], # mypy treats this as a FrozenDict
+ device=device,
+ dtype=t2i_adapter_model.dtype,
+ resize_mode=t2i_adapter_field.resize_mode,
+ )
+
+ # 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.
+ # We crop the image to this size so that the positions match the input image on non-standard resolutions
+ t2i_input_height = latents_shape[2] // max_unet_downscale * total_downscale_factor
+ t2i_input_width = latents_shape[3] // max_unet_downscale * total_downscale_factor
+ if t2i_image.shape[2] > t2i_input_height or t2i_image.shape[3] > t2i_input_width:
+ t2i_image = t2i_image[
+ :, :, : min(t2i_image.shape[2], t2i_input_height), : min(t2i_image.shape[3], t2i_input_width)
+ ]
+
+ 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
+ # TODO: research more for second order schedulers timesteps
+ @staticmethod
+ def init_scheduler(
+ scheduler: Union[Scheduler, ConfigMixin],
+ device: torch.device,
+ steps: int,
+ denoising_start: float,
+ denoising_end: float,
+ seed: int,
+ ) -> Tuple[torch.Tensor, torch.Tensor, Dict[str, Any]]:
+ assert isinstance(scheduler, ConfigMixin)
+ if scheduler.config.get("cpu_only", False):
+ scheduler.set_timesteps(steps, device="cpu")
+ timesteps = scheduler.timesteps.to(device=device)
+ else:
+ scheduler.set_timesteps(steps, device=device)
+ timesteps = scheduler.timesteps
+
+ # skip greater order timesteps
+ _timesteps = timesteps[:: scheduler.order]
+
+ # get start timestep index
+ t_start_val = int(round(scheduler.config["num_train_timesteps"] * (1 - denoising_start)))
+ t_start_idx = len(list(filter(lambda ts: ts >= t_start_val, _timesteps)))
+
+ # get end timestep index
+ t_end_val = int(round(scheduler.config["num_train_timesteps"] * (1 - denoising_end)))
+ t_end_idx = len(list(filter(lambda ts: ts >= t_end_val, _timesteps[t_start_idx:])))
+
+ # apply order to indexes
+ t_start_idx *= scheduler.order
+ t_end_idx *= scheduler.order
+
+ init_timestep = timesteps[t_start_idx : t_start_idx + 1]
+ timesteps = timesteps[t_start_idx : t_start_idx + t_end_idx]
+
+ scheduler_step_kwargs: Dict[str, Any] = {}
+ scheduler_step_signature = inspect.signature(scheduler.step)
+ if "generator" in scheduler_step_signature.parameters:
+ # At some point, someone decided that schedulers that accept a generator should use the original seed with
+ # all bits flipped. I don't know the original rationale for this, but now we must keep it like this for
+ # reproducibility.
+ #
+ # These Invoke-supported schedulers accept a generator as of 2024-06-04:
+ # - DDIMScheduler
+ # - DDPMScheduler
+ # - DPMSolverMultistepScheduler
+ # - EulerAncestralDiscreteScheduler
+ # - EulerDiscreteScheduler
+ # - KDPM2AncestralDiscreteScheduler
+ # - LCMScheduler
+ # - TCDScheduler
+ scheduler_step_kwargs.update({"generator": torch.Generator(device=device).manual_seed(seed ^ 0xFFFFFFFF)})
+ if isinstance(scheduler, TCDScheduler):
+ scheduler_step_kwargs.update({"eta": 1.0})
+
+ return timesteps, init_timestep, scheduler_step_kwargs
+
+ def prep_inpaint_mask(
+ self, context: InvocationContext, latents: torch.Tensor
+ ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], bool]:
+ if self.denoise_mask is None:
+ return None, None, False
+
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+ mask = tv_resize(mask, latents.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
+ if self.denoise_mask.masked_latents_name is not None:
+ masked_latents = context.tensors.load(self.denoise_mask.masked_latents_name)
+ else:
+ masked_latents = torch.where(mask < 0.5, 0.0, latents)
+
+ return mask, masked_latents, self.denoise_mask.gradient
+
+ @staticmethod
+ def prepare_noise_and_latents(
+ context: InvocationContext, noise_field: LatentsField | None, latents_field: LatentsField | None
+ ) -> Tuple[int, torch.Tensor | None, torch.Tensor]:
+ """Depending on the workflow, we expect different combinations of noise and latents to be provided. This
+ function handles preparing these values accordingly.
+
+ Expected workflows:
+ - Text-to-Image Denoising: `noise` is provided, `latents` is not. `latents` is initialized to zeros.
+ - Image-to-Image Denoising: `noise` and `latents` are both provided.
+ - Text-to-Image SDXL Refiner Denoising: `latents` is provided, `noise` is not.
+ - Image-to-Image SDXL Refiner Denoising: `latents` is provided, `noise` is not.
+
+ NOTE(ryand): I wrote this docstring, but I am not the original author of this code. There may be other workflows
+ I haven't considered.
+ """
+ noise = None
+ if noise_field is not None:
+ noise = context.tensors.load(noise_field.latents_name)
+
+ if latents_field is not None:
+ latents = context.tensors.load(latents_field.latents_name)
+ elif noise is not None:
+ latents = torch.zeros_like(noise)
+ else:
+ raise ValueError("'latents' or 'noise' must be provided!")
+
+ if noise is not None and noise.shape[1:] != latents.shape[1:]:
+ raise ValueError(f"Incompatible 'noise' and 'latents' shapes: {latents.shape=} {noise.shape=}")
+
+ # The seed comes from (in order of priority): the noise field, the latents field, or 0.
+ seed = 0
+ if noise_field is not None and noise_field.seed is not None:
+ seed = noise_field.seed
+ elif latents_field is not None and latents_field.seed is not None:
+ seed = latents_field.seed
+ else:
+ seed = 0
+
+ return seed, noise, latents
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ if os.environ.get("USE_MODULAR_DENOISE", False):
+ return self._new_invoke(context)
+ else:
+ return self._old_invoke(context)
+
+ @torch.no_grad()
+ @SilenceWarnings() # This quenches the NSFW nag from diffusers.
+ def _new_invoke(self, context: InvocationContext) -> LatentsOutput:
+ ext_manager = ExtensionsManager(is_canceled=context.util.is_canceled)
+
+ device = TorchDevice.choose_torch_device()
+ dtype = TorchDevice.choose_torch_dtype()
+
+ seed, noise, latents = self.prepare_noise_and_latents(context, self.noise, self.latents)
+ _, _, latent_height, latent_width = latents.shape
+
+ # get the unet's config so that we can pass the base to sd_step_callback()
+ unet_config = context.models.get_config(self.unet.unet.key)
+
+ conditioning_data = self.get_conditioning_data(
+ context=context,
+ positive_conditioning_field=self.positive_conditioning,
+ negative_conditioning_field=self.negative_conditioning,
+ cfg_scale=self.cfg_scale,
+ steps=self.steps,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ device=device,
+ dtype=dtype,
+ # TODO: old backend, remove
+ cfg_rescale_multiplier=self.cfg_rescale_multiplier,
+ )
+
+ scheduler = get_scheduler(
+ context=context,
+ scheduler_info=self.unet.scheduler,
+ scheduler_name=self.scheduler,
+ seed=seed,
+ unet_config=unet_config,
+ )
+
+ timesteps, init_timestep, scheduler_step_kwargs = self.init_scheduler(
+ scheduler,
+ seed=seed,
+ device=device,
+ steps=self.steps,
+ denoising_start=self.denoising_start,
+ denoising_end=self.denoising_end,
+ )
+
+ ### preview
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, unet_config.base)
+
+ ext_manager.add_extension(PreviewExt(step_callback))
+
+ ### cfg rescale
+ if self.cfg_rescale_multiplier > 0:
+ ext_manager.add_extension(RescaleCFGExt(self.cfg_rescale_multiplier))
+
+ ### freeu
+ if self.unet.freeu_config:
+ ext_manager.add_extension(FreeUExt(self.unet.freeu_config))
+
+ ### lora
+ if self.unet.loras:
+ for lora_field in self.unet.loras:
+ ext_manager.add_extension(
+ LoRAExt(
+ node_context=context,
+ model_id=lora_field.lora,
+ weight=lora_field.weight,
+ )
+ )
+ ### seamless
+ if self.unet.seamless_axes:
+ ext_manager.add_extension(SeamlessExt(self.unet.seamless_axes))
+
+ ### inpaint
+ mask, masked_latents, is_gradient_mask = self.prep_inpaint_mask(context, latents)
+ # NOTE: We used to identify inpainting models by inspecting the shape of the loaded UNet model weights. Now we
+ # use the ModelVariantType config. During testing, there was a report of a user with models that had an
+ # incorrect ModelVariantType value. Re-installing the model fixed the issue. If this issue turns out to be
+ # prevalent, we will have to revisit how we initialize the inpainting extensions.
+ if unet_config.variant == ModelVariantType.Inpaint:
+ ext_manager.add_extension(InpaintModelExt(mask, masked_latents, is_gradient_mask))
+ elif mask is not None:
+ ext_manager.add_extension(InpaintExt(mask, is_gradient_mask))
+
+ # Initialize context for modular denoise
+ latents = latents.to(device=device, dtype=dtype)
+ if noise is not None:
+ noise = noise.to(device=device, dtype=dtype)
+ denoise_ctx = DenoiseContext(
+ inputs=DenoiseInputs(
+ orig_latents=latents,
+ timesteps=timesteps,
+ init_timestep=init_timestep,
+ noise=noise,
+ seed=seed,
+ scheduler_step_kwargs=scheduler_step_kwargs,
+ conditioning_data=conditioning_data,
+ attention_processor_cls=CustomAttnProcessor2_0,
+ ),
+ unet=None,
+ scheduler=scheduler,
+ )
+
+ # context for loading additional models
+ with ExitStack() as exit_stack:
+ # later should be smth like:
+ # for extension_field in self.extensions:
+ # ext = extension_field.to_extension(exit_stack, context, ext_manager)
+ # ext_manager.add_extension(ext)
+ self.parse_controlnet_field(exit_stack, context, self.control, ext_manager)
+ bgr_mode = self.unet.unet.base == BaseModelType.StableDiffusionXL
+ self.parse_t2i_adapter_field(exit_stack, context, self.t2i_adapter, ext_manager, bgr_mode)
+
+ # ext: t2i/ip adapter
+ ext_manager.run_callback(ExtensionCallbackType.SETUP, denoise_ctx)
+
+ with (
+ context.models.load(self.unet.unet).model_on_device() as (cached_weights, unet),
+ ModelPatcher.patch_unet_attention_processor(unet, denoise_ctx.inputs.attention_processor_cls),
+ # ext: controlnet
+ ext_manager.patch_extensions(denoise_ctx),
+ # ext: freeu, seamless, ip adapter, lora
+ ext_manager.patch_unet(unet, cached_weights),
+ ):
+ sd_backend = StableDiffusionBackend(unet, scheduler)
+ denoise_ctx.unet = unet
+ result_latents = sd_backend.latents_from_embeddings(denoise_ctx, ext_manager)
+
+ # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
+ result_latents = result_latents.detach().to("cpu")
+ TorchDevice.empty_cache()
+
+ name = context.tensors.save(tensor=result_latents)
+ return LatentsOutput.build(latents_name=name, latents=result_latents, seed=None)
+
+ @torch.no_grad()
+ @SilenceWarnings() # This quenches the NSFW nag from diffusers.
+ def _old_invoke(self, context: InvocationContext) -> LatentsOutput:
+ device = TorchDevice.choose_torch_device()
+ seed, noise, latents = self.prepare_noise_and_latents(context, self.noise, self.latents)
+
+ mask, masked_latents, gradient_mask = self.prep_inpaint_mask(context, latents)
+ # At this point, the mask ranges from 0 (leave unchanged) to 1 (inpaint).
+ # We invert the mask here for compatibility with the old backend implementation.
+ if mask is not None:
+ mask = 1 - mask
+
+ # 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,
+ device=device,
+ do_classifier_free_guidance=True,
+ )
+
+ ip_adapters: List[IPAdapterField] = []
+ if self.ip_adapter is not None:
+ # ip_adapter could be a list or a single IPAdapterField. Normalize to a list here.
+ if isinstance(self.ip_adapter, list):
+ ip_adapters = self.ip_adapter
+ else:
+ ip_adapters = [self.ip_adapter]
+
+ # If there are IP adapters, the following line runs the adapters' CLIPVision image encoders to return
+ # a series of image conditioning embeddings. This is being done here rather than in the
+ # big model context below in order to use less VRAM on low-VRAM systems.
+ # The image prompts are then passed to prep_ip_adapter_data().
+ image_prompts = self.prep_ip_adapter_image_prompts(context=context, ip_adapters=ip_adapters)
+
+ # get the unet's config so that we can pass the base to sd_step_callback()
+ unet_config = context.models.get_config(self.unet.unet.key)
+
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, unet_config.base)
+
+ def _lora_loader() -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in self.unet.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+ return
+
+ with (
+ ExitStack() as exit_stack,
+ context.models.load(self.unet.unet).model_on_device() as (cached_weights, unet),
+ ModelPatcher.apply_freeu(unet, self.unet.freeu_config),
+ SeamlessExt.static_patch_model(unet, self.unet.seamless_axes), # FIXME
+ # Apply the LoRA after unet has been moved to its target device for faster patching.
+ LayerPatcher.apply_smart_model_patches(
+ model=unet,
+ patches=_lora_loader(),
+ prefix="lora_unet_",
+ dtype=unet.dtype,
+ cached_weights=cached_weights,
+ ),
+ ):
+ assert isinstance(unet, UNet2DConditionModel)
+ latents = latents.to(device=device, dtype=unet.dtype)
+ if noise is not None:
+ noise = noise.to(device=device, dtype=unet.dtype)
+ if mask is not None:
+ mask = mask.to(device=device, dtype=unet.dtype)
+ if masked_latents is not None:
+ masked_latents = masked_latents.to(device=device, dtype=unet.dtype)
+
+ scheduler = get_scheduler(
+ context=context,
+ scheduler_info=self.unet.scheduler,
+ scheduler_name=self.scheduler,
+ seed=seed,
+ unet_config=unet_config,
+ )
+
+ pipeline = self.create_pipeline(unet, scheduler)
+
+ _, _, latent_height, latent_width = latents.shape
+ conditioning_data = self.get_conditioning_data(
+ context=context,
+ positive_conditioning_field=self.positive_conditioning,
+ negative_conditioning_field=self.negative_conditioning,
+ device=device,
+ dtype=unet.dtype,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ cfg_scale=self.cfg_scale,
+ steps=self.steps,
+ cfg_rescale_multiplier=self.cfg_rescale_multiplier,
+ )
+
+ controlnet_data = self.prep_control_data(
+ context=context,
+ control_input=self.control,
+ latents_shape=latents.shape,
+ device=device,
+ # do_classifier_free_guidance=(self.cfg_scale >= 1.0))
+ do_classifier_free_guidance=True,
+ exit_stack=exit_stack,
+ )
+
+ ip_adapter_data = self.prep_ip_adapter_data(
+ context=context,
+ ip_adapters=ip_adapters,
+ image_prompts=image_prompts,
+ exit_stack=exit_stack,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ dtype=unet.dtype,
+ )
+
+ timesteps, init_timestep, scheduler_step_kwargs = self.init_scheduler(
+ scheduler,
+ device=device,
+ steps=self.steps,
+ denoising_start=self.denoising_start,
+ denoising_end=self.denoising_end,
+ seed=seed,
+ )
+
+ result_latents = pipeline.latents_from_embeddings(
+ latents=latents,
+ timesteps=timesteps,
+ init_timestep=init_timestep,
+ noise=noise,
+ seed=seed,
+ mask=mask,
+ masked_latents=masked_latents,
+ is_gradient_mask=gradient_mask,
+ scheduler_step_kwargs=scheduler_step_kwargs,
+ conditioning_data=conditioning_data,
+ control_data=controlnet_data,
+ ip_adapter_data=ip_adapter_data,
+ t2i_adapter_data=t2i_adapter_data,
+ callback=step_callback,
+ )
+
+ # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
+ result_latents = result_latents.to("cpu")
+ TorchDevice.empty_cache()
+
+ name = context.tensors.save(tensor=result_latents)
+ return LatentsOutput.build(latents_name=name, latents=result_latents, seed=None)
diff --git a/invokeai/app/invocations/depth_anything.py b/invokeai/app/invocations/depth_anything.py
new file mode 100644
index 00000000000..1fd808efde5
--- /dev/null
+++ b/invokeai/app/invocations/depth_anything.py
@@ -0,0 +1,45 @@
+from typing import Literal
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.depth_anything.depth_anything_pipeline import DepthAnythingPipeline
+
+DEPTH_ANYTHING_MODEL_SIZES = Literal["large", "base", "small", "small_v2"]
+# DepthAnything V2 Small model is licensed under Apache 2.0 but not the base and large models.
+DEPTH_ANYTHING_MODELS = {
+ "large": "LiheYoung/depth-anything-large-hf",
+ "base": "LiheYoung/depth-anything-base-hf",
+ "small": "LiheYoung/depth-anything-small-hf",
+ "small_v2": "depth-anything/Depth-Anything-V2-Small-hf",
+}
+
+
+@invocation(
+ "depth_anything_depth_estimation",
+ title="Depth Anything Depth Estimation",
+ tags=["controlnet", "depth", "depth anything"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class DepthAnythingDepthEstimationInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates a depth map using a Depth Anything model."""
+
+ image: ImageField = InputField(description="The image to process")
+ model_size: DEPTH_ANYTHING_MODEL_SIZES = InputField(
+ default="small_v2", description="The size of the depth model to use"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ model_url = DEPTH_ANYTHING_MODELS[self.model_size]
+ image = context.images.get_pil(self.image.image_name, "RGB")
+
+ loaded_model = context.models.load_remote_model(model_url, DepthAnythingPipeline.load_model)
+
+ with loaded_model as depth_anything_detector:
+ assert isinstance(depth_anything_detector, DepthAnythingPipeline)
+ depth_map = depth_anything_detector.generate_depth(image)
+
+ image_dto = context.images.save(image=depth_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/dw_openpose.py b/invokeai/app/invocations/dw_openpose.py
new file mode 100644
index 00000000000..918a4bc4d03
--- /dev/null
+++ b/invokeai/app/invocations/dw_openpose.py
@@ -0,0 +1,50 @@
+import onnxruntime as ort
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector
+
+
+@invocation(
+ "dw_openpose_detection",
+ title="DW Openpose Detection",
+ tags=["controlnet", "dwpose", "openpose"],
+ category="controlnet_preprocessors",
+ version="1.1.1",
+)
+class DWOpenposeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an openpose pose from an image using DWPose"""
+
+ image: ImageField = InputField(description="The image to process")
+ draw_body: bool = InputField(default=True)
+ draw_face: bool = InputField(default=False)
+ draw_hands: bool = InputField(default=False)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+
+ onnx_det_path = context.models.download_and_cache_model(DWOpenposeDetector.get_model_url_det())
+ onnx_pose_path = context.models.download_and_cache_model(DWOpenposeDetector.get_model_url_pose())
+
+ loaded_session_det = context.models.load_local_model(
+ onnx_det_path, DWOpenposeDetector.create_onnx_inference_session
+ )
+ loaded_session_pose = context.models.load_local_model(
+ onnx_pose_path, DWOpenposeDetector.create_onnx_inference_session
+ )
+
+ with loaded_session_det as session_det, loaded_session_pose as session_pose:
+ assert isinstance(session_det, ort.InferenceSession)
+ assert isinstance(session_pose, ort.InferenceSession)
+ detector = DWOpenposeDetector(session_det=session_det, session_pose=session_pose)
+ detected_image = detector.run(
+ image,
+ draw_face=self.draw_face,
+ draw_hands=self.draw_hands,
+ draw_body=self.draw_body,
+ )
+ image_dto = context.images.save(image=detected_image)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/external_image_generation.py b/invokeai/app/invocations/external_image_generation.py
new file mode 100644
index 00000000000..a6c6822d9dd
--- /dev/null
+++ b/invokeai/app/invocations/external_image_generation.py
@@ -0,0 +1,351 @@
+from typing import TYPE_CHECKING, Any, ClassVar, Literal
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ MetadataField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageCollectionOutput
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+if TYPE_CHECKING:
+ from invokeai.app.services.invocation_services import InvocationServices
+
+
+class BaseExternalImageGenerationInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generate images using an external provider."""
+
+ provider_id: ClassVar[str | None] = None
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ )
+ mode: ExternalGenerationMode = InputField(
+ default="txt2img",
+ description="Generation mode. Not all modes are supported by every model; unsupported modes raise at runtime.",
+ )
+ prompt: str = InputField(description="Prompt")
+ seed: int | None = InputField(default=None, description=FieldDescriptions.seed)
+ num_images: int = InputField(default=1, gt=0, description="Number of images to generate")
+ width: int = InputField(default=1024, gt=0, description=FieldDescriptions.width)
+ height: int = InputField(default=1024, gt=0, description=FieldDescriptions.height)
+ image_size: str | None = InputField(default=None, description="Image size preset (e.g. 1K, 2K, 4K)")
+ init_image: ImageField | None = InputField(default=None, description="Init image for img2img/inpaint")
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint")
+ reference_images: list[ImageField] = InputField(default=[], description="Reference images")
+
+ def _build_provider_options(self) -> dict[str, Any] | None:
+ """Override in provider-specific subclasses to pass extra options."""
+ return None
+
+ def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
+ model_config = context.models.get_config(self.model)
+ if not isinstance(model_config, ExternalApiModelConfig):
+ raise ValueError("Selected model is not an external API model")
+
+ if self.provider_id is not None and model_config.provider_id != self.provider_id:
+ raise ValueError(
+ f"Selected model provider '{model_config.provider_id}' does not match node provider '{self.provider_id}'"
+ )
+
+ init_image = None
+ if self.init_image is not None:
+ init_image = context.images.get_pil(self.init_image.image_name, mode="RGB")
+
+ mask_image = None
+ if self.mask_image is not None:
+ mask_image = context.images.get_pil(self.mask_image.image_name, mode="L")
+
+ reference_images: list[ExternalReferenceImage] = []
+ for image_field in self.reference_images:
+ reference_image = context.images.get_pil(image_field.image_name, mode="RGB")
+ reference_images.append(ExternalReferenceImage(image=reference_image))
+
+ request = ExternalGenerationRequest(
+ model=model_config,
+ mode=self.mode,
+ prompt=self.prompt,
+ seed=self.seed,
+ num_images=self.num_images,
+ width=self.width,
+ height=self.height,
+ image_size=self.image_size,
+ init_image=init_image,
+ mask_image=mask_image,
+ reference_images=reference_images,
+ metadata=self._build_request_metadata(),
+ provider_options=self._build_provider_options(),
+ )
+
+ result = context._services.external_generation.generate(request)
+
+ outputs: list[ImageField] = []
+ for generated in result.images:
+ metadata = self._build_output_metadata(model_config, result, generated.seed)
+ image_dto = context.images.save(image=generated.image, metadata=metadata)
+ outputs.append(ImageField(image_name=image_dto.image_name))
+
+ return ImageCollectionOutput(collection=outputs)
+
+ def invoke_internal(self, context: InvocationContext, services: "InvocationServices") -> BaseInvocationOutput:
+ """Override default cache behavior so cache hits produce new gallery entries.
+
+ The standard invocation cache returns the cached output (with stale image_name
+ references) without re-running invoke(), which means no new images are saved
+ to the gallery on repeat invokes. For external API nodes — where the API call
+ is the expensive part — we want cache hits to skip the API call but still
+ produce fresh gallery entries by copying the cached images.
+ """
+ if services.configuration.node_cache_size == 0 or not self.use_cache:
+ return super().invoke_internal(context, services)
+
+ key = services.invocation_cache.create_key(self)
+ cached_value = services.invocation_cache.get(key)
+ if cached_value is None:
+ services.logger.debug(f'Invocation cache miss for type "{self.get_type()}": {self.id}')
+ output = self.invoke(context)
+ services.invocation_cache.save(key, output)
+ return output
+
+ services.logger.debug(f'Invocation cache hit for type "{self.get_type()}": {self.id}, duplicating images')
+ if not isinstance(cached_value, ImageCollectionOutput):
+ return cached_value
+
+ outputs: list[ImageField] = []
+ for image_field in cached_value.collection:
+ cached_image = context.images.get_pil(image_field.image_name, mode="RGB")
+ image_dto = context.images.save(image=cached_image)
+ outputs.append(ImageField(image_name=image_dto.image_name))
+ return ImageCollectionOutput(collection=outputs)
+
+ def _build_request_metadata(self) -> dict[str, Any] | None:
+ if self.metadata is None:
+ return None
+ return self.metadata.root
+
+ def _build_output_metadata(
+ self,
+ model_config: ExternalApiModelConfig,
+ result: ExternalGenerationResult,
+ image_seed: int | None,
+ ) -> MetadataField | None:
+ metadata: dict[str, Any] = {}
+
+ if self.metadata is not None:
+ metadata.update(self.metadata.root)
+
+ metadata.update(
+ {
+ "external_provider": model_config.provider_id,
+ "external_model_id": model_config.provider_model_id,
+ }
+ )
+
+ if self.image_size is not None:
+ metadata["image_size"] = self.image_size
+
+ provider_request_id = getattr(result, "provider_request_id", None)
+ if provider_request_id:
+ metadata["external_request_id"] = provider_request_id
+
+ provider_metadata = getattr(result, "provider_metadata", None)
+ if provider_metadata:
+ metadata["external_provider_metadata"] = provider_metadata
+
+ if image_seed is not None:
+ metadata["external_seed"] = image_seed
+
+ metadata.update(self._build_output_provider_metadata())
+
+ if not metadata:
+ return None
+ return MetadataField(root=metadata)
+
+ def _build_output_provider_metadata(self) -> dict[str, Any]:
+ """Override in provider-specific subclasses to add recall-relevant fields to the image metadata."""
+ return {}
+
+
+@invocation(
+ "openai_image_generation",
+ title="OpenAI Image Generation",
+ tags=["external", "generation", "openai"],
+ category="image",
+ version="1.0.0",
+)
+class OpenAIImageGenerationInvocation(BaseExternalImageGenerationInvocation):
+ """Generate images using an OpenAI-hosted external model."""
+
+ provider_id = "openai"
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ ui_model_provider_id=["openai"],
+ )
+
+ # OpenAI's API has no img2img/inpaint distinction — the edits endpoint is used
+ # automatically when reference images are provided. Hide mode and init_image
+ # (init_image is functionally identical to a reference image), and hide
+ # mask_image since no OpenAI model supports inpainting.
+ mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
+ init_image: ImageField | None = InputField(
+ default=None, description="Init image (use reference_images instead)", ui_hidden=True
+ )
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
+
+ quality: Literal["auto", "high", "medium", "low"] = InputField(default="auto", description="Output image quality")
+ background: Literal["auto", "transparent", "opaque"] = InputField(
+ default="auto", description="Background transparency handling"
+ )
+ input_fidelity: Literal["low", "high"] | None = InputField(
+ default=None, description="Fidelity to source images (edits only)"
+ )
+
+ def _build_provider_options(self) -> dict[str, Any]:
+ options: dict[str, Any] = {
+ "quality": self.quality,
+ "background": self.background,
+ }
+ if self.input_fidelity is not None:
+ options["input_fidelity"] = self.input_fidelity
+ return options
+
+ def _build_output_provider_metadata(self) -> dict[str, Any]:
+ metadata: dict[str, Any] = {
+ "openai_quality": self.quality,
+ "openai_background": self.background,
+ }
+ if self.input_fidelity is not None:
+ metadata["openai_input_fidelity"] = self.input_fidelity
+ return metadata
+
+
+@invocation(
+ "gemini_image_generation",
+ title="Gemini Image Generation",
+ tags=["external", "generation", "gemini"],
+ category="image",
+ version="1.0.0",
+)
+class GeminiImageGenerationInvocation(BaseExternalImageGenerationInvocation):
+ """Generate images using a Gemini-hosted external model."""
+
+ provider_id = "gemini"
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ ui_model_provider_id=["gemini"],
+ )
+
+ # Gemini only supports txt2img — hide mode/init_image/mask_image fields
+ # that are inherited from the base class but not usable with any Gemini model.
+ mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
+ init_image: ImageField | None = InputField(
+ default=None, description="Init image for img2img/inpaint", ui_hidden=True
+ )
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
+
+ temperature: float | None = InputField(default=None, ge=0.0, le=2.0, description="Sampling temperature")
+ thinking_level: Literal["minimal", "high"] | None = InputField(
+ default=None, description="Thinking level for image generation"
+ )
+
+ def _build_provider_options(self) -> dict[str, Any] | None:
+ options: dict[str, Any] = {}
+ if self.temperature is not None:
+ options["temperature"] = self.temperature
+ if self.thinking_level is not None:
+ options["thinking_level"] = self.thinking_level
+ return options or None
+
+ def _build_output_provider_metadata(self) -> dict[str, Any]:
+ metadata: dict[str, Any] = {}
+ if self.temperature is not None:
+ metadata["gemini_temperature"] = self.temperature
+ if self.thinking_level is not None:
+ metadata["gemini_thinking_level"] = self.thinking_level
+ return metadata
+
+
+@invocation(
+ "seedream_image_generation",
+ title="Seedream Image Generation",
+ tags=["external", "generation", "seedream"],
+ category="image",
+ version="1.1.0",
+)
+class SeedreamImageGenerationInvocation(BaseExternalImageGenerationInvocation):
+ """Generate images using a BytePlus Seedream model."""
+
+ provider_id = "seedream"
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ ui_model_provider_id=["seedream"],
+ )
+
+ # Seedream's API has only one endpoint and no inpaint support — mode is implicit
+ # from inputs (img2img happens automatically when init_image or reference_images
+ # are provided). Hide mode and mask_image since they have no effect.
+ mode: ExternalGenerationMode = InputField(default="txt2img", description="Generation mode.", ui_hidden=True)
+ mask_image: ImageField | None = InputField(default=None, description="Mask image for inpaint", ui_hidden=True)
+
+ watermark: bool = InputField(default=False, description="Add watermark to generated images")
+ optimize_prompt: bool = InputField(default=False, description="Let the model optimize the prompt before generation")
+
+ def _build_provider_options(self) -> dict[str, Any]:
+ return {
+ "watermark": self.watermark,
+ "optimize_prompt": self.optimize_prompt,
+ }
+
+ def _build_output_provider_metadata(self) -> dict[str, Any]:
+ return {
+ "seedream_watermark": self.watermark,
+ "seedream_optimize_prompt": self.optimize_prompt,
+ }
+
+
+@invocation(
+ "alibabacloud_image_generation",
+ title="Alibaba Cloud DashScope Image Generation",
+ tags=["external", "generation", "alibabacloud", "dashscope"],
+ category="image",
+ version="1.0.0",
+)
+class AlibabaCloudImageGenerationInvocation(BaseExternalImageGenerationInvocation):
+ """Generate images using an Alibaba Cloud DashScope external model."""
+
+ provider_id = "alibabacloud"
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.External],
+ ui_model_type=[ModelType.ExternalImageGenerator],
+ ui_model_format=[ModelFormat.ExternalApi],
+ ui_model_provider_id=["alibabacloud"],
+ )
diff --git a/invokeai/app/invocations/facetools.py b/invokeai/app/invocations/facetools.py
new file mode 100644
index 00000000000..1092a67ce95
--- /dev/null
+++ b/invokeai/app/invocations/facetools.py
@@ -0,0 +1,689 @@
+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,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@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.logger.warning("FaceTools --> -X-axis padding reached image edge.")
+ x_max -= x_min
+ x_min = 0
+ elif x_max > mask.width:
+ context.logger.warning("FaceTools --> +X-axis padding reached image edge.")
+ x_min -= x_max - mask.width
+ x_max = mask.width
+
+ if y_min < 0:
+ context.logger.warning("FaceTools --> +Y-axis padding reached image edge.")
+ y_max -= y_min
+ y_min = 0
+ elif y_max > mask.height:
+ context.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.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.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.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.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.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.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.logger.info(f"FaceTools --> Chunk starting at y = {y}")
+
+ for idx in range(len(image_chunks)):
+ context.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.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="segmentation", version="1.2.2"
+)
+class FaceOffInvocation(BaseInvocation, 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.logger.warning("FaceOff --> No faces detected. Passing through original image.")
+ return None
+
+ if self.face_id > len(all_faces) - 1:
+ context.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.images.get_pil(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.images.save(image=result_image)
+
+ mask_dto = context.images.save(image=result_mask, image_category=ImageCategory.MASK)
+
+ 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="segmentation", version="1.2.2"
+)
+class FaceMaskInvocation(BaseInvocation, 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.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.images.get_pil(self.image.image_name)
+ result = self.facemask(context=context, image=image)
+
+ image_dto = context.images.save(image=result["image"])
+
+ mask_dto = context.images.save(image=result["mask"], image_category=ImageCategory.MASK)
+
+ 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="segmentation",
+ version="1.2.2",
+)
+class FaceIdentifierInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """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.images.get_pil(self.image.image_name)
+ result_image = self.faceidentifier(context=context, image=image)
+
+ image_dto = context.images.save(image=result_image)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/fields.py b/invokeai/app/invocations/fields.py
new file mode 100644
index 00000000000..e53aeb417b2
--- /dev/null
+++ b/invokeai/app/invocations/fields.py
@@ -0,0 +1,879 @@
+from enum import Enum
+from typing import Any, Callable, Optional, Tuple
+
+from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter
+from pydantic.fields import _Unset
+from pydantic_core import PydanticUndefined
+
+from invokeai.app.util.metaenum import MetaEnum
+from invokeai.backend.image_util.segment_anything.shared import BoundingBox
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ClipVariantType,
+ ModelFormat,
+ ModelType,
+ ModelVariantType,
+)
+from invokeai.backend.util.logging import InvokeAILogger
+
+logger = InvokeAILogger.get_logger()
+
+
+class UIType(str, Enum, metaclass=MetaEnum):
+ """
+ Type hints for the UI for situations in which the field type is not enough to infer the correct UI type.
+
+ - Model Fields
+ The most common node-author-facing use will be for model fields. Internally, there is no difference
+ between SD-1, SD-2 and SDXL model fields - they all use the class `MainModelField`. To ensure the
+ base-model-specific UI is rendered, use e.g. `ui_type=UIType.SDXLMainModelField` to indicate that
+ the field is an SDXL main model field.
+
+ - Any Field
+ We cannot infer the usage of `typing.Any` via schema parsing, so you *must* use `ui_type=UIType.Any` to
+ indicate that the field accepts any type. Use with caution. This cannot be used on outputs.
+
+ - Scheduler Field
+ Special handling in the UI is needed for this field, which otherwise would be parsed as a plain enum field.
+
+ - Internal Fields
+ Similar to the Any Field, the `collect` and `iterate` nodes make use of `typing.Any`. To facilitate
+ handling these types in the client, we use `UIType._Collection` and `UIType._CollectionItem`. These
+ should not be used by node authors.
+
+ - DEPRECATED Fields
+ These types are deprecated and should not be used by node authors. A warning will be logged if one is
+ used, and the type will be ignored. They are included here for backwards compatibility.
+ """
+
+ # region Misc Field Types
+ Scheduler = "SchedulerField"
+ Any = "AnyField"
+ # endregion
+
+ # region Internal Field Types
+ _Collection = "CollectionField"
+ _CollectionItem = "CollectionItemField"
+ _IsIntermediate = "IsIntermediate"
+ # endregion
+
+ # region DEPRECATED
+ Boolean = "DEPRECATED_Boolean"
+ Color = "DEPRECATED_Color"
+ Conditioning = "DEPRECATED_Conditioning"
+ Control = "DEPRECATED_Control"
+ Float = "DEPRECATED_Float"
+ Image = "DEPRECATED_Image"
+ Integer = "DEPRECATED_Integer"
+ Latents = "DEPRECATED_Latents"
+ String = "DEPRECATED_String"
+ BooleanCollection = "DEPRECATED_BooleanCollection"
+ ColorCollection = "DEPRECATED_ColorCollection"
+ ConditioningCollection = "DEPRECATED_ConditioningCollection"
+ ControlCollection = "DEPRECATED_ControlCollection"
+ FloatCollection = "DEPRECATED_FloatCollection"
+ ImageCollection = "DEPRECATED_ImageCollection"
+ IntegerCollection = "DEPRECATED_IntegerCollection"
+ LatentsCollection = "DEPRECATED_LatentsCollection"
+ StringCollection = "DEPRECATED_StringCollection"
+ BooleanPolymorphic = "DEPRECATED_BooleanPolymorphic"
+ ColorPolymorphic = "DEPRECATED_ColorPolymorphic"
+ ConditioningPolymorphic = "DEPRECATED_ConditioningPolymorphic"
+ ControlPolymorphic = "DEPRECATED_ControlPolymorphic"
+ FloatPolymorphic = "DEPRECATED_FloatPolymorphic"
+ ImagePolymorphic = "DEPRECATED_ImagePolymorphic"
+ IntegerPolymorphic = "DEPRECATED_IntegerPolymorphic"
+ LatentsPolymorphic = "DEPRECATED_LatentsPolymorphic"
+ StringPolymorphic = "DEPRECATED_StringPolymorphic"
+ UNet = "DEPRECATED_UNet"
+ Vae = "DEPRECATED_Vae"
+ CLIP = "DEPRECATED_CLIP"
+ Collection = "DEPRECATED_Collection"
+ CollectionItem = "DEPRECATED_CollectionItem"
+ Enum = "DEPRECATED_Enum"
+ WorkflowField = "DEPRECATED_WorkflowField"
+ BoardField = "DEPRECATED_BoardField"
+ MetadataItem = "DEPRECATED_MetadataItem"
+ MetadataItemCollection = "DEPRECATED_MetadataItemCollection"
+ MetadataItemPolymorphic = "DEPRECATED_MetadataItemPolymorphic"
+ MetadataDict = "DEPRECATED_MetadataDict"
+
+ # Deprecated Model Field Types - use ui_model_[base|type|variant|format] instead
+ MainModel = "DEPRECATED_MainModelField"
+ CogView4MainModel = "DEPRECATED_CogView4MainModelField"
+ FluxMainModel = "DEPRECATED_FluxMainModelField"
+ SD3MainModel = "DEPRECATED_SD3MainModelField"
+ SDXLMainModel = "DEPRECATED_SDXLMainModelField"
+ SDXLRefinerModel = "DEPRECATED_SDXLRefinerModelField"
+ ONNXModel = "DEPRECATED_ONNXModelField"
+ VAEModel = "DEPRECATED_VAEModelField"
+ FluxVAEModel = "DEPRECATED_FluxVAEModelField"
+ LoRAModel = "DEPRECATED_LoRAModelField"
+ ControlNetModel = "DEPRECATED_ControlNetModelField"
+ IPAdapterModel = "DEPRECATED_IPAdapterModelField"
+ T2IAdapterModel = "DEPRECATED_T2IAdapterModelField"
+ T5EncoderModel = "DEPRECATED_T5EncoderModelField"
+ CLIPEmbedModel = "DEPRECATED_CLIPEmbedModelField"
+ CLIPLEmbedModel = "DEPRECATED_CLIPLEmbedModelField"
+ CLIPGEmbedModel = "DEPRECATED_CLIPGEmbedModelField"
+ SpandrelImageToImageModel = "DEPRECATED_SpandrelImageToImageModelField"
+ ControlLoRAModel = "DEPRECATED_ControlLoRAModelField"
+ SigLipModel = "DEPRECATED_SigLipModelField"
+ FluxReduxModel = "DEPRECATED_FluxReduxModelField"
+ LlavaOnevisionModel = "DEPRECATED_LLaVAModelField"
+ Imagen3Model = "DEPRECATED_Imagen3ModelField"
+ Imagen4Model = "DEPRECATED_Imagen4ModelField"
+ ChatGPT4oModel = "DEPRECATED_ChatGPT4oModelField"
+ Gemini2_5Model = "DEPRECATED_Gemini2_5ModelField"
+ FluxKontextModel = "DEPRECATED_FluxKontextModelField"
+ Veo3Model = "DEPRECATED_Veo3ModelField"
+ RunwayModel = "DEPRECATED_RunwayModelField"
+ # endregion
+
+
+class UIComponent(str, Enum, metaclass=MetaEnum):
+ """
+ The type of UI component to use for a field, used to override the default components, which are
+ inferred from the field type.
+ """
+
+ None_ = "none"
+ Textarea = "textarea"
+ Slider = "slider"
+
+
+class FieldDescriptions:
+ 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"
+ cfg_rescale_multiplier = "Rescale multiplier for CFG guidance, used for models trained with zero-terminal SNR"
+ 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"
+ t5_encoder = "T5 tokenizer and text encoder"
+ glm_encoder = "GLM (THUDM) tokenizer and text encoder"
+ qwen3_encoder = "Qwen3 tokenizer and text encoder"
+ clip_embed_model = "CLIP Embed loader"
+ clip_g_model = "CLIP-G Embed loader"
+ unet = "UNet (scheduler, LoRAs)"
+ transformer = "Transformer"
+ mmditx = "MMDiTX"
+ vae = "VAE"
+ cond = "Conditioning tensor"
+ controlnet_model = "ControlNet model to load"
+ vae_model = "VAE model to load"
+ lora_model = "LoRA model to load"
+ control_lora_model = "Control LoRA model to load"
+ main_model = "Main model (UNet, VAE, CLIP) to load"
+ flux_model = "Flux model (Transformer) to load"
+ sd3_model = "SD3 model (MMDiTX) to load"
+ cogview4_model = "CogView4 model (Transformer) to load"
+ z_image_model = "Z-Image model (Transformer) to load"
+ qwen_image_model = "Qwen Image Edit model (Transformer) to load"
+ qwen_vl_encoder = "Qwen2.5-VL tokenizer, processor and text/vision encoder"
+ 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"
+ spandrel_image_to_image_model = "Image-to-Image model"
+ vllm_model = "VLLM model"
+ 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"
+ ip_adapter = "IP-Adapter to apply"
+ t2i_adapter = "T2I-Adapter(s) to apply"
+ denoised_latents = "Denoised latents tensor"
+ latents = "Latents tensor"
+ strength = "Strength of denoising (proportional to steps)"
+ metadata = "Optional metadata to be saved with the image"
+ metadata_collection = "Collection of Metadata"
+ metadata_item_polymorphic = "A single metadata item or collection of metadata items"
+ metadata_item_label = "Label for this metadata item"
+ metadata_item_value = "The value for this metadata item (may be any type)"
+ workflow = "Optional workflow to be saved with the 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)"
+ vae_tile_size = "The tile size for VAE tiling in pixels (image space). If set to 0, the default tile size for the model will be used. Larger tile sizes generally produce better results at the cost of higher memory usage."
+ 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"
+ denoise_mask = "A mask of the region to apply the denoising process to. Values of 0.0 represent the regions to be fully denoised, and 1.0 represent the regions to be preserved."
+ board = "The board to save the image to"
+ image = "The image to process"
+ tile_size = "Tile size"
+ inclusive_low = "The inclusive low value"
+ exclusive_high = "The exclusive high value"
+ decimal_places = "The number of decimal places to round to"
+ freeu_s1 = 'Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.'
+ freeu_s2 = 'Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to mitigate the "oversmoothing effect" in the enhanced denoising process.'
+ freeu_b1 = "Scaling factor for stage 1 to amplify the contributions of backbone features."
+ freeu_b2 = "Scaling factor for stage 2 to amplify the contributions of backbone features."
+ instantx_control_mode = "The control mode for InstantX ControlNet union models. Ignored for other ControlNet models. The standard mapping is: canny (0), tile (1), depth (2), blur (3), pose (4), gray (5), low quality (6). Negative values will be treated as 'None'."
+ flux_redux_conditioning = "FLUX Redux conditioning tensor"
+ vllm_model = "The VLLM model to use"
+ text_llm_model = "The text language model to use for text generation"
+ flux_fill_conditioning = "FLUX Fill conditioning tensor"
+ flux_kontext_conditioning = "FLUX Kontext conditioning (reference image)"
+
+
+class ImageField(BaseModel):
+ """An image primitive field"""
+
+ image_name: str = Field(description="The name of the image")
+
+
+class BoardField(BaseModel):
+ """A board primitive field"""
+
+ board_id: str = Field(description="The id of the board")
+
+
+class StylePresetField(BaseModel):
+ """A style preset primitive field"""
+
+ style_preset_id: str = Field(description="The id of the style preset")
+
+
+class DenoiseMaskField(BaseModel):
+ """An inpaint mask field"""
+
+ mask_name: str = Field(description="The name of the mask image")
+ masked_latents_name: Optional[str] = Field(default=None, description="The name of the masked image latents")
+ gradient: bool = Field(default=False, description="Used for gradient inpainting")
+
+
+class TensorField(BaseModel):
+ """A tensor primitive field."""
+
+ tensor_name: str = Field(description="The name of a tensor.")
+
+
+class LatentsField(BaseModel):
+ """A latents tensor primitive field"""
+
+ latents_name: str = Field(description="The name of the latents")
+ seed: Optional[int] = Field(default=None, description="Seed used to generate this latents")
+
+
+class ColorField(BaseModel):
+ """A color primitive field"""
+
+ r: int = Field(ge=0, le=255, description="The red component")
+ g: int = Field(ge=0, le=255, description="The green component")
+ b: int = Field(ge=0, le=255, description="The blue component")
+ a: int = Field(ge=0, le=255, description="The alpha component")
+
+ def tuple(self) -> Tuple[int, int, int, int]:
+ return (self.r, self.g, self.b, self.a)
+
+
+class FluxConditioningField(BaseModel):
+ """A conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The mask associated with this conditioning tensor. Excluded regions should be set to False, "
+ "included regions should be set to True.",
+ )
+
+
+class FluxReduxConditioningField(BaseModel):
+ """A FLUX Redux conditioning tensor primitive value"""
+
+ conditioning: TensorField = Field(description="The Redux image conditioning tensor.")
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The mask associated with this conditioning tensor. Excluded regions should be set to False, "
+ "included regions should be set to True.",
+ )
+
+
+class FluxFillConditioningField(BaseModel):
+ """A FLUX Fill conditioning field."""
+
+ image: ImageField = Field(description="The FLUX Fill reference image.")
+ mask: TensorField = Field(description="The FLUX Fill inpaint mask.")
+
+
+class FluxKontextConditioningField(BaseModel):
+ """A conditioning field for FLUX Kontext (reference image)."""
+
+ image: ImageField = Field(description="The Kontext reference image.")
+
+
+class SD3ConditioningField(BaseModel):
+ """A conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+
+
+class CogView4ConditioningField(BaseModel):
+ """A conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+
+
+class ZImageConditioningField(BaseModel):
+ """A Z-Image conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The mask associated with this conditioning tensor for regional prompting. "
+ "Excluded regions should be set to False, included regions should be set to True.",
+ )
+
+
+class QwenImageConditioningField(BaseModel):
+ """A Qwen Image Edit conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+
+
+class AnimaConditioningField(BaseModel):
+ """An Anima conditioning tensor primitive value.
+
+ Anima conditioning contains Qwen3 0.6B hidden states and T5-XXL token IDs,
+ which are combined by the LLM Adapter inside the transformer.
+ """
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The mask associated with this conditioning tensor for regional prompting. "
+ "Excluded regions should be set to False, included regions should be set to True.",
+ )
+
+
+class ConditioningField(BaseModel):
+ """A conditioning tensor primitive value"""
+
+ conditioning_name: str = Field(description="The name of conditioning tensor")
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The mask associated with this conditioning tensor. Excluded regions should be set to False, "
+ "included regions should be set to True.",
+ )
+
+
+class BoundingBoxField(BoundingBox):
+ """A bounding box primitive value."""
+
+ score: Optional[float] = Field(
+ default=None,
+ ge=0.0,
+ le=1.0,
+ description="The score associated with the bounding box. In the range [0, 1]. This value is typically set "
+ "when the bounding box was produced by a detector and has an associated confidence score.",
+ )
+
+
+class MetadataField(RootModel[dict[str, Any]]):
+ """
+ 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 Input(str, Enum, metaclass=MetaEnum):
+ """
+ The type of input a field accepts.
+ - `Input.Direct`: The field must have its value provided directly, when the invocation and field \
+ are instantiated.
+ - `Input.Connection`: The field must have its value provided by a connection.
+ - `Input.Any`: The field may have its value provided either directly or by a connection.
+ """
+
+ Connection = "connection"
+ Direct = "direct"
+ Any = "any"
+
+
+class FieldKind(str, Enum, metaclass=MetaEnum):
+ """
+ The kind of field.
+ - `Input`: An input field on a node.
+ - `Output`: An output field on a node.
+ - `Internal`: A field which is treated as an input, but cannot be used in node definitions. Metadata is
+ one example. It is provided to nodes via the WithMetadata class, and we want to reserve the field name
+ "metadata" for this on all nodes. `FieldKind` is used to short-circuit the field name validation logic,
+ allowing "metadata" for that field.
+ - `NodeAttribute`: The field is a node attribute. These are fields which are not inputs or outputs,
+ but which are used to store information about the node. For example, the `id` and `type` fields are node
+ attributes.
+
+ The presence of this in `json_schema_extra["field_kind"]` is used when initializing node schemas on app
+ startup, and when generating the OpenAPI schema for the workflow editor.
+ """
+
+ Input = "input"
+ Output = "output"
+ Internal = "internal"
+ NodeAttribute = "node_attribute"
+
+
+class InputFieldJSONSchemaExtra(BaseModel):
+ """
+ Extra attributes to be added to input fields and their OpenAPI schema. Used during graph execution,
+ and by the workflow editor during schema parsing and UI rendering.
+ """
+
+ input: Input
+ field_kind: FieldKind
+ orig_required: bool = True
+ default: Optional[Any] = None
+ orig_default: Optional[Any] = None
+ ui_hidden: bool = False
+ ui_type: Optional[UIType] = None
+ ui_component: Optional[UIComponent] = None
+ ui_order: Optional[int] = None
+ ui_choice_labels: Optional[dict[str, str]] = None
+ ui_model_base: Optional[list[BaseModelType]] = None
+ ui_model_type: Optional[list[ModelType]] = None
+ ui_model_variant: Optional[list[ClipVariantType | ModelVariantType]] = None
+ ui_model_format: Optional[list[ModelFormat]] = None
+ ui_model_provider_id: Optional[list[str]] = None
+
+ model_config = ConfigDict(
+ validate_assignment=True,
+ json_schema_serialization_defaults_required=True,
+ use_enum_values=True,
+ )
+
+
+class WithMetadata(BaseModel):
+ """
+ Inherit from this class if your node needs a metadata input field.
+ """
+
+ metadata: Optional[MetadataField] = Field(
+ default=None,
+ description=FieldDescriptions.metadata,
+ json_schema_extra=InputFieldJSONSchemaExtra(
+ field_kind=FieldKind.Internal,
+ input=Input.Connection,
+ orig_required=False,
+ ).model_dump(exclude_none=True),
+ )
+
+
+class WithWorkflow:
+ workflow = None
+
+ def __init_subclass__(cls) -> None:
+ logger.warning(
+ f"{cls.__module__.split('.')[0]}.{cls.__name__}: WithWorkflow is deprecated. Use `context.workflow` to access the workflow."
+ )
+ super().__init_subclass__()
+
+
+class WithBoard(BaseModel):
+ """
+ Inherit from this class if your node needs a board input field.
+ """
+
+ board: Optional[BoardField] = Field(
+ default=None,
+ description=FieldDescriptions.board,
+ json_schema_extra=InputFieldJSONSchemaExtra(
+ field_kind=FieldKind.Internal,
+ input=Input.Direct,
+ orig_required=False,
+ ).model_dump(exclude_none=True),
+ )
+
+
+class OutputFieldJSONSchemaExtra(BaseModel):
+ """
+ Extra attributes to be added to input fields and their OpenAPI schema. Used by the workflow editor
+ during schema parsing and UI rendering.
+ """
+
+ field_kind: FieldKind
+ ui_hidden: bool = False
+ ui_order: Optional[int] = None
+ ui_type: Optional[UIType] = None
+
+ model_config = ConfigDict(
+ validate_assignment=True,
+ json_schema_serialization_defaults_required=True,
+ use_enum_values=True,
+ )
+
+
+def migrate_model_ui_type(ui_type: UIType | str, json_schema_extra: dict[str, Any]) -> bool:
+ """Migrate deprecated model-specifier ui_type values to new-style ui_model_[base|type|variant|format] in json_schema_extra."""
+ if not isinstance(ui_type, UIType):
+ ui_type = UIType(ui_type)
+
+ ui_model_type: list[ModelType] | None = None
+ ui_model_base: list[BaseModelType] | None = None
+ ui_model_format: list[ModelFormat] | None = None
+ ui_model_variant: list[ClipVariantType | ModelVariantType] | None = None
+
+ match ui_type:
+ case UIType.MainModel:
+ ui_model_base = [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
+ ui_model_type = [ModelType.Main]
+ case UIType.CogView4MainModel:
+ ui_model_base = [BaseModelType.CogView4]
+ ui_model_type = [ModelType.Main]
+ case UIType.FluxMainModel:
+ ui_model_base = [BaseModelType.Flux]
+ ui_model_type = [ModelType.Main]
+ case UIType.SD3MainModel:
+ ui_model_base = [BaseModelType.StableDiffusion3]
+ ui_model_type = [ModelType.Main]
+ case UIType.SDXLMainModel:
+ ui_model_base = [BaseModelType.StableDiffusionXL]
+ ui_model_type = [ModelType.Main]
+ case UIType.SDXLRefinerModel:
+ ui_model_base = [BaseModelType.StableDiffusionXLRefiner]
+ ui_model_type = [ModelType.Main]
+ case UIType.VAEModel:
+ ui_model_type = [ModelType.VAE]
+ case UIType.FluxVAEModel:
+ ui_model_base = [BaseModelType.Flux, BaseModelType.Flux2]
+ ui_model_type = [ModelType.VAE]
+ case UIType.LoRAModel:
+ ui_model_type = [ModelType.LoRA]
+ case UIType.ControlNetModel:
+ ui_model_type = [ModelType.ControlNet]
+ case UIType.IPAdapterModel:
+ ui_model_type = [ModelType.IPAdapter]
+ case UIType.T2IAdapterModel:
+ ui_model_type = [ModelType.T2IAdapter]
+ case UIType.T5EncoderModel:
+ ui_model_type = [ModelType.T5Encoder]
+ case UIType.CLIPEmbedModel:
+ ui_model_type = [ModelType.CLIPEmbed]
+ case UIType.CLIPLEmbedModel:
+ ui_model_type = [ModelType.CLIPEmbed]
+ ui_model_variant = [ClipVariantType.L]
+ case UIType.CLIPGEmbedModel:
+ ui_model_type = [ModelType.CLIPEmbed]
+ ui_model_variant = [ClipVariantType.G]
+ case UIType.SpandrelImageToImageModel:
+ ui_model_type = [ModelType.SpandrelImageToImage]
+ case UIType.ControlLoRAModel:
+ ui_model_type = [ModelType.ControlLoRa]
+ case UIType.SigLipModel:
+ ui_model_type = [ModelType.SigLIP]
+ case UIType.FluxReduxModel:
+ ui_model_type = [ModelType.FluxRedux]
+ case UIType.LlavaOnevisionModel:
+ ui_model_type = [ModelType.LlavaOnevision]
+ case _:
+ pass
+
+ did_migrate = False
+
+ if ui_model_type is not None:
+ json_schema_extra["ui_model_type"] = [m.value for m in ui_model_type]
+ did_migrate = True
+ if ui_model_base is not None:
+ json_schema_extra["ui_model_base"] = [m.value for m in ui_model_base]
+ did_migrate = True
+ if ui_model_format is not None:
+ json_schema_extra["ui_model_format"] = [m.value for m in ui_model_format]
+ did_migrate = True
+ if ui_model_variant is not None:
+ json_schema_extra["ui_model_variant"] = [m.value for m in ui_model_variant]
+ did_migrate = True
+
+ return did_migrate
+
+
+def InputField(
+ # copied from pydantic's Field
+ # TODO: Can we support default_factory?
+ default: Any = _Unset,
+ default_factory: Callable[[], Any] | None = _Unset,
+ title: str | None = _Unset,
+ description: str | None = _Unset,
+ pattern: str | None = _Unset,
+ strict: bool | None = _Unset,
+ gt: float | None = _Unset,
+ ge: float | None = _Unset,
+ lt: float | None = _Unset,
+ le: float | None = _Unset,
+ multiple_of: float | None = _Unset,
+ allow_inf_nan: bool | None = _Unset,
+ max_digits: int | None = _Unset,
+ decimal_places: int | None = _Unset,
+ min_length: int | None = _Unset,
+ max_length: int | None = _Unset,
+ # custom
+ input: Input = Input.Any,
+ ui_type: Optional[UIType] = None,
+ ui_component: Optional[UIComponent] = None,
+ ui_hidden: Optional[bool] = None,
+ ui_order: Optional[int] = None,
+ ui_choice_labels: Optional[dict[str, str]] = None,
+ ui_model_base: Optional[BaseModelType | list[BaseModelType]] = None,
+ ui_model_type: Optional[ModelType | list[ModelType]] = None,
+ ui_model_variant: Optional[ClipVariantType | ModelVariantType | list[ClipVariantType | ModelVariantType]] = None,
+ ui_model_format: Optional[ModelFormat | list[ModelFormat]] = None,
+ ui_model_provider_id: Optional[str | list[str]] = None,
+) -> Any:
+ """
+ Creates an input field for an invocation.
+
+ This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field)
+ that adds a few extra parameters to support graph execution and the node editor UI.
+
+ If the field is a `ModelIdentifierField`, use the `ui_model_[base|type|variant|format]` args to filter the model list
+ in the Workflow Editor. Otherwise, use `ui_type` to provide extra type hints for the UI.
+
+ Don't use both `ui_type` and `ui_model_[base|type|variant|format]` - if both are provided, a warning will be
+ logged and `ui_type` will be ignored.
+
+ Args:
+ input: The kind of input this field requires.
+ - `Input.Direct` means a value must be provided on instantiation.
+ - `Input.Connection` means the value must be provided by a connection.
+ - `Input.Any` means either will do.
+
+ ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
+ to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
+ dropdown in the UI. Use `UIType.Scheduler` to indicate this.
+
+ ui_component: Optionally specifies a specific component to use in the UI. The UI will always render a suitable
+ component, but sometimes you want something different than the default. For example, a `string` field will
+ default to a single-line input, but you may want a multi-line textarea instead. In this case, you could use
+ `UIComponent.Textarea`.
+
+ ui_hidden: Specifies whether or not this field should be hidden in the UI.
+
+ ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
+ rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
+
+ ui_model_base: Specifies the base model architectures to filter the model list by in the Workflow Editor. For
+ example, `ui_model_base=BaseModelType.StableDiffusionXL` will show only SDXL architecture models. This arg is
+ only valid if this Input field is annotated as a `ModelIdentifierField`.
+
+ ui_model_type: Specifies the model type(s) to filter the model list by in the Workflow Editor. For example,
+ `ui_model_type=ModelType.VAE` will show only VAE models. This arg is only valid if this Input field is
+ annotated as a `ModelIdentifierField`.
+
+ ui_model_variant: Specifies the model variant(s) to filter the model list by in the Workflow Editor. For example,
+ `ui_model_variant=ModelVariantType.Inpainting` will show only inpainting models. This arg is only valid if this
+ Input field is annotated as a `ModelIdentifierField`.
+
+ ui_model_format: Specifies the model format(s) to filter the model list by in the Workflow Editor. For example,
+ `ui_model_format=ModelFormat.Diffusers` will show only models in the diffusers format. This arg is only valid
+ if this Input field is annotated as a `ModelIdentifierField`.
+
+ ui_model_provider_id: Specifies the external provider id(s) to filter the model list by in the Workflow Editor.
+ For example, `ui_model_provider_id="openai"` will show only models registered under the OpenAI external provider.
+ This arg is only valid if this Input field is annotated as a `ModelIdentifierField` and the target models are
+ external API models.
+
+ ui_choice_labels: Specifies the labels to use for the choices in an enum field. If omitted, the enum values
+ will be used. This arg is only valid if the field is annotated with as a `Literal`. For example,
+ `Literal["choice1", "choice2", "choice3"]` with `ui_choice_labels={"choice1": "Choice 1", "choice2": "Choice 2",
+ "choice3": "Choice 3"}` will render a dropdown with the labels "Choice 1", "Choice 2" and "Choice 3".
+ """
+
+ json_schema_extra_ = InputFieldJSONSchemaExtra(
+ input=input,
+ field_kind=FieldKind.Input,
+ )
+
+ if ui_component is not None:
+ json_schema_extra_.ui_component = ui_component
+ if ui_hidden is not None:
+ json_schema_extra_.ui_hidden = ui_hidden
+ if ui_order is not None:
+ json_schema_extra_.ui_order = ui_order
+ if ui_choice_labels is not None:
+ json_schema_extra_.ui_choice_labels = ui_choice_labels
+ if ui_model_base is not None:
+ if isinstance(ui_model_base, list):
+ json_schema_extra_.ui_model_base = ui_model_base
+ else:
+ json_schema_extra_.ui_model_base = [ui_model_base]
+ if ui_model_type is not None:
+ if isinstance(ui_model_type, list):
+ json_schema_extra_.ui_model_type = ui_model_type
+ else:
+ json_schema_extra_.ui_model_type = [ui_model_type]
+ if ui_model_variant is not None:
+ if isinstance(ui_model_variant, list):
+ json_schema_extra_.ui_model_variant = ui_model_variant
+ else:
+ json_schema_extra_.ui_model_variant = [ui_model_variant]
+ if ui_model_format is not None:
+ if isinstance(ui_model_format, list):
+ json_schema_extra_.ui_model_format = ui_model_format
+ else:
+ json_schema_extra_.ui_model_format = [ui_model_format]
+ if ui_model_provider_id is not None:
+ if isinstance(ui_model_provider_id, list):
+ json_schema_extra_.ui_model_provider_id = ui_model_provider_id
+ else:
+ json_schema_extra_.ui_model_provider_id = [ui_model_provider_id]
+ if ui_type is not None:
+ json_schema_extra_.ui_type = ui_type
+
+ """
+ There is a conflict between the typing of invocation definitions and the typing of an invocation's
+ `invoke()` function.
+
+ On instantiation of a node, the invocation definition is used to create the python class. At this time,
+ any number of fields may be optional, because they may be provided by connections.
+
+ On calling of `invoke()`, however, those fields may be required.
+
+ 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
+ connection from an ancestor node, which outputs an image.
+
+ This means we want to type the `image` field as optional for the node class definition, but required
+ for the `invoke()` function.
+
+ If we use `typing.Optional` in the node class definition, the field will be typed as optional in the
+ `invoke()` method, and we'll have to do a lot of runtime checks to ensure the field is present - or
+ any static type analysis tools will complain.
+
+ To get around this, in node class definitions, we type all fields correctly for the `invoke()` function,
+ but secretly make them optional in `InputField()`. We also store the original required bool and/or default
+ value. When we call `invoke()`, we use this stored information to do an additional check on the class.
+ """
+
+ if default_factory is not _Unset and default_factory is not None:
+ default = default_factory()
+ logger.warning('"default_factory" is not supported, calling it now to set "default"')
+
+ # These are the args we may wish pass to the pydantic `Field()` function
+ field_args = {
+ "default": default,
+ "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,
+ }
+
+ # We only want to pass the args that were provided, otherwise the `Field()`` function won't work as expected
+ provided_args = {k: v for (k, v) in field_args.items() if v is not PydanticUndefined}
+
+ # Because we are manually making fields optional, we need to store the original required bool for reference later
+ json_schema_extra_.orig_required = default is PydanticUndefined
+
+ # 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:
+ default_ = None if default is PydanticUndefined else default
+ provided_args.update({"default": default_})
+ if default is not PydanticUndefined:
+ # Before invoking, we'll check for the original default value and set it on the field if the field has no value
+ json_schema_extra_.default = default
+ json_schema_extra_.orig_default = default
+ elif default is not PydanticUndefined:
+ default_ = default
+ provided_args.update({"default": default_})
+ json_schema_extra_.orig_default = default_
+
+ return Field(
+ **provided_args,
+ json_schema_extra=json_schema_extra_.model_dump(exclude_unset=True),
+ )
+
+
+def OutputField(
+ # copied from pydantic's Field
+ default: Any = _Unset,
+ title: str | None = _Unset,
+ description: str | None = _Unset,
+ pattern: str | None = _Unset,
+ strict: bool | None = _Unset,
+ gt: float | None = _Unset,
+ ge: float | None = _Unset,
+ lt: float | None = _Unset,
+ le: float | None = _Unset,
+ multiple_of: float | None = _Unset,
+ allow_inf_nan: bool | None = _Unset,
+ max_digits: int | None = _Unset,
+ decimal_places: int | None = _Unset,
+ min_length: int | None = _Unset,
+ max_length: int | None = _Unset,
+ # custom
+ ui_type: Optional[UIType] = None,
+ ui_hidden: bool = False,
+ ui_order: Optional[int] = None,
+) -> Any:
+ """
+ Creates an output field for an invocation output.
+
+ This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization)
+ that adds a few extra parameters to support graph execution and the node editor UI.
+
+ Args:
+ ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
+ to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
+ dropdown in the UI. Use `UIType.Scheduler` to indicate this.
+
+ ui_hidden: Specifies whether or not this field should be hidden in the UI.
+
+ ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
+ rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
+ """
+
+ return Field(
+ default=default,
+ 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,
+ json_schema_extra=OutputFieldJSONSchemaExtra(
+ ui_hidden=ui_hidden,
+ ui_order=ui_order,
+ ui_type=ui_type,
+ field_kind=FieldKind.Output,
+ ).model_dump(exclude_none=True),
+ )
diff --git a/invokeai/app/invocations/flux2_denoise.py b/invokeai/app/invocations/flux2_denoise.py
new file mode 100644
index 00000000000..3b9d3d4ce89
--- /dev/null
+++ b/invokeai/app/invocations/flux2_denoise.py
@@ -0,0 +1,579 @@
+"""Flux2 Klein Denoise Invocation.
+
+Run denoising process with a FLUX.2 Klein transformer model.
+Uses Qwen3 conditioning instead of CLIP+T5.
+"""
+
+from contextlib import ExitStack
+from typing import Callable, Iterator, Optional, Tuple
+
+import torch
+import torchvision.transforms as tv_transforms
+from torchvision.transforms.functional import resize as tv_resize
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ FluxConditioningField,
+ FluxKontextConditioningField,
+ Input,
+ InputField,
+ LatentsField,
+)
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import TransformerField, VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.sampling_utils import clip_timestep_schedule_fractional
+from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_LABELS, FLUX_SCHEDULER_MAP, FLUX_SCHEDULER_NAME_VALUES
+from invokeai.backend.flux2.denoise import denoise
+from invokeai.backend.flux2.ref_image_extension import Flux2RefImageExtension
+from invokeai.backend.flux2.sampling_utils import (
+ compute_empirical_mu,
+ generate_img_ids_flux2,
+ get_noise_flux2,
+ get_schedule_flux2,
+ pack_flux2,
+ unpack_flux2,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.flux_bfl_peft_lora_conversion_utils import (
+ convert_bfl_lora_patch_to_diffusers,
+)
+from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import FLUXConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "flux2_denoise",
+ title="FLUX2 Denoise",
+ tags=["image", "flux", "flux2", "klein", "denoise"],
+ category="latents",
+ version="1.5.0",
+ classification=Classification.Prototype,
+)
+class Flux2DenoiseInvocation(BaseInvocation):
+ """Run denoising process with a FLUX.2 Klein transformer model.
+
+ This node is designed for FLUX.2 Klein models which use Qwen3 as the text encoder.
+ It does not support ControlNet, IP-Adapters, or regional prompting.
+ """
+
+ latents: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.noise,
+ input=Input.Connection,
+ )
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None,
+ description=FieldDescriptions.denoise_mask,
+ input=Input.Connection,
+ )
+ 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,
+ )
+ add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.flux_model,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ positive_text_conditioning: FluxConditioningField = InputField(
+ description=FieldDescriptions.positive_cond,
+ input=Input.Connection,
+ )
+ negative_text_conditioning: Optional[FluxConditioningField] = InputField(
+ default=None,
+ description="Negative conditioning tensor. Can be None if cfg_scale is 1.0.",
+ input=Input.Connection,
+ )
+ guidance: float = InputField(
+ default=4.0,
+ ge=0,
+ le=20,
+ description="Guidance strength for distilled guidance-embedding models. "
+ "Inert for all current FLUX.2 Klein variants (their guidance_embeds weights are absent/zero); "
+ "kept for node-graph compatibility and future guidance-embedded models.",
+ )
+ cfg_scale: float = InputField(
+ default=1.0,
+ description=FieldDescriptions.cfg_scale,
+ title="CFG Scale",
+ )
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ num_steps: int = InputField(
+ default=4,
+ description="Number of diffusion steps. Use 4 for distilled models, 28+ for base models.",
+ )
+ scheduler: FLUX_SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description="Scheduler (sampler) for the denoising process. 'euler' is fast and standard. "
+ "'heun' is 2nd-order (better quality, 2x slower). 'lcm' is optimized for few steps.",
+ ui_choice_labels=FLUX_SCHEDULER_LABELS,
+ )
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+ vae: VAEField = InputField(
+ description="FLUX.2 VAE model (required for BN statistics).",
+ input=Input.Connection,
+ )
+ kontext_conditioning: FluxKontextConditioningField | list[FluxKontextConditioningField] | None = InputField(
+ default=None,
+ description="FLUX Kontext conditioning (reference images for multi-reference image editing).",
+ input=Input.Connection,
+ title="Reference Images",
+ )
+
+ def _get_bn_stats(self, context: InvocationContext) -> Optional[Tuple[torch.Tensor, torch.Tensor]]:
+ """Extract BN statistics from the FLUX.2 VAE.
+
+ The FLUX.2 VAE uses batch normalization on the patchified 128-channel representation.
+ IMPORTANT: BFL FLUX.2 VAE uses affine=False, so there are NO learnable weight/bias.
+
+ BN formula (affine=False): y = (x - mean) / std
+ Inverse: x = y * std + mean
+
+ Returns:
+ Tuple of (bn_mean, bn_std) tensors of shape (128,), or None if BN layer not found.
+ """
+ with context.models.load(self.vae.vae).model_on_device() as (_, vae):
+ # Ensure VAE is in eval mode to prevent BN stats from being updated
+ vae.eval()
+
+ # Try to find the BN layer - it may be at different locations depending on model format
+ bn_layer = None
+ if hasattr(vae, "bn"):
+ bn_layer = vae.bn
+ elif hasattr(vae, "batch_norm"):
+ bn_layer = vae.batch_norm
+ elif hasattr(vae, "encoder") and hasattr(vae.encoder, "bn"):
+ bn_layer = vae.encoder.bn
+
+ if bn_layer is None:
+ return None
+
+ # Verify running statistics are initialized
+ if bn_layer.running_mean is None or bn_layer.running_var is None:
+ return None
+
+ # Get BN running statistics from VAE
+ bn_mean = bn_layer.running_mean.clone() # Shape: (128,)
+ bn_var = bn_layer.running_var.clone() # Shape: (128,)
+ bn_eps = bn_layer.eps if hasattr(bn_layer, "eps") else 1e-4 # BFL uses 1e-4
+ bn_std = torch.sqrt(bn_var + bn_eps)
+
+ return bn_mean, bn_std
+
+ def _bn_normalize(
+ self,
+ x: torch.Tensor,
+ bn_mean: torch.Tensor,
+ bn_std: torch.Tensor,
+ ) -> torch.Tensor:
+ """Apply BN normalization to packed latents.
+
+ BN formula (affine=False): y = (x - mean) / std
+
+ Args:
+ x: Packed latents of shape (B, seq, 128).
+ bn_mean: BN running mean of shape (128,).
+ bn_std: BN running std of shape (128,).
+
+ Returns:
+ Normalized latents of same shape.
+ """
+ # x: (B, seq, 128), params: (128,) -> broadcast over batch and sequence dims
+ bn_mean = bn_mean.to(x.device, x.dtype)
+ bn_std = bn_std.to(x.device, x.dtype)
+ return (x - bn_mean) / bn_std
+
+ def _bn_denormalize(
+ self,
+ x: torch.Tensor,
+ bn_mean: torch.Tensor,
+ bn_std: torch.Tensor,
+ ) -> torch.Tensor:
+ """Apply BN denormalization to packed latents (inverse of normalization).
+
+ Inverse BN (affine=False): x = y * std + mean
+
+ Args:
+ x: Packed latents of shape (B, seq, 128).
+ bn_mean: BN running mean of shape (128,).
+ bn_std: BN running std of shape (128,).
+
+ Returns:
+ Denormalized latents of same shape.
+ """
+ # x: (B, seq, 128), params: (128,) -> broadcast over batch and sequence dims
+ bn_mean = bn_mean.to(x.device, x.dtype)
+ bn_std = bn_std.to(x.device, x.dtype)
+ return x * bn_std + bn_mean
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
+ inference_dtype = torch.bfloat16
+ device = TorchDevice.choose_torch_device()
+
+ # Get BN statistics from VAE for latent denormalization (optional)
+ # BFL FLUX.2 VAE uses affine=False, so only mean/std are needed
+ # Some VAE formats (e.g. diffusers) may not expose BN stats directly
+ bn_stats = self._get_bn_stats(context)
+ bn_mean, bn_std = bn_stats if bn_stats is not None else (None, None)
+
+ # Load the input latents, if provided
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+
+ # Prepare input noise (FLUX.2 uses 32 channels).
+ # If noise will never be consumed, avoid validating/loading it.
+ should_ignore_noise = init_latents is not None and not self.add_noise and self.denoise_mask is None
+ noise: Optional[torch.Tensor]
+ if should_ignore_noise:
+ noise = None
+ b, _c, latent_h, latent_w = init_latents.shape
+ else:
+ noise = self._prepare_noise_tensor(context, inference_dtype, device)
+ b, _c, latent_h, latent_w = noise.shape
+ packed_h = latent_h // 2
+ packed_w = latent_w // 2
+
+ # Load the conditioning data
+ pos_cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
+ assert len(pos_cond_data.conditionings) == 1
+ pos_flux_conditioning = pos_cond_data.conditionings[0]
+ assert isinstance(pos_flux_conditioning, FLUXConditioningInfo)
+ pos_flux_conditioning = pos_flux_conditioning.to(dtype=inference_dtype, device=device)
+
+ # Qwen3 stacked embeddings (stored in t5_embeds field for compatibility)
+ txt = pos_flux_conditioning.t5_embeds
+
+ # Generate text position IDs (4D format for FLUX.2: T, H, W, L)
+ # FLUX.2 uses 4D position coordinates for its rotary position embeddings
+ # IMPORTANT: Position IDs must be int64 (long) dtype
+ # Diffusers uses: T=0, H=0, W=0, L=0..seq_len-1
+ seq_len = txt.shape[1]
+ txt_ids = torch.zeros(1, seq_len, 4, device=device, dtype=torch.long)
+ txt_ids[..., 3] = torch.arange(seq_len, device=device, dtype=torch.long) # L coordinate varies
+
+ # Load negative conditioning if provided
+ neg_txt = None
+ neg_txt_ids = None
+ if self.negative_text_conditioning is not None:
+ neg_cond_data = context.conditioning.load(self.negative_text_conditioning.conditioning_name)
+ assert len(neg_cond_data.conditionings) == 1
+ neg_flux_conditioning = neg_cond_data.conditionings[0]
+ assert isinstance(neg_flux_conditioning, FLUXConditioningInfo)
+ neg_flux_conditioning = neg_flux_conditioning.to(dtype=inference_dtype, device=device)
+ neg_txt = neg_flux_conditioning.t5_embeds
+ # For text tokens: T=0, H=0, W=0, L=0..seq_len-1 (only L varies per token)
+ neg_seq_len = neg_txt.shape[1]
+ neg_txt_ids = torch.zeros(1, neg_seq_len, 4, device=device, dtype=torch.long)
+ neg_txt_ids[..., 3] = torch.arange(neg_seq_len, device=device, dtype=torch.long)
+
+ # Validate transformer config
+ transformer_config = context.models.get_config(self.transformer.transformer)
+ assert transformer_config.base == BaseModelType.Flux2 and transformer_config.type == ModelType.Main
+
+ # Calculate the timestep schedule using FLUX.2 specific schedule
+ # This matches diffusers' Flux2Pipeline implementation
+ # Note: Schedule shifting is handled by the scheduler via mu parameter
+ image_seq_len = packed_h * packed_w
+ timesteps = get_schedule_flux2(
+ num_steps=self.num_steps,
+ image_seq_len=image_seq_len,
+ )
+ # Compute mu for dynamic schedule shifting (used by FlowMatchEulerDiscreteScheduler)
+ mu = compute_empirical_mu(image_seq_len=image_seq_len, num_steps=self.num_steps)
+
+ # Clip the timesteps schedule based on denoising_start and denoising_end
+ timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
+
+ # Prepare input latent image
+ if init_latents is not None:
+ if self.add_noise:
+ assert noise is not None
+ # Noise the init latents using the first timestep from the clipped
+ # InvokeAI schedule.
+ #
+ # Known limitation: if a scheduler later uses a different first
+ # effective timestep/sigma than this precomputed schedule, the
+ # img2img preblend below may not match that scheduler exactly.
+ # This is an existing pipeline limitation and applies to both
+ # seed-generated noise and externally supplied noise.
+ t_0 = timesteps[0]
+ x = t_0 * noise + (1.0 - t_0) * init_latents
+ else:
+ x = init_latents
+ else:
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ assert noise is not None
+ x = noise
+
+ # If len(timesteps) == 1, then short-circuit
+ if len(timesteps) <= 1:
+ return x
+
+ # Generate image position IDs (FLUX.2 uses 4D coordinates)
+ # Position IDs use int64 dtype like diffusers
+ img_ids = generate_img_ids_flux2(h=latent_h, w=latent_w, batch_size=b, device=device)
+
+ # Prepare inpaint mask
+ inpaint_mask = self._prep_inpaint_mask(context, x)
+
+ # Pack all latent tensors
+ init_latents_packed = pack_flux2(init_latents) if init_latents is not None else None
+ inpaint_mask_packed = pack_flux2(inpaint_mask) if inpaint_mask is not None else None
+ noise_packed = pack_flux2(noise) if noise is not None else None
+ x = pack_flux2(x)
+
+ # BN normalization for img2img/inpainting:
+ # - The init_latents from VAE encode are NOT BN-normalized
+ # - The transformer operates in BN-normalized space
+ # - We must normalize x, init_latents, AND noise for InpaintExtension
+ # - Output MUST be denormalized after denoising before VAE decode
+ #
+ # This ensures that:
+ # 1. x starts in the correct normalized space for the transformer
+ # 2. When InpaintExtension merges intermediate_latents with noised_init_latents,
+ # both are in the same scale/space (noise and init_latents must be in same space
+ # for the linear interpolation: noised = noise * t + init * (1-t))
+ if bn_mean is not None and bn_std is not None:
+ if init_latents_packed is not None:
+ init_latents_packed = self._bn_normalize(init_latents_packed, bn_mean, bn_std)
+ # Also normalize noise for InpaintExtension - it's used to compute
+ # noised_init_latents = noise * t + init_latents * (1-t)
+ # Both operands must be in the same normalized space
+ if noise_packed is not None:
+ noise_packed = self._bn_normalize(noise_packed, bn_mean, bn_std)
+ # For img2img/inpainting, x is computed from init_latents and must also be normalized
+ # For txt2img, x is pure noise (already N(0,1)) - normalizing it would be incorrect
+ # We detect img2img by checking if init_latents was provided
+ if init_latents is not None:
+ x = self._bn_normalize(x, bn_mean, bn_std)
+
+ # Verify packed dimensions
+ assert packed_h * packed_w == x.shape[1]
+
+ # Prepare inpaint extension
+ inpaint_extension: Optional[RectifiedFlowInpaintExtension] = None
+ if inpaint_mask_packed is not None:
+ assert init_latents_packed is not None
+ assert noise_packed is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents_packed,
+ inpaint_mask=inpaint_mask_packed,
+ noise=noise_packed,
+ )
+
+ # Prepare CFG scale list
+ num_steps = len(timesteps) - 1
+ cfg_scale_list = [self.cfg_scale] * num_steps
+
+ # Check if we're doing inpainting (have a mask or a clipped schedule)
+ is_inpainting = self.denoise_mask is not None or self.denoising_start > 1e-5
+
+ # Create scheduler with FLUX.2 Klein configuration
+ # For inpainting/img2img, use manual Euler stepping to preserve the exact
+ # clipped timestep schedule used for the initial latent/noise preblend.
+ # For txt2img, use the scheduler with dynamic shifting for optimal results.
+ #
+ # This split is intentional. Reusing a scheduler for img2img here can
+ # change the first effective timestep/sigma and break parity with the
+ # preblend computed above.
+ scheduler = None
+ if self.scheduler in FLUX_SCHEDULER_MAP and not is_inpainting:
+ # Only use scheduler for txt2img - use manual Euler for inpainting to preserve exact timesteps
+ scheduler_class = FLUX_SCHEDULER_MAP[self.scheduler]
+ # FlowMatchHeunDiscreteScheduler only supports num_train_timesteps and shift parameters
+ # FlowMatchEulerDiscreteScheduler and FlowMatchLCMScheduler support dynamic shifting
+ if self.scheduler == "heun":
+ scheduler = scheduler_class(
+ num_train_timesteps=1000,
+ shift=3.0,
+ )
+ else:
+ scheduler = scheduler_class(
+ num_train_timesteps=1000,
+ shift=3.0,
+ use_dynamic_shifting=True,
+ base_shift=0.5,
+ max_shift=1.15,
+ base_image_seq_len=256,
+ max_image_seq_len=4096,
+ time_shift_type="exponential",
+ )
+
+ # Prepare reference image extension for FLUX.2 Klein built-in editing
+ ref_image_extension = None
+ if self.kontext_conditioning:
+ ref_image_extension = Flux2RefImageExtension(
+ context=context,
+ ref_image_conditioning=self.kontext_conditioning
+ if isinstance(self.kontext_conditioning, list)
+ else [self.kontext_conditioning],
+ vae_field=self.vae,
+ device=device,
+ dtype=inference_dtype,
+ bn_mean=bn_mean,
+ bn_std=bn_std,
+ )
+
+ with ExitStack() as exit_stack:
+ # Load the transformer model
+ (cached_weights, transformer) = exit_stack.enter_context(
+ context.models.load(self.transformer.transformer).model_on_device()
+ )
+ config = transformer_config
+
+ # Determine if the model is quantized
+ if config.format in [ModelFormat.Diffusers]:
+ model_is_quantized = False
+ elif config.format in [
+ ModelFormat.BnbQuantizedLlmInt8b,
+ ModelFormat.BnbQuantizednf4b,
+ ModelFormat.GGUFQuantized,
+ ]:
+ model_is_quantized = True
+ else:
+ model_is_quantized = False
+
+ # Apply LoRA models to the transformer
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=transformer,
+ patches=self._lora_iterator(context),
+ prefix=FLUX_LORA_TRANSFORMER_PREFIX,
+ dtype=inference_dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=model_is_quantized,
+ )
+ )
+
+ # Prepare reference image conditioning if provided
+ img_cond_seq = None
+ img_cond_seq_ids = None
+ if ref_image_extension is not None:
+ # Ensure batch sizes match
+ ref_image_extension.ensure_batch_size(x.shape[0])
+ img_cond_seq, img_cond_seq_ids = (
+ ref_image_extension.ref_image_latents,
+ ref_image_extension.ref_image_ids,
+ )
+
+ x = denoise(
+ model=transformer,
+ img=x,
+ img_ids=img_ids,
+ txt=txt,
+ txt_ids=txt_ids,
+ timesteps=timesteps,
+ step_callback=self._build_step_callback(context),
+ guidance=self.guidance,
+ cfg_scale=cfg_scale_list,
+ neg_txt=neg_txt,
+ neg_txt_ids=neg_txt_ids,
+ scheduler=scheduler,
+ mu=mu,
+ inpaint_extension=inpaint_extension,
+ img_cond_seq=img_cond_seq,
+ img_cond_seq_ids=img_cond_seq_ids,
+ )
+
+ # Apply BN denormalization if BN stats are available
+ # The diffusers Flux2KleinPipeline applies: latents = latents * bn_std + bn_mean
+ # This transforms latents from normalized space to VAE's expected input space
+ if bn_mean is not None and bn_std is not None:
+ x = self._bn_denormalize(x, bn_mean, bn_std)
+
+ x = unpack_flux2(x.float(), self.height, self.width)
+ return x
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "FLUX.2", self.width, self.height)
+ return noise
+
+ return get_noise_flux2(
+ num_samples=1,
+ height=self.height,
+ width=self.width,
+ device=device,
+ dtype=inference_dtype,
+ seed=self.seed,
+ )
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> Optional[torch.Tensor]:
+ """Prepare the inpaint mask."""
+ if self.denoise_mask is None:
+ return None
+
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask.expand_as(latents)
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply.
+
+ Converts BFL-format LoRA keys to diffusers format if needed, since FLUX.2 Klein
+ uses Flux2Transformer2DModel (diffusers naming) but LoRAs may have been loaded
+ with BFL naming (e.g. when a Klein 4B LoRA is misidentified as FLUX.1).
+ """
+ for lora in self.transformer.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ converted = convert_bfl_lora_patch_to_diffusers(lora_info.model)
+ yield (converted, lora.weight)
+ del lora_info
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ """Build a callback for step progress updates."""
+
+ def step_callback(state: PipelineIntermediateState) -> None:
+ latents = state.latents.float()
+ state.latents = unpack_flux2(latents, self.height, self.width).squeeze()
+ context.util.flux2_step_callback(state)
+
+ return step_callback
diff --git a/invokeai/app/invocations/flux2_klein_lora_loader.py b/invokeai/app/invocations/flux2_klein_lora_loader.py
new file mode 100644
index 00000000000..b7d55b6b134
--- /dev/null
+++ b/invokeai/app/invocations/flux2_klein_lora_loader.py
@@ -0,0 +1,182 @@
+"""FLUX.2 Klein LoRA Loader Invocation.
+
+Applies LoRA models to a FLUX.2 Klein transformer and/or Qwen3 text encoder.
+Unlike standard FLUX which uses CLIP+T5, Klein uses only Qwen3 for text encoding.
+"""
+
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, Qwen3EncoderField, TransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("flux2_klein_lora_loader_output")
+class Flux2KleinLoRALoaderOutput(BaseInvocationOutput):
+ """FLUX.2 Klein LoRA Loader Output"""
+
+ transformer: Optional[TransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="Transformer"
+ )
+ qwen3_encoder: Optional[Qwen3EncoderField] = OutputField(
+ default=None, description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder"
+ )
+
+
+@invocation(
+ "flux2_klein_lora_loader",
+ title="Apply LoRA - Flux2 Klein",
+ tags=["lora", "model", "flux", "klein", "flux2"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class Flux2KleinLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA model to a FLUX.2 Klein transformer and/or Qwen3 text encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.Flux2,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ transformer: TransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> Flux2KleinLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ # Warn if LoRA variant doesn't match transformer variant
+ lora_config = context.models.get_config(lora_key)
+ lora_variant = getattr(lora_config, "variant", None)
+ if lora_variant and self.transformer is not None:
+ transformer_config = context.models.get_config(self.transformer.transformer.key)
+ transformer_variant = getattr(transformer_config, "variant", None)
+ if transformer_variant and lora_variant != transformer_variant:
+ context.logger.warning(
+ f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
+ f"but transformer is {transformer_variant.value}. This may cause shape errors."
+ )
+
+ # Check for existing LoRAs with the same key.
+ if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
+ if self.qwen3_encoder and any(lora.lora.key == lora_key for lora in self.qwen3_encoder.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to Qwen3 encoder.')
+
+ output = Flux2KleinLoRALoaderOutput()
+
+ # Attach LoRA layers to the models.
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ output.transformer.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+ output.qwen3_encoder.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "flux2_klein_lora_collection_loader",
+ title="Apply LoRA Collection - Flux2 Klein",
+ tags=["lora", "model", "flux", "klein", "flux2"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class Flux2KleinLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to a FLUX.2 Klein transformer and/or Qwen3 text encoder."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+
+ transformer: Optional[TransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> Flux2KleinLoRALoaderOutput:
+ output = Flux2KleinLoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ assert lora.lora.base in (BaseModelType.Flux, BaseModelType.Flux2)
+
+ # Warn if LoRA variant doesn't match transformer variant
+ lora_config = context.models.get_config(lora.lora.key)
+ lora_variant = getattr(lora_config, "variant", None)
+ if lora_variant and self.transformer is not None:
+ transformer_config = context.models.get_config(self.transformer.transformer.key)
+ transformer_variant = getattr(transformer_config, "variant", None)
+ if transformer_variant and lora_variant != transformer_variant:
+ context.logger.warning(
+ f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
+ f"but transformer is {transformer_variant.value}. This may cause shape errors."
+ )
+
+ added_loras.append(lora.lora.key)
+
+ if self.transformer is not None and output.transformer is not None:
+ output.transformer.loras.append(lora)
+
+ if self.qwen3_encoder is not None and output.qwen3_encoder is not None:
+ output.qwen3_encoder.loras.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/flux2_klein_model_loader.py b/invokeai/app/invocations/flux2_klein_model_loader.py
new file mode 100644
index 00000000000..2091fd380d7
--- /dev/null
+++ b/invokeai/app/invocations/flux2_klein_model_loader.py
@@ -0,0 +1,222 @@
+"""Flux2 Klein Model Loader Invocation.
+
+Loads a Flux2 Klein model with its Qwen3 text encoder and VAE.
+Unlike standard FLUX which uses CLIP+T5, Klein uses only Qwen3.
+"""
+
+from typing import Literal, Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ ModelIdentifierField,
+ Qwen3EncoderField,
+ TransformerField,
+ VAEField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ Flux2VariantType,
+ ModelFormat,
+ ModelType,
+ Qwen3VariantType,
+ SubModelType,
+)
+
+
+@invocation_output("flux2_klein_model_loader_output")
+class Flux2KleinModelLoaderOutput(BaseInvocationOutput):
+ """Flux2 Klein model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ qwen3_encoder: Qwen3EncoderField = OutputField(description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+ max_seq_len: Literal[256, 512] = OutputField(
+ description="The max sequence length for the Qwen3 encoder.",
+ title="Max Seq Length",
+ )
+
+
+@invocation(
+ "flux2_klein_model_loader",
+ title="Main Model - Flux2 Klein",
+ tags=["model", "flux", "klein", "qwen3"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class Flux2KleinModelLoaderInvocation(BaseInvocation):
+ """Loads a Flux2 Klein model, outputting its submodels.
+
+ Flux2 Klein uses Qwen3 as the text encoder instead of CLIP+T5.
+ It uses a 32-channel VAE (AutoencoderKLFlux2) instead of the 16-channel FLUX.1 VAE.
+
+ When using a Diffusers format model, both VAE and Qwen3 encoder are extracted
+ automatically from the main model. You can override with standalone models:
+ - Transformer: Always from Flux2 Klein main model
+ - VAE: From main model (Diffusers) or standalone VAE
+ - Qwen3 Encoder: From main model (Diffusers) or standalone Qwen3 model
+ """
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.flux_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Flux2,
+ ui_model_type=ModelType.Main,
+ title="Transformer",
+ )
+
+ vae_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone VAE model. Flux2 Klein uses the same VAE as FLUX (16-channel). "
+ "If not provided, VAE will be loaded from the Qwen3 Source model.",
+ input=Input.Direct,
+ ui_model_base=[BaseModelType.Flux, BaseModelType.Flux2],
+ ui_model_type=ModelType.VAE,
+ title="VAE",
+ )
+
+ qwen3_encoder_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Qwen3 Encoder model. "
+ "If not provided, encoder will be loaded from the Qwen3 Source model.",
+ input=Input.Direct,
+ ui_model_type=ModelType.Qwen3Encoder,
+ title="Qwen3 Encoder",
+ )
+
+ qwen3_source_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Diffusers Flux2 Klein model to extract VAE and/or Qwen3 encoder from. "
+ "Use this if you don't have separate VAE/Qwen3 models. "
+ "Ignored if both VAE and Qwen3 Encoder are provided separately.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Flux2,
+ ui_model_type=ModelType.Main,
+ ui_model_format=ModelFormat.Diffusers,
+ title="Qwen3 Source (Diffusers)",
+ )
+
+ max_seq_len: Literal[256, 512] = InputField(
+ default=512,
+ description="Max sequence length for the Qwen3 encoder.",
+ title="Max Seq Length",
+ )
+
+ def invoke(self, context: InvocationContext) -> Flux2KleinModelLoaderOutput:
+ # Transformer always comes from the main model
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+
+ # Check if main model is Diffusers format (can extract VAE directly)
+ main_config = context.models.get_config(self.model)
+ main_is_diffusers = main_config.format == ModelFormat.Diffusers
+
+ # Determine VAE source
+ # IMPORTANT: FLUX.2 Klein uses a 32-channel VAE (AutoencoderKLFlux2), not the 16-channel FLUX.1 VAE.
+ # The VAE should come from the FLUX.2 Klein Diffusers model, not a separate FLUX VAE.
+ if self.vae_model is not None:
+ # Use standalone VAE (user explicitly selected one)
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif main_is_diffusers:
+ # Extract VAE from main model (recommended for FLUX.2)
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif self.qwen3_source_model is not None:
+ # Extract from Qwen3 source Diffusers model
+ self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
+ vae = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ else:
+ raise ValueError(
+ "No VAE source provided. Standalone safetensors/GGUF models require a separate VAE. "
+ "Options:\n"
+ " 1. Set 'VAE' to a standalone FLUX VAE model\n"
+ " 2. Set 'Qwen3 Source' to a Diffusers Flux2 Klein model to extract the VAE from"
+ )
+
+ # Determine Qwen3 Encoder source
+ if self.qwen3_encoder_model is not None:
+ # Use standalone Qwen3 Encoder - validate it matches the FLUX.2 Klein variant
+ self._validate_qwen3_encoder_variant(context, main_config)
+ qwen3_tokenizer = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif main_is_diffusers:
+ # Extract from main model (recommended for FLUX.2 Klein)
+ qwen3_tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif self.qwen3_source_model is not None:
+ # Extract from separate Diffusers model
+ self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
+ qwen3_tokenizer = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ else:
+ raise ValueError(
+ "No Qwen3 Encoder source provided. Standalone safetensors/GGUF models require a separate text encoder. "
+ "Options:\n"
+ " 1. Set 'Qwen3 Encoder' to a standalone Qwen3 text encoder model "
+ "(Klein 4B needs Qwen3 4B, Klein 9B needs Qwen3 8B)\n"
+ " 2. Set 'Qwen3 Source' to a Diffusers Flux2 Klein model to extract the encoder from"
+ )
+
+ return Flux2KleinModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ qwen3_encoder=Qwen3EncoderField(tokenizer=qwen3_tokenizer, text_encoder=qwen3_encoder),
+ vae=VAEField(vae=vae),
+ max_seq_len=self.max_seq_len,
+ )
+
+ def _validate_diffusers_format(
+ self, context: InvocationContext, model: ModelIdentifierField, model_name: str
+ ) -> None:
+ """Validate that a model is in Diffusers format."""
+ config = context.models.get_config(model)
+ if config.format != ModelFormat.Diffusers:
+ raise ValueError(
+ f"The {model_name} model must be a Diffusers format model. "
+ f"The selected model '{config.name}' is in {config.format.value} format."
+ )
+
+ def _validate_qwen3_encoder_variant(self, context: InvocationContext, main_config) -> None:
+ """Validate that the standalone Qwen3 encoder variant matches the FLUX.2 Klein variant.
+
+ - FLUX.2 Klein 4B requires Qwen3 4B encoder
+ - FLUX.2 Klein 9B requires Qwen3 8B encoder
+ """
+ if self.qwen3_encoder_model is None:
+ return
+
+ # Get the Qwen3 encoder config
+ qwen3_config = context.models.get_config(self.qwen3_encoder_model)
+
+ # Check if the config has a variant field
+ if not hasattr(qwen3_config, "variant"):
+ # Can't validate, skip
+ return
+
+ qwen3_variant = qwen3_config.variant
+
+ # Get the FLUX.2 Klein variant from the main model config
+ if not hasattr(main_config, "variant"):
+ return
+
+ flux2_variant = main_config.variant
+
+ # Validate the variants match
+ # Klein4B/Klein4BBase requires Qwen3_4B, Klein9B/Klein9BBase requires Qwen3_8B
+ expected_qwen3_variant = None
+ if flux2_variant in (Flux2VariantType.Klein4B, Flux2VariantType.Klein4BBase):
+ expected_qwen3_variant = Qwen3VariantType.Qwen3_4B
+ elif flux2_variant in (Flux2VariantType.Klein9B, Flux2VariantType.Klein9BBase):
+ expected_qwen3_variant = Qwen3VariantType.Qwen3_8B
+
+ if expected_qwen3_variant is not None and qwen3_variant != expected_qwen3_variant:
+ raise ValueError(
+ f"Qwen3 encoder variant mismatch: FLUX.2 Klein {flux2_variant.value} requires "
+ f"{expected_qwen3_variant.value} encoder, but {qwen3_variant.value} was selected. "
+ f"Please select a matching Qwen3 encoder or use a Diffusers format model which includes the correct encoder."
+ )
diff --git a/invokeai/app/invocations/flux2_klein_text_encoder.py b/invokeai/app/invocations/flux2_klein_text_encoder.py
new file mode 100644
index 00000000000..b2728d1d7cc
--- /dev/null
+++ b/invokeai/app/invocations/flux2_klein_text_encoder.py
@@ -0,0 +1,200 @@
+"""Flux2 Klein Text Encoder Invocation.
+
+Flux2 Klein uses Qwen3 as the text encoder instead of CLIP+T5.
+The key difference is that it extracts hidden states from layers (9, 18, 27)
+and stacks them together for richer text representations.
+
+This implementation matches the diffusers Flux2KleinPipeline exactly.
+"""
+
+from contextlib import ExitStack
+from typing import Iterator, Literal, Optional, Tuple
+
+import torch
+from transformers import PreTrainedModel, PreTrainedTokenizerBase
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ FluxConditioningField,
+ Input,
+ InputField,
+ TensorField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import Qwen3EncoderField
+from invokeai.app.invocations.primitives import FluxConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_T5_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, FLUXConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+# FLUX.2 Klein extracts hidden states from these specific layers
+# Matching diffusers Flux2KleinPipeline: (9, 18, 27)
+# hidden_states[0] is embedding layer, so layer N is at index N
+KLEIN_EXTRACTION_LAYERS = (9, 18, 27)
+
+# Default max sequence length for Klein models
+KLEIN_MAX_SEQ_LEN = 512
+
+
+@invocation(
+ "flux2_klein_text_encoder",
+ title="Prompt - Flux2 Klein",
+ tags=["prompt", "conditioning", "flux", "klein", "qwen3"],
+ category="prompt",
+ version="1.1.1",
+ classification=Classification.Prototype,
+)
+class Flux2KleinTextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for Flux2 Klein image generation.
+
+ Flux2 Klein uses Qwen3 as the text encoder, extracting hidden states from
+ layers (9, 18, 27) and stacking them for richer text representations.
+ This matches the diffusers Flux2KleinPipeline implementation exactly.
+ """
+
+ prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
+ qwen3_encoder: Qwen3EncoderField = InputField(
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+ max_seq_len: Literal[256, 512] = InputField(
+ default=512,
+ description="Max sequence length for the Qwen3 encoder.",
+ )
+ mask: Optional[TensorField] = InputField(
+ default=None,
+ description="A mask defining the region that this conditioning prompt applies to.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
+ # Open the exitstack here to lock models for the duration of the node
+ with ExitStack() as exit_stack:
+ # Pass the locked stack down to the helper function
+ qwen3_embeds, pooled_embeds = self._encode_prompt(context, exit_stack)
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[FLUXConditioningInfo(clip_embeds=pooled_embeds, t5_embeds=qwen3_embeds)]
+ )
+
+ # The models are still locked while we save the data
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return FluxConditioningOutput(
+ conditioning=FluxConditioningField(conditioning_name=conditioning_name, mask=self.mask)
+ )
+
+ def _encode_prompt(self, context: InvocationContext, exit_stack: ExitStack) -> Tuple[torch.Tensor, torch.Tensor]:
+ prompt = self.prompt
+
+ # Reordered loading to prevent the annoying cache drop issue
+ # This prevents it from being evicted while we look up the tokenizer
+ text_encoder_info = context.models.load(self.qwen3_encoder.text_encoder)
+ (cached_weights, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device())
+
+ # Now it is safe to load and lock the tokenizer
+ tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer)
+ (_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device())
+
+ repaired_tensors = text_encoder_info.repair_required_tensors_on_device()
+ device = get_effective_device(text_encoder)
+ if repaired_tensors > 0:
+ context.logger.warning(
+ f"Recovered {repaired_tensors} required Qwen3 tensor(s) onto {device} after a partial device mismatch."
+ )
+
+ # Apply LoRA models
+ lora_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=text_encoder,
+ patches=self._lora_iterator(context),
+ prefix=FLUX_LORA_T5_PREFIX,
+ dtype=lora_dtype,
+ cached_weights=cached_weights,
+ )
+ )
+
+ context.util.signal_progress("Running Qwen3 text encoder (Klein)")
+
+ if not isinstance(text_encoder, PreTrainedModel):
+ raise TypeError(
+ f"Expected PreTrainedModel for text encoder, got {type(text_encoder).__name__}. "
+ "The Qwen3 encoder model may be corrupted or incompatible."
+ )
+ if not isinstance(tokenizer, PreTrainedTokenizerBase):
+ raise TypeError(
+ f"Expected PreTrainedTokenizerBase for tokenizer, got {type(tokenizer).__name__}. "
+ "The Qwen3 tokenizer may be corrupted or incompatible."
+ )
+
+ messages = [{"role": "user", "content": prompt}]
+
+ text: str = tokenizer.apply_chat_template( # type: ignore[assignment]
+ messages,
+ tokenize=False,
+ add_generation_prompt=True,
+ enable_thinking=False,
+ )
+
+ inputs = tokenizer(
+ text,
+ return_tensors="pt",
+ padding="max_length",
+ truncation=True,
+ max_length=self.max_seq_len,
+ )
+
+ input_ids = inputs["input_ids"].to(device)
+ attention_mask = inputs["attention_mask"].to(device)
+
+ # Forward pass through the model
+ outputs = text_encoder(
+ input_ids=input_ids,
+ attention_mask=attention_mask,
+ output_hidden_states=True,
+ use_cache=False,
+ )
+ if not hasattr(outputs, "hidden_states") or outputs.hidden_states is None:
+ raise RuntimeError(
+ "Text encoder did not return hidden_states. "
+ "Ensure output_hidden_states=True is supported by this model."
+ )
+ num_hidden_layers = len(outputs.hidden_states)
+
+ hidden_states_list = []
+ for layer_idx in KLEIN_EXTRACTION_LAYERS:
+ if layer_idx >= num_hidden_layers:
+ layer_idx = num_hidden_layers - 1
+ hidden_states_list.append(outputs.hidden_states[layer_idx])
+
+ out = torch.stack(hidden_states_list, dim=1)
+ out = out.to(dtype=text_encoder.dtype, device=device)
+
+ batch_size, num_channels, seq_len, hidden_dim = out.shape
+ prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
+
+ last_hidden_state = outputs.hidden_states[-1]
+ expanded_mask = attention_mask.unsqueeze(-1).expand_as(last_hidden_state).float()
+ sum_embeds = (last_hidden_state * expanded_mask).sum(dim=1)
+ num_tokens = expanded_mask.sum(dim=1).clamp(min=1)
+ pooled_embeds = sum_embeds / num_tokens
+
+ return prompt_embeds, pooled_embeds
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the Qwen3 text encoder."""
+ for lora in self.qwen3_encoder.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
+ "The LoRA model may be corrupted or incompatible."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/flux2_vae_decode.py b/invokeai/app/invocations/flux2_vae_decode.py
new file mode 100644
index 00000000000..ecbc7d9cb83
--- /dev/null
+++ b/invokeai/app/invocations/flux2_vae_decode.py
@@ -0,0 +1,92 @@
+"""Flux2 Klein VAE Decode Invocation.
+
+Decodes latents to images using the FLUX.2 32-channel VAE (AutoencoderKLFlux2).
+"""
+
+import torch
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "flux2_vae_decode",
+ title="Latents to Image - FLUX2",
+ tags=["latents", "image", "vae", "l2i", "flux2", "klein"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class Flux2VaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents using FLUX.2 Klein's 32-channel VAE."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+
+ def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
+ """Decode latents to image using FLUX.2 VAE.
+
+ Input latents should already be in the correct space after BN denormalization
+ was applied in the denoiser. The VAE expects (B, 32, H, W) format.
+ """
+ with vae_info.model_on_device() as (_, vae):
+ vae_dtype = next(iter(vae.parameters())).dtype
+ device = TorchDevice.choose_torch_device()
+ latents = latents.to(device=device, dtype=vae_dtype)
+
+ # Decode using diffusers API
+ decoded = vae.decode(latents, return_dict=False)[0]
+
+ # Convert from [-1, 1] to [0, 1] then to [0, 255] PIL image
+ img = (decoded / 2 + 0.5).clamp(0, 1)
+ img = rearrange(img[0], "c h w -> h w c")
+ img_np = (img * 255).byte().cpu().numpy()
+ # Explicitly create RGB image (not grayscale)
+ img_pil = Image.fromarray(img_np, mode="RGB")
+ return img_pil
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ # Log latent statistics for debugging black image issues
+ context.logger.debug(
+ f"FLUX.2 VAE decode input: shape={latents.shape}, "
+ f"min={latents.min().item():.4f}, max={latents.max().item():.4f}, "
+ f"mean={latents.mean().item():.4f}"
+ )
+
+ # Warn if input latents are all zeros or very small (would cause black images)
+ if latents.abs().max() < 1e-6:
+ context.logger.warning(
+ "FLUX.2 VAE decode received near-zero latents! This will cause black images. "
+ "The latent cache may be corrupted - try clearing the cache."
+ )
+
+ vae_info = context.models.load(self.vae.vae)
+ context.util.signal_progress("Running VAE")
+ image = self._vae_decode(vae_info=vae_info, latents=latents)
+
+ TorchDevice.empty_cache()
+ image_dto = context.images.save(image=image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/flux2_vae_encode.py b/invokeai/app/invocations/flux2_vae_encode.py
new file mode 100644
index 00000000000..1b43483a408
--- /dev/null
+++ b/invokeai/app/invocations/flux2_vae_encode.py
@@ -0,0 +1,88 @@
+"""Flux2 Klein VAE Encode Invocation.
+
+Encodes images to latents using the FLUX.2 32-channel VAE (AutoencoderKLFlux2).
+"""
+
+import einops
+import torch
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "flux2_vae_encode",
+ title="Image to Latents - FLUX2",
+ tags=["latents", "image", "vae", "i2l", "flux2", "klein"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class Flux2VaeEncodeInvocation(BaseInvocation):
+ """Encodes an image into latents using FLUX.2 Klein's 32-channel VAE."""
+
+ image: ImageField = InputField(
+ description="The image to encode.",
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+
+ def _vae_encode(self, vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ """Encode image to latents using FLUX.2 VAE.
+
+ The VAE encodes to 32-channel latent space.
+ Output latents shape: (B, 32, H/8, W/8).
+ """
+ with vae_info.model_on_device() as (_, vae):
+ vae_dtype = next(iter(vae.parameters())).dtype
+ device = TorchDevice.choose_torch_device()
+ image_tensor = image_tensor.to(device=device, dtype=vae_dtype)
+
+ # Encode using diffusers API
+ # The VAE.encode() returns a DiagonalGaussianDistribution-like object
+ latent_dist = vae.encode(image_tensor, return_dict=False)[0]
+
+ # Sample from the distribution (or use mode for deterministic output)
+ # Using mode() for deterministic encoding
+ if hasattr(latent_dist, "mode"):
+ latents = latent_dist.mode()
+ elif hasattr(latent_dist, "sample"):
+ # Fall back to sampling if mode is not available
+ generator = torch.Generator(device=device).manual_seed(0)
+ latents = latent_dist.sample(generator=generator)
+ else:
+ # Direct tensor output (some VAE implementations)
+ latents = latent_dist
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ vae_info = context.models.load(self.vae.vae)
+
+ # Convert image to tensor (HWC -> CHW, normalize to [-1, 1])
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ context.util.signal_progress("Running VAE Encode")
+ latents = self._vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/flux_control_lora_loader.py b/invokeai/app/invocations/flux_control_lora_loader.py
new file mode 100644
index 00000000000..25025488667
--- /dev/null
+++ b/invokeai/app/invocations/flux_control_lora_loader.py
@@ -0,0 +1,51 @@
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
+from invokeai.app.invocations.model import ControlLoRAField, ModelIdentifierField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("flux_control_lora_loader_output")
+class FluxControlLoRALoaderOutput(BaseInvocationOutput):
+ """Flux Control LoRA Loader Output"""
+
+ control_lora: ControlLoRAField = OutputField(
+ title="Flux Control LoRA", description="Control LoRAs to apply on model loading", default=None
+ )
+
+
+@invocation(
+ "flux_control_lora_loader",
+ title="Control LoRA - FLUX",
+ tags=["lora", "model", "flux"],
+ category="model",
+ version="1.1.1",
+)
+class FluxControlLoRALoaderInvocation(BaseInvocation):
+ """LoRA model and Image to use with FLUX transformer generation."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.control_lora_model,
+ title="Control LoRA",
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.ControlLoRa,
+ )
+ image: ImageField = InputField(description="The image to encode.")
+ weight: float = InputField(description="The weight of the LoRA.", default=1.0)
+
+ def invoke(self, context: InvocationContext) -> FluxControlLoRALoaderOutput:
+ if not context.models.exists(self.lora.key):
+ raise ValueError(f"Unknown lora: {self.lora.key}!")
+
+ return FluxControlLoRALoaderOutput(
+ control_lora=ControlLoRAField(
+ lora=self.lora,
+ img=self.image,
+ weight=self.weight,
+ )
+ )
diff --git a/invokeai/app/invocations/flux_controlnet.py b/invokeai/app/invocations/flux_controlnet.py
new file mode 100644
index 00000000000..b11d497f31f
--- /dev/null
+++ b/invokeai/app/invocations/flux_controlnet.py
@@ -0,0 +1,100 @@
+from pydantic import BaseModel, Field, field_validator, model_validator
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+class FluxControlNetField(BaseModel):
+ image: ImageField = Field(description="The control image")
+ control_model: ModelIdentifierField = Field(description="The ControlNet model to use")
+ control_weight: float | list[float] = Field(default=1, description="The weight given to the ControlNet")
+ begin_step_percent: float = Field(
+ default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)"
+ )
+ end_step_percent: float = Field(
+ default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
+ )
+ resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
+ instantx_control_mode: int | None = Field(default=-1, description=FieldDescriptions.instantx_control_mode)
+
+ @field_validator("control_weight")
+ @classmethod
+ def validate_control_weight(cls, v: float | list[float]) -> float | list[float]:
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self):
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+
+@invocation_output("flux_controlnet_output")
+class FluxControlNetOutput(BaseInvocationOutput):
+ """FLUX ControlNet info"""
+
+ control: FluxControlNetField = OutputField(description=FieldDescriptions.control)
+
+
+@invocation(
+ "flux_controlnet",
+ title="FLUX ControlNet",
+ tags=["controlnet", "flux"],
+ category="conditioning",
+ version="1.0.0",
+)
+class FluxControlNetInvocation(BaseInvocation):
+ """Collect FLUX ControlNet info to pass to other nodes."""
+
+ image: ImageField = InputField(description="The control image")
+ control_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.controlnet_model,
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.ControlNet,
+ )
+ control_weight: float | list[float] = InputField(
+ default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"
+ )
+ begin_step_percent: float = InputField(
+ default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)"
+ )
+ end_step_percent: float = InputField(
+ default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
+ )
+ resize_mode: CONTROLNET_RESIZE_VALUES = InputField(default="just_resize", description="The resize mode used")
+ # Note: We default to -1 instead of None, because in the workflow editor UI None is not currently supported.
+ instantx_control_mode: int | None = InputField(default=-1, description=FieldDescriptions.instantx_control_mode)
+
+ @field_validator("control_weight")
+ @classmethod
+ def validate_control_weight(cls, v: float | list[float]) -> float | list[float]:
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self):
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+ def invoke(self, context: InvocationContext) -> FluxControlNetOutput:
+ return FluxControlNetOutput(
+ control=FluxControlNetField(
+ image=self.image,
+ control_model=self.control_model,
+ control_weight=self.control_weight,
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ resize_mode=self.resize_mode,
+ instantx_control_mode=self.instantx_control_mode,
+ ),
+ )
diff --git a/invokeai/app/invocations/flux_denoise.py b/invokeai/app/invocations/flux_denoise.py
new file mode 100644
index 00000000000..06147229232
--- /dev/null
+++ b/invokeai/app/invocations/flux_denoise.py
@@ -0,0 +1,1019 @@
+from contextlib import ExitStack
+from typing import Callable, Iterator, Optional, Tuple, Union
+
+import einops
+import numpy as np
+import numpy.typing as npt
+import torch
+import torchvision.transforms as tv_transforms
+from PIL import Image
+from torchvision.transforms.functional import resize as tv_resize
+from transformers import CLIPImageProcessor, CLIPVisionModelWithProjection
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ FluxConditioningField,
+ FluxFillConditioningField,
+ FluxKontextConditioningField,
+ FluxReduxConditioningField,
+ ImageField,
+ Input,
+ InputField,
+ LatentsField,
+)
+from invokeai.app.invocations.flux_controlnet import FluxControlNetField
+from invokeai.app.invocations.flux_vae_encode import FluxVaeEncodeInvocation
+from invokeai.app.invocations.ip_adapter import IPAdapterField
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import ControlLoRAField, LoRAField, TransformerField, VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux
+from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux
+from invokeai.backend.flux.denoise import denoise
+from invokeai.backend.flux.dype.presets import (
+ DYPE_PRESET_LABELS,
+ DYPE_PRESET_OFF,
+ DyPEPreset,
+ get_dype_config_from_preset,
+)
+from invokeai.backend.flux.extensions.dype_extension import DyPEExtension
+from invokeai.backend.flux.extensions.instantx_controlnet_extension import InstantXControlNetExtension
+from invokeai.backend.flux.extensions.kontext_extension import KontextExtension
+from invokeai.backend.flux.extensions.regional_prompting_extension import RegionalPromptingExtension
+from invokeai.backend.flux.extensions.xlabs_controlnet_extension import XLabsControlNetExtension
+from invokeai.backend.flux.extensions.xlabs_ip_adapter_extension import XLabsIPAdapterExtension
+from invokeai.backend.flux.ip_adapter.xlabs_ip_adapter_flux import XlabsIpAdapterFlux
+from invokeai.backend.flux.model import Flux
+from invokeai.backend.flux.sampling_utils import (
+ clip_timestep_schedule_fractional,
+ generate_img_ids,
+ get_noise,
+ get_schedule,
+ pack,
+ unpack,
+)
+from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_LABELS, FLUX_SCHEDULER_MAP, FLUX_SCHEDULER_NAME_VALUES
+from invokeai.backend.flux.text_conditioning import FluxReduxConditioning, FluxTextConditioning
+from invokeai.backend.model_manager.taxonomy import BaseModelType, FluxVariantType, ModelFormat, ModelType
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import FLUXConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "flux_denoise",
+ title="FLUX Denoise",
+ tags=["image", "flux"],
+ category="latents",
+ version="4.6.0",
+)
+class FluxDenoiseInvocation(BaseInvocation):
+ """Run denoising process with a FLUX transformer model."""
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None,
+ description=FieldDescriptions.noise,
+ input=Input.Connection,
+ )
+ # denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None,
+ description=FieldDescriptions.denoise_mask,
+ input=Input.Connection,
+ )
+ 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)
+ add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.flux_model,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ control_lora: Optional[ControlLoRAField] = InputField(
+ description=FieldDescriptions.control_lora_model, input=Input.Connection, title="Control LoRA", default=None
+ )
+ positive_text_conditioning: FluxConditioningField | list[FluxConditioningField] = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_text_conditioning: FluxConditioningField | list[FluxConditioningField] | None = InputField(
+ default=None,
+ description="Negative conditioning tensor. Can be None if cfg_scale is 1.0.",
+ input=Input.Connection,
+ )
+ redux_conditioning: FluxReduxConditioningField | list[FluxReduxConditioningField] | None = InputField(
+ default=None,
+ description="FLUX Redux conditioning tensor.",
+ input=Input.Connection,
+ )
+ fill_conditioning: FluxFillConditioningField | None = InputField(
+ default=None,
+ description="FLUX Fill conditioning.",
+ input=Input.Connection,
+ )
+ cfg_scale: float | list[float] = InputField(default=1.0, description=FieldDescriptions.cfg_scale, title="CFG Scale")
+ cfg_scale_start_step: int = InputField(
+ default=0,
+ title="CFG Scale Start Step",
+ description="Index of the first step to apply cfg_scale. Negative indices count backwards from the "
+ + "the last step (e.g. a value of -1 refers to the final step).",
+ )
+ cfg_scale_end_step: int = InputField(
+ default=-1,
+ title="CFG Scale End Step",
+ description="Index of the last step to apply cfg_scale. Negative indices count backwards from the "
+ + "last step (e.g. a value of -1 refers to the final step).",
+ )
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ num_steps: int = InputField(
+ default=4, description="Number of diffusion steps. Recommended values are schnell: 4, dev: 50."
+ )
+ scheduler: FLUX_SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description="Scheduler (sampler) for the denoising process. 'euler' is fast and standard. "
+ "'heun' is 2nd-order (better quality, 2x slower). 'lcm' is optimized for few steps.",
+ ui_choice_labels=FLUX_SCHEDULER_LABELS,
+ )
+ guidance: float = InputField(
+ default=4.0,
+ description="The guidance strength. Higher values adhere more strictly to the prompt, and will produce less diverse images. FLUX dev only, ignored for schnell.",
+ )
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+ control: FluxControlNetField | list[FluxControlNetField] | None = InputField(
+ default=None, input=Input.Connection, description="ControlNet models."
+ )
+ controlnet_vae: VAEField | None = InputField(
+ default=None,
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+ # This node accepts a images for features like FLUX Fill, ControlNet, and Kontext, but needs to operate on them in
+ # latent space. We'll run the VAE to encode them in this node instead of requiring the user to run the VAE in
+ # upstream nodes.
+
+ ip_adapter: IPAdapterField | list[IPAdapterField] | None = InputField(
+ description=FieldDescriptions.ip_adapter, title="IP-Adapter", default=None, input=Input.Connection
+ )
+
+ kontext_conditioning: FluxKontextConditioningField | list[FluxKontextConditioningField] | None = InputField(
+ default=None,
+ description="FLUX Kontext conditioning (reference image).",
+ input=Input.Connection,
+ )
+
+ # DyPE (Dynamic Position Extrapolation) for high-resolution generation
+ dype_preset: DyPEPreset = InputField(
+ default=DYPE_PRESET_OFF,
+ description=(
+ "DyPE preset for high-resolution generation. 'auto' enables automatically for resolutions > 1536px. "
+ "'area' enables automatically based on image area. '4k' uses optimized settings for 4K output."
+ ),
+ ui_order=100,
+ ui_choice_labels=DYPE_PRESET_LABELS,
+ )
+ dype_scale: Optional[float] = InputField(
+ default=None,
+ ge=0.0,
+ le=8.0,
+ description="DyPE magnitude (λs). Higher values = stronger extrapolation. Only used when dype_preset is not 'off'.",
+ ui_order=101,
+ )
+ dype_exponent: Optional[float] = InputField(
+ default=None,
+ ge=0.0,
+ le=1000.0,
+ description="DyPE decay speed (λt). Controls transition from low to high frequency detail. Only used when dype_preset is not 'off'.",
+ ui_order=102,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _run_diffusion(
+ self,
+ context: InvocationContext,
+ ):
+ inference_dtype = torch.bfloat16
+ device = TorchDevice.choose_torch_device()
+
+ # Load the input latents, if provided.
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+
+ # Prepare input noise.
+ # If noise will never be consumed, avoid validating/loading it.
+ should_ignore_noise = init_latents is not None and not self.add_noise and self.denoise_mask is None
+ noise: Optional[torch.Tensor]
+ if should_ignore_noise:
+ noise = None
+ b, _c, latent_h, latent_w = init_latents.shape
+ else:
+ noise = self._prepare_noise_tensor(context, inference_dtype, device)
+ b, _c, latent_h, latent_w = noise.shape
+ packed_h = latent_h // 2
+ packed_w = latent_w // 2
+
+ # Load the conditioning data.
+ pos_text_conditionings = self._load_text_conditioning(
+ context=context,
+ cond_field=self.positive_text_conditioning,
+ packed_height=packed_h,
+ packed_width=packed_w,
+ dtype=inference_dtype,
+ device=device,
+ )
+ neg_text_conditionings: list[FluxTextConditioning] | None = None
+ if self.negative_text_conditioning is not None:
+ neg_text_conditionings = self._load_text_conditioning(
+ context=context,
+ cond_field=self.negative_text_conditioning,
+ packed_height=packed_h,
+ packed_width=packed_w,
+ dtype=inference_dtype,
+ device=device,
+ )
+ redux_conditionings: list[FluxReduxConditioning] = self._load_redux_conditioning(
+ context=context,
+ redux_cond_field=self.redux_conditioning,
+ packed_height=packed_h,
+ packed_width=packed_w,
+ device=device,
+ dtype=inference_dtype,
+ )
+ pos_regional_prompting_extension = RegionalPromptingExtension.from_text_conditioning(
+ text_conditioning=pos_text_conditionings,
+ redux_conditioning=redux_conditionings,
+ img_seq_len=packed_h * packed_w,
+ )
+ neg_regional_prompting_extension = (
+ RegionalPromptingExtension.from_text_conditioning(
+ text_conditioning=neg_text_conditionings, redux_conditioning=[], img_seq_len=packed_h * packed_w
+ )
+ if neg_text_conditionings
+ else None
+ )
+
+ transformer_config = context.models.get_config(self.transformer.transformer)
+ assert (
+ transformer_config.base in (BaseModelType.Flux, BaseModelType.Flux2)
+ and transformer_config.type is ModelType.Main
+ )
+ # Schnell is only for FLUX.1, FLUX.2 Klein behaves like Dev (with guidance)
+ is_schnell = (
+ transformer_config.base is BaseModelType.Flux and transformer_config.variant is FluxVariantType.Schnell
+ )
+
+ # Calculate the timestep schedule.
+ timesteps = get_schedule(
+ num_steps=self.num_steps,
+ image_seq_len=packed_h * packed_w,
+ shift=not is_schnell,
+ )
+
+ # Create scheduler if not using default euler
+ scheduler = None
+ if self.scheduler in FLUX_SCHEDULER_MAP:
+ scheduler_class = FLUX_SCHEDULER_MAP[self.scheduler]
+ scheduler = scheduler_class(num_train_timesteps=1000)
+
+ # Clip the timesteps schedule based on denoising_start and denoising_end.
+ timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
+
+ # Prepare input latent image.
+ if init_latents is not None:
+ # If init_latents is provided, we are doing image-to-image.
+
+ if is_schnell:
+ context.logger.warning(
+ "Running image-to-image with a FLUX schnell model. This is not recommended. The results are likely "
+ "to be poor. Consider using a FLUX dev model instead."
+ )
+
+ if self.add_noise:
+ assert noise is not None
+ # Noise the orig_latents by the appropriate amount for the first
+ # timestep in InvokeAI's clipped schedule.
+ #
+ # Known limitation: if the selected scheduler later replaces this
+ # schedule with its own first effective timestep/sigma (for example
+ # Heun internal expansion or LCM's scheduler-defined schedule), the
+ # img2img preblend below may not match that scheduler's true first
+ # step exactly. This is an existing pipeline limitation and affects
+ # both internally generated noise and externally supplied noise.
+ t_0 = timesteps[0]
+ x = t_0 * noise + (1.0 - t_0) * init_latents
+ else:
+ x = init_latents
+ else:
+ # init_latents are not provided, so we are not doing image-to-image (i.e. we are starting from pure noise).
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+
+ assert noise is not None
+ x = noise
+
+ # If len(timesteps) == 1, then short-circuit. We are just noising the input latents, but not taking any
+ # denoising steps.
+ if len(timesteps) <= 1:
+ return x
+
+ if is_schnell and self.control_lora:
+ raise ValueError("Control LoRAs cannot be used with FLUX Schnell")
+
+ # Prepare the extra image conditioning tensor (img_cond) for either FLUX structural control or FLUX Fill.
+ img_cond: torch.Tensor | None = None
+ is_flux_fill = transformer_config.variant is FluxVariantType.DevFill
+ if is_flux_fill:
+ img_cond = self._prep_flux_fill_img_cond(context, device=device, dtype=inference_dtype)
+ else:
+ if self.fill_conditioning is not None:
+ raise ValueError("fill_conditioning was provided, but the model is not a FLUX Fill model.")
+
+ if self.control_lora is not None:
+ img_cond = self._prep_structural_control_img_cond(context)
+
+ inpaint_mask = self._prep_inpaint_mask(context, x)
+
+ img_ids = generate_img_ids(h=latent_h, w=latent_w, batch_size=b, device=x.device, dtype=x.dtype)
+
+ # Pack all latent tensors.
+ init_latents = pack(init_latents) if init_latents is not None else None
+ inpaint_mask = pack(inpaint_mask) if inpaint_mask is not None else None
+ noise = pack(noise)
+ x = pack(x)
+
+ # Now that we have 'packed' the latent tensors, verify that we calculated the image_seq_len, packed_h, and
+ # packed_w correctly.
+ assert packed_h * packed_w == x.shape[1]
+
+ # Prepare inpaint extension.
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ assert init_latents is not None
+ assert noise is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents,
+ inpaint_mask=inpaint_mask,
+ noise=noise,
+ )
+
+ # Compute the IP-Adapter image prompt clip embeddings.
+ # We do this before loading other models to minimize peak memory.
+ # TODO(ryand): We should really do this in a separate invocation to benefit from caching.
+ ip_adapter_fields = self._normalize_ip_adapter_fields()
+ pos_image_prompt_clip_embeds, neg_image_prompt_clip_embeds = self._prep_ip_adapter_image_prompt_clip_embeds(
+ ip_adapter_fields, context, device=x.device
+ )
+
+ cfg_scale = self.prep_cfg_scale(
+ cfg_scale=self.cfg_scale,
+ timesteps=timesteps,
+ cfg_scale_start_step=self.cfg_scale_start_step,
+ cfg_scale_end_step=self.cfg_scale_end_step,
+ )
+
+ kontext_extension = None
+ if self.kontext_conditioning:
+ if not self.controlnet_vae:
+ raise ValueError("A VAE (e.g., controlnet_vae) must be provided to use Kontext conditioning.")
+
+ kontext_extension = KontextExtension(
+ context=context,
+ kontext_conditioning=self.kontext_conditioning
+ if isinstance(self.kontext_conditioning, list)
+ else [self.kontext_conditioning],
+ vae_field=self.controlnet_vae,
+ device=device,
+ dtype=inference_dtype,
+ )
+
+ with ExitStack() as exit_stack:
+ # Prepare ControlNet extensions.
+ # Note: We do this before loading the transformer model to minimize peak memory (see implementation).
+ controlnet_extensions = self._prep_controlnet_extensions(
+ context=context,
+ exit_stack=exit_stack,
+ latent_height=latent_h,
+ latent_width=latent_w,
+ dtype=inference_dtype,
+ device=x.device,
+ )
+
+ # Load the transformer model.
+ (cached_weights, transformer) = exit_stack.enter_context(
+ context.models.load(self.transformer.transformer).model_on_device()
+ )
+ assert isinstance(transformer, Flux)
+ config = transformer_config
+ assert config is not None
+
+ # Determine if the model is quantized.
+ # If the model is quantized, then we need to apply the LoRA weights as sidecar layers. This results in
+ # slower inference than direct patching, but is agnostic to the quantization format.
+ if config.format in [ModelFormat.Checkpoint]:
+ model_is_quantized = False
+ elif config.format in [
+ ModelFormat.BnbQuantizedLlmInt8b,
+ ModelFormat.BnbQuantizednf4b,
+ ModelFormat.GGUFQuantized,
+ ]:
+ model_is_quantized = True
+ else:
+ raise ValueError(f"Unsupported model format: {config.format}")
+
+ # Apply LoRA models to the transformer.
+ # Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=transformer,
+ patches=self._lora_iterator(context),
+ prefix=FLUX_LORA_TRANSFORMER_PREFIX,
+ dtype=inference_dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=model_is_quantized,
+ )
+ )
+
+ # Prepare IP-Adapter extensions.
+ pos_ip_adapter_extensions, neg_ip_adapter_extensions = self._prep_ip_adapter_extensions(
+ pos_image_prompt_clip_embeds=pos_image_prompt_clip_embeds,
+ neg_image_prompt_clip_embeds=neg_image_prompt_clip_embeds,
+ ip_adapter_fields=ip_adapter_fields,
+ context=context,
+ exit_stack=exit_stack,
+ dtype=inference_dtype,
+ )
+
+ # Prepare Kontext conditioning if provided
+ img_cond_seq = None
+ img_cond_seq_ids = None
+ if kontext_extension is not None:
+ # Ensure batch sizes match
+ kontext_extension.ensure_batch_size(x.shape[0])
+ img_cond_seq, img_cond_seq_ids = kontext_extension.kontext_latents, kontext_extension.kontext_ids
+
+ # Prepare DyPE extension for high-resolution generation
+ dype_extension: DyPEExtension | None = None
+ dype_config = get_dype_config_from_preset(
+ preset=self.dype_preset,
+ width=self.width,
+ height=self.height,
+ custom_scale=self.dype_scale,
+ custom_exponent=self.dype_exponent,
+ )
+ if dype_config is not None:
+ dype_extension = DyPEExtension(
+ config=dype_config,
+ target_height=self.height,
+ target_width=self.width,
+ )
+ context.logger.info(
+ f"DyPE enabled: resolution={self.width}x{self.height}, preset={self.dype_preset}, "
+ f"scale={dype_config.dype_scale:.2f}, "
+ f"exponent={dype_config.dype_exponent:.2f}, start_sigma={dype_config.dype_start_sigma:.2f}, "
+ f"base_resolution={dype_config.base_resolution}"
+ )
+ else:
+ context.logger.debug(f"DyPE disabled: resolution={self.width}x{self.height}, preset={self.dype_preset}")
+
+ x = denoise(
+ model=transformer,
+ img=x,
+ img_ids=img_ids,
+ pos_regional_prompting_extension=pos_regional_prompting_extension,
+ neg_regional_prompting_extension=neg_regional_prompting_extension,
+ timesteps=timesteps,
+ step_callback=self._build_step_callback(context),
+ guidance=self.guidance,
+ cfg_scale=cfg_scale,
+ inpaint_extension=inpaint_extension,
+ controlnet_extensions=controlnet_extensions,
+ pos_ip_adapter_extensions=pos_ip_adapter_extensions,
+ neg_ip_adapter_extensions=neg_ip_adapter_extensions,
+ img_cond=img_cond,
+ img_cond_seq=img_cond_seq,
+ img_cond_seq_ids=img_cond_seq_ids,
+ dype_extension=dype_extension,
+ scheduler=scheduler,
+ )
+
+ x = unpack(x.float(), self.height, self.width)
+ return x
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "FLUX", self.width, self.height)
+ return noise
+
+ return get_noise(
+ num_samples=1,
+ height=self.height,
+ width=self.width,
+ device=device,
+ dtype=inference_dtype,
+ seed=self.seed,
+ )
+
+ def _load_text_conditioning(
+ self,
+ context: InvocationContext,
+ cond_field: FluxConditioningField | list[FluxConditioningField],
+ packed_height: int,
+ packed_width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> list[FluxTextConditioning]:
+ """Load text conditioning data from a FluxConditioningField or a list of FluxConditioningFields."""
+ # Normalize to a list of FluxConditioningFields.
+ cond_list = [cond_field] if isinstance(cond_field, FluxConditioningField) else cond_field
+
+ text_conditionings: list[FluxTextConditioning] = []
+ for cond_field in cond_list:
+ # Load the text embeddings.
+ cond_data = context.conditioning.load(cond_field.conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ flux_conditioning = cond_data.conditionings[0]
+ assert isinstance(flux_conditioning, FLUXConditioningInfo)
+ flux_conditioning = flux_conditioning.to(dtype=dtype, device=device)
+ t5_embeddings = flux_conditioning.t5_embeds
+ clip_embeddings = flux_conditioning.clip_embeds
+
+ # Load the mask, if provided.
+ mask: Optional[torch.Tensor] = None
+ if cond_field.mask is not None:
+ mask = context.tensors.load(cond_field.mask.tensor_name)
+ mask = mask.to(device=device)
+ mask = RegionalPromptingExtension.preprocess_regional_prompt_mask(
+ mask, packed_height, packed_width, dtype, device
+ )
+
+ text_conditionings.append(FluxTextConditioning(t5_embeddings, clip_embeddings, mask))
+
+ return text_conditionings
+
+ def _load_redux_conditioning(
+ self,
+ context: InvocationContext,
+ redux_cond_field: FluxReduxConditioningField | list[FluxReduxConditioningField] | None,
+ packed_height: int,
+ packed_width: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> list[FluxReduxConditioning]:
+ # Normalize to a list of FluxReduxConditioningFields.
+ if redux_cond_field is None:
+ return []
+
+ redux_cond_list = (
+ [redux_cond_field] if isinstance(redux_cond_field, FluxReduxConditioningField) else redux_cond_field
+ )
+
+ redux_conditionings: list[FluxReduxConditioning] = []
+ for redux_cond_field in redux_cond_list:
+ # Load the Redux conditioning tensor.
+ redux_cond_data = context.tensors.load(redux_cond_field.conditioning.tensor_name)
+ redux_cond_data.to(device=device, dtype=dtype)
+
+ # Load the mask, if provided.
+ mask: Optional[torch.Tensor] = None
+ if redux_cond_field.mask is not None:
+ mask = context.tensors.load(redux_cond_field.mask.tensor_name)
+ mask = mask.to(device=device)
+ mask = RegionalPromptingExtension.preprocess_regional_prompt_mask(
+ mask, packed_height, packed_width, dtype, device
+ )
+
+ redux_conditionings.append(FluxReduxConditioning(redux_embeddings=redux_cond_data, mask=mask))
+
+ return redux_conditionings
+
+ @classmethod
+ def prep_cfg_scale(
+ cls, cfg_scale: float | list[float], timesteps: list[float], cfg_scale_start_step: int, cfg_scale_end_step: int
+ ) -> list[float]:
+ """Prepare the cfg_scale schedule.
+
+ - Clips the cfg_scale schedule based on cfg_scale_start_step and cfg_scale_end_step.
+ - If cfg_scale is a list, then it is assumed to be a schedule and is returned as-is.
+ - If cfg_scale is a scalar, then a linear schedule is created from cfg_scale_start_step to cfg_scale_end_step.
+ """
+ # num_steps is the number of denoising steps, which is one less than the number of timesteps.
+ num_steps = len(timesteps) - 1
+
+ # Normalize cfg_scale to a list if it is a scalar.
+ cfg_scale_list: list[float]
+ if isinstance(cfg_scale, float):
+ cfg_scale_list = [cfg_scale] * num_steps
+ elif isinstance(cfg_scale, list):
+ cfg_scale_list = cfg_scale
+ else:
+ raise ValueError(f"Unsupported cfg_scale type: {type(cfg_scale)}")
+ assert len(cfg_scale_list) == num_steps
+
+ # Handle negative indices for cfg_scale_start_step and cfg_scale_end_step.
+ start_step_index = cfg_scale_start_step
+ if start_step_index < 0:
+ start_step_index = num_steps + start_step_index
+ end_step_index = cfg_scale_end_step
+ if end_step_index < 0:
+ end_step_index = num_steps + end_step_index
+
+ # Validate the start and end step indices.
+ if not (0 <= start_step_index < num_steps):
+ raise ValueError(f"Invalid cfg_scale_start_step. Out of range: {cfg_scale_start_step}.")
+ if not (0 <= end_step_index < num_steps):
+ raise ValueError(f"Invalid cfg_scale_end_step. Out of range: {cfg_scale_end_step}.")
+ if start_step_index > end_step_index:
+ raise ValueError(
+ f"cfg_scale_start_step ({cfg_scale_start_step}) must be before cfg_scale_end_step "
+ + f"({cfg_scale_end_step})."
+ )
+
+ # Set values outside the start and end step indices to 1.0. This is equivalent to disabling cfg_scale for those
+ # steps.
+ clipped_cfg_scale = [1.0] * num_steps
+ clipped_cfg_scale[start_step_index : end_step_index + 1] = cfg_scale_list[start_step_index : end_step_index + 1]
+
+ return clipped_cfg_scale
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ """Prepare the inpaint mask.
+
+ - Loads the mask
+ - Resizes if necessary
+ - Casts to same device/dtype as latents
+ - Expands mask to the same shape as latents so that they line up after 'packing'
+
+ Args:
+ context (InvocationContext): The invocation context, for loading the inpaint mask.
+ latents (torch.Tensor): A latent image tensor. In 'unpacked' format. Used to determine the target shape,
+ device, and dtype for the inpaint mask.
+
+ Returns:
+ torch.Tensor | None: Inpaint mask. Values of 0.0 represent the regions to be fully denoised, and 1.0
+ represent the regions to be preserved.
+ """
+ if self.denoise_mask is None:
+ return None
+
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+
+ # The input denoise_mask contains values in [0, 1], where 0.0 represents the regions to be fully denoised, and
+ # 1.0 represents the regions to be preserved.
+ # We invert the mask so that the regions to be preserved are 0.0 and the regions to be denoised are 1.0.
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+
+ # Expand the inpaint mask to the same shape as `latents` so that when we 'pack' `mask` it lines up with
+ # `latents`.
+ return mask.expand_as(latents)
+
+ def _prep_controlnet_extensions(
+ self,
+ context: InvocationContext,
+ exit_stack: ExitStack,
+ latent_height: int,
+ latent_width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> list[XLabsControlNetExtension | InstantXControlNetExtension]:
+ # Normalize the controlnet input to list[ControlField].
+ controlnets: list[FluxControlNetField]
+ if self.control is None:
+ controlnets = []
+ elif isinstance(self.control, FluxControlNetField):
+ controlnets = [self.control]
+ elif isinstance(self.control, list):
+ controlnets = self.control
+ else:
+ raise ValueError(f"Unsupported controlnet type: {type(self.control)}")
+
+ # TODO(ryand): Add a field to the model config so that we can distinguish between XLabs and InstantX ControlNets
+ # before loading the models. Then make sure that all VAE encoding is done before loading the ControlNets to
+ # minimize peak memory.
+
+ # Calculate the controlnet conditioning tensors.
+ # We do this before loading the ControlNet models because it may require running the VAE, and we are trying to
+ # keep peak memory down.
+ controlnet_conds: list[torch.Tensor] = []
+ for controlnet in controlnets:
+ image = context.images.get_pil(controlnet.image.image_name)
+
+ # HACK(ryand): We have to load the ControlNet model to determine whether the VAE needs to be run. We really
+ # shouldn't have to load the model here. There's a risk that the model will be dropped from the model cache
+ # before we load it into VRAM and thus we'll have to load it again (context:
+ # https://github.com/invoke-ai/InvokeAI/issues/7513).
+ controlnet_model = context.models.load(controlnet.control_model)
+ if isinstance(controlnet_model.model, InstantXControlNetFlux):
+ if self.controlnet_vae is None:
+ raise ValueError("A ControlNet VAE is required when using an InstantX FLUX ControlNet.")
+ vae_info = context.models.load(self.controlnet_vae.vae)
+ controlnet_conds.append(
+ InstantXControlNetExtension.prepare_controlnet_cond(
+ controlnet_image=image,
+ vae_info=vae_info,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ dtype=dtype,
+ device=device,
+ resize_mode=controlnet.resize_mode,
+ )
+ )
+ elif isinstance(controlnet_model.model, XLabsControlNetFlux):
+ controlnet_conds.append(
+ XLabsControlNetExtension.prepare_controlnet_cond(
+ controlnet_image=image,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ dtype=dtype,
+ device=device,
+ resize_mode=controlnet.resize_mode,
+ )
+ )
+
+ # Finally, load the ControlNet models and initialize the ControlNet extensions.
+ controlnet_extensions: list[XLabsControlNetExtension | InstantXControlNetExtension] = []
+ for controlnet, controlnet_cond in zip(controlnets, controlnet_conds, strict=True):
+ model = exit_stack.enter_context(context.models.load(controlnet.control_model))
+
+ if isinstance(model, XLabsControlNetFlux):
+ controlnet_extensions.append(
+ XLabsControlNetExtension(
+ model=model,
+ controlnet_cond=controlnet_cond,
+ weight=controlnet.control_weight,
+ begin_step_percent=controlnet.begin_step_percent,
+ end_step_percent=controlnet.end_step_percent,
+ )
+ )
+ elif isinstance(model, InstantXControlNetFlux):
+ instantx_control_mode: torch.Tensor | None = None
+ if controlnet.instantx_control_mode is not None and controlnet.instantx_control_mode >= 0:
+ instantx_control_mode = torch.tensor(controlnet.instantx_control_mode, dtype=torch.long)
+ instantx_control_mode = instantx_control_mode.reshape([-1, 1])
+
+ controlnet_extensions.append(
+ InstantXControlNetExtension(
+ model=model,
+ controlnet_cond=controlnet_cond,
+ instantx_control_mode=instantx_control_mode,
+ weight=controlnet.control_weight,
+ begin_step_percent=controlnet.begin_step_percent,
+ end_step_percent=controlnet.end_step_percent,
+ )
+ )
+ else:
+ raise ValueError(f"Unsupported ControlNet model type: {type(model)}")
+
+ return controlnet_extensions
+
+ def _prep_structural_control_img_cond(self, context: InvocationContext) -> torch.Tensor | None:
+ if self.control_lora is None:
+ return None
+
+ if not self.controlnet_vae:
+ raise ValueError("controlnet_vae must be set when using a FLUX Control LoRA.")
+
+ # Load the conditioning image and resize it to the target image size.
+ cond_img = context.images.get_pil(self.control_lora.img.image_name)
+ cond_img = cond_img.convert("RGB")
+ cond_img = cond_img.resize((self.width, self.height), Image.Resampling.BICUBIC)
+ cond_img = np.array(cond_img)
+
+ # Normalize the conditioning image to the range [-1, 1].
+ # This normalization is based on the original implementations here:
+ # https://github.com/black-forest-labs/flux/blob/805da8571a0b49b6d4043950bd266a65328c243b/src/flux/modules/image_embedders.py#L34
+ # https://github.com/black-forest-labs/flux/blob/805da8571a0b49b6d4043950bd266a65328c243b/src/flux/modules/image_embedders.py#L60
+ img_cond = torch.from_numpy(cond_img).float() / 127.5 - 1.0
+ img_cond = einops.rearrange(img_cond, "h w c -> 1 c h w")
+
+ vae_info = context.models.load(self.controlnet_vae.vae)
+ img_cond = FluxVaeEncodeInvocation.vae_encode(vae_info=vae_info, image_tensor=img_cond)
+
+ return pack(img_cond)
+
+ def _prep_flux_fill_img_cond(
+ self, context: InvocationContext, device: torch.device, dtype: torch.dtype
+ ) -> torch.Tensor:
+ """Prepare the FLUX Fill conditioning. This method should be called iff the model is a FLUX Fill model.
+
+ This logic is based on:
+ https://github.com/black-forest-labs/flux/blob/716724eb276d94397be99710a0a54d352664e23b/src/flux/sampling.py#L107-L157
+ """
+ # Validate inputs.
+ if self.fill_conditioning is None:
+ raise ValueError("A FLUX Fill model is being used without fill_conditioning.")
+ # TODO(ryand): We should probable rename controlnet_vae. It's used for more than just ControlNets.
+ if self.controlnet_vae is None:
+ raise ValueError("A FLUX Fill model is being used without controlnet_vae.")
+ if self.control_lora is not None:
+ raise ValueError(
+ "A FLUX Fill model is being used, but a control_lora was provided. Control LoRAs are not compatible with FLUX Fill models."
+ )
+
+ # Log input warnings related to FLUX Fill usage.
+ if self.denoise_mask is not None:
+ context.logger.warning(
+ "Both fill_conditioning and a denoise_mask were provided. You probably meant to use one or the other."
+ )
+ if self.guidance < 25.0:
+ context.logger.warning("A guidance value of ~30.0 is recommended for FLUX Fill models.")
+
+ # Load the conditioning image and resize it to the target image size.
+ cond_img = context.images.get_pil(self.fill_conditioning.image.image_name, mode="RGB")
+ cond_img = cond_img.resize((self.width, self.height), Image.Resampling.BICUBIC)
+ cond_img = np.array(cond_img)
+ cond_img = torch.from_numpy(cond_img).float() / 127.5 - 1.0
+ cond_img = einops.rearrange(cond_img, "h w c -> 1 c h w")
+ cond_img = cond_img.to(device=device, dtype=dtype)
+
+ # Load the mask and resize it to the target image size.
+ mask = context.tensors.load(self.fill_conditioning.mask.tensor_name)
+ # We expect mask to be a bool tensor with shape [1, H, W].
+ assert mask.dtype == torch.bool
+ assert mask.dim() == 3
+ assert mask.shape[0] == 1
+ mask = tv_resize(mask, size=[self.height, self.width], interpolation=tv_transforms.InterpolationMode.NEAREST)
+ mask = mask.to(device=device, dtype=dtype)
+ mask = einops.rearrange(mask, "1 h w -> 1 1 h w")
+
+ # Prepare image conditioning.
+ cond_img = cond_img * (1 - mask)
+ vae_info = context.models.load(self.controlnet_vae.vae)
+ cond_img = FluxVaeEncodeInvocation.vae_encode(vae_info=vae_info, image_tensor=cond_img)
+ cond_img = pack(cond_img)
+
+ # Prepare mask conditioning.
+ mask = mask[:, 0, :, :]
+ # Rearrange mask to a 16-channel representation that matches the shape of the VAE-encoded latent space.
+ mask = einops.rearrange(mask, "b (h ph) (w pw) -> b (ph pw) h w", ph=8, pw=8)
+ mask = pack(mask)
+
+ # Merge image and mask conditioning.
+ img_cond = torch.cat((cond_img, mask), dim=-1)
+ return img_cond
+
+ def _normalize_ip_adapter_fields(self) -> list[IPAdapterField]:
+ if self.ip_adapter is None:
+ return []
+ elif isinstance(self.ip_adapter, IPAdapterField):
+ return [self.ip_adapter]
+ elif isinstance(self.ip_adapter, list):
+ return self.ip_adapter
+ else:
+ raise ValueError(f"Unsupported IP-Adapter type: {type(self.ip_adapter)}")
+
+ def _prep_ip_adapter_image_prompt_clip_embeds(
+ self,
+ ip_adapter_fields: list[IPAdapterField],
+ context: InvocationContext,
+ device: torch.device,
+ ) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
+ """Run the IPAdapter CLIPVisionModel, returning image prompt embeddings."""
+ clip_image_processor = CLIPImageProcessor()
+
+ pos_image_prompt_clip_embeds: list[torch.Tensor] = []
+ neg_image_prompt_clip_embeds: list[torch.Tensor] = []
+ for ip_adapter_field in ip_adapter_fields:
+ # `ip_adapter_field.image` could be a list or a single ImageField. Normalize to a list here.
+ ipa_image_fields: list[ImageField]
+ if isinstance(ip_adapter_field.image, ImageField):
+ ipa_image_fields = [ip_adapter_field.image]
+ elif isinstance(ip_adapter_field.image, list):
+ ipa_image_fields = ip_adapter_field.image
+ else:
+ raise ValueError(f"Unsupported IP-Adapter image type: {type(ip_adapter_field.image)}")
+
+ if len(ipa_image_fields) != 1:
+ raise ValueError(
+ f"FLUX IP-Adapter only supports a single image prompt (received {len(ipa_image_fields)})."
+ )
+
+ ipa_images = [context.images.get_pil(image.image_name, mode="RGB") for image in ipa_image_fields]
+
+ pos_images: list[npt.NDArray[np.uint8]] = []
+ neg_images: list[npt.NDArray[np.uint8]] = []
+ for ipa_image in ipa_images:
+ assert ipa_image.mode == "RGB"
+ pos_image = np.array(ipa_image)
+ # We use a black image as the negative image prompt for parity with
+ # https://github.com/XLabs-AI/x-flux-comfyui/blob/45c834727dd2141aebc505ae4b01f193a8414e38/nodes.py#L592-L593
+ # An alternative scheme would be to apply zeros_like() after calling the clip_image_processor.
+ neg_image = np.zeros_like(pos_image)
+ pos_images.append(pos_image)
+ neg_images.append(neg_image)
+
+ with context.models.load(ip_adapter_field.image_encoder_model) as image_encoder_model:
+ assert isinstance(image_encoder_model, CLIPVisionModelWithProjection)
+
+ clip_image: torch.Tensor = clip_image_processor(images=pos_images, return_tensors="pt").pixel_values
+ clip_image = clip_image.to(device=device, dtype=image_encoder_model.dtype)
+ pos_clip_image_embeds = image_encoder_model(clip_image).image_embeds
+
+ clip_image = clip_image_processor(images=neg_images, return_tensors="pt").pixel_values
+ clip_image = clip_image.to(device=device, dtype=image_encoder_model.dtype)
+ neg_clip_image_embeds = image_encoder_model(clip_image).image_embeds
+
+ pos_image_prompt_clip_embeds.append(pos_clip_image_embeds)
+ neg_image_prompt_clip_embeds.append(neg_clip_image_embeds)
+
+ return pos_image_prompt_clip_embeds, neg_image_prompt_clip_embeds
+
+ def _prep_ip_adapter_extensions(
+ self,
+ ip_adapter_fields: list[IPAdapterField],
+ pos_image_prompt_clip_embeds: list[torch.Tensor],
+ neg_image_prompt_clip_embeds: list[torch.Tensor],
+ context: InvocationContext,
+ exit_stack: ExitStack,
+ dtype: torch.dtype,
+ ) -> tuple[list[XLabsIPAdapterExtension], list[XLabsIPAdapterExtension]]:
+ pos_ip_adapter_extensions: list[XLabsIPAdapterExtension] = []
+ neg_ip_adapter_extensions: list[XLabsIPAdapterExtension] = []
+ for ip_adapter_field, pos_image_prompt_clip_embed, neg_image_prompt_clip_embed in zip(
+ ip_adapter_fields, pos_image_prompt_clip_embeds, neg_image_prompt_clip_embeds, strict=True
+ ):
+ ip_adapter_model = exit_stack.enter_context(context.models.load(ip_adapter_field.ip_adapter_model))
+ assert isinstance(ip_adapter_model, XlabsIpAdapterFlux)
+ ip_adapter_model = ip_adapter_model.to(dtype=dtype)
+ if ip_adapter_field.mask is not None:
+ raise ValueError("IP-Adapter masks are not yet supported in Flux.")
+ ip_adapter_extension = XLabsIPAdapterExtension(
+ model=ip_adapter_model,
+ image_prompt_clip_embed=pos_image_prompt_clip_embed,
+ weight=ip_adapter_field.weight,
+ begin_step_percent=ip_adapter_field.begin_step_percent,
+ end_step_percent=ip_adapter_field.end_step_percent,
+ )
+ ip_adapter_extension.run_image_proj(dtype=dtype)
+ pos_ip_adapter_extensions.append(ip_adapter_extension)
+
+ ip_adapter_extension = XLabsIPAdapterExtension(
+ model=ip_adapter_model,
+ image_prompt_clip_embed=neg_image_prompt_clip_embed,
+ weight=ip_adapter_field.weight,
+ begin_step_percent=ip_adapter_field.begin_step_percent,
+ end_step_percent=ip_adapter_field.end_step_percent,
+ )
+ ip_adapter_extension.run_image_proj(dtype=dtype)
+ neg_ip_adapter_extensions.append(ip_adapter_extension)
+
+ return pos_ip_adapter_extensions, neg_ip_adapter_extensions
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ loras: list[Union[LoRAField, ControlLoRAField]] = [*self.transformer.loras]
+ if self.control_lora:
+ # Note: Since FLUX structural control LoRAs modify the shape of some weights, it is important that they are
+ # applied last.
+ loras.append(self.control_lora)
+ for lora in loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ # The denoise function now handles Kontext conditioning correctly,
+ # so we don't need to slice the latents here
+ latents = state.latents.float()
+ state.latents = unpack(latents, self.height, self.width).squeeze()
+ context.util.flux_step_callback(state)
+
+ return step_callback
diff --git a/invokeai/app/invocations/flux_fill.py b/invokeai/app/invocations/flux_fill.py
new file mode 100644
index 00000000000..440f3e5c971
--- /dev/null
+++ b/invokeai/app/invocations/flux_fill.py
@@ -0,0 +1,46 @@
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ FluxFillConditioningField,
+ InputField,
+ OutputField,
+ TensorField,
+)
+from invokeai.app.invocations.primitives import ImageField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("flux_fill_output")
+class FluxFillOutput(BaseInvocationOutput):
+ """The conditioning output of a FLUX Fill invocation."""
+
+ fill_cond: FluxFillConditioningField = OutputField(
+ description=FieldDescriptions.flux_redux_conditioning, title="Conditioning"
+ )
+
+
+@invocation(
+ "flux_fill",
+ title="FLUX Fill Conditioning",
+ tags=["inpaint"],
+ category="conditioning",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class FluxFillInvocation(BaseInvocation):
+ """Prepare the FLUX Fill conditioning data."""
+
+ image: ImageField = InputField(description="The FLUX Fill reference image.")
+ mask: TensorField = InputField(
+ description="The bool inpainting mask. Excluded regions should be set to "
+ "False, included regions should be set to True.",
+ )
+
+ def invoke(self, context: InvocationContext) -> FluxFillOutput:
+ return FluxFillOutput(fill_cond=FluxFillConditioningField(image=self.image, mask=self.mask))
diff --git a/invokeai/app/invocations/flux_ip_adapter.py b/invokeai/app/invocations/flux_ip_adapter.py
new file mode 100644
index 00000000000..c0d797d0bdd
--- /dev/null
+++ b/invokeai/app/invocations/flux_ip_adapter.py
@@ -0,0 +1,89 @@
+from builtins import float
+from typing import List, Literal, Union
+
+from pydantic import field_validator, model_validator
+from typing_extensions import Self
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import InputField
+from invokeai.app.invocations.ip_adapter import (
+ CLIP_VISION_MODEL_MAP,
+ IPAdapterField,
+ IPAdapterInvocation,
+ IPAdapterOutput,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageField
+from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.configs.ip_adapter import IPAdapter_Checkpoint_FLUX_Config
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation(
+ "flux_ip_adapter",
+ title="FLUX IP-Adapter",
+ tags=["ip_adapter", "control"],
+ category="conditioning",
+ version="1.0.0",
+)
+class FluxIPAdapterInvocation(BaseInvocation):
+ """Collects FLUX IP-Adapter info to pass to other nodes."""
+
+ # FLUXIPAdapterInvocation is based closely on IPAdapterInvocation, but with some unsupported features removed.
+
+ image: ImageField = InputField(description="The IP-Adapter image prompt(s).")
+ ip_adapter_model: ModelIdentifierField = InputField(
+ description="The IP-Adapter model.",
+ title="IP-Adapter Model",
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.IPAdapter,
+ )
+ # Currently, the only known ViT model used by FLUX IP-Adapters is ViT-L.
+ clip_vision_model: Literal["ViT-L"] = InputField(description="CLIP Vision model to use.", default="ViT-L")
+ weight: Union[float, List[float]] = InputField(
+ default=1, description="The weight given to the IP-Adapter", title="Weight"
+ )
+ begin_step_percent: float = InputField(
+ default=0, ge=0, le=1, 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)"
+ )
+
+ @field_validator("weight")
+ @classmethod
+ def validate_ip_adapter_weight(cls, v: float) -> float:
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self) -> Self:
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+ 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.models.get_config(self.ip_adapter_model.key)
+ assert isinstance(ip_adapter_info, IPAdapter_Checkpoint_FLUX_Config)
+
+ # Note: There is a IPAdapterInvokeAIConfig.image_encoder_model_id field, but it isn't trustworthy.
+ image_encoder_starter_model = CLIP_VISION_MODEL_MAP[self.clip_vision_model]
+ image_encoder_model_id = image_encoder_starter_model.source
+ image_encoder_model_name = image_encoder_starter_model.name
+ image_encoder_model = IPAdapterInvocation.get_clip_image_encoder(
+ context, image_encoder_model_id, image_encoder_model_name
+ )
+
+ return IPAdapterOutput(
+ ip_adapter=IPAdapterField(
+ image=self.image,
+ ip_adapter_model=self.ip_adapter_model,
+ image_encoder_model=ModelIdentifierField.from_config(image_encoder_model),
+ weight=self.weight,
+ target_blocks=[], # target_blocks is currently unused for FLUX IP-Adapters.
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ mask=None, # mask is currently unused for FLUX IP-Adapters.
+ ),
+ )
diff --git a/invokeai/app/invocations/flux_kontext.py b/invokeai/app/invocations/flux_kontext.py
new file mode 100644
index 00000000000..6820f3b3514
--- /dev/null
+++ b/invokeai/app/invocations/flux_kontext.py
@@ -0,0 +1,40 @@
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ FluxKontextConditioningField,
+ InputField,
+ OutputField,
+)
+from invokeai.app.invocations.primitives import ImageField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("flux_kontext_output")
+class FluxKontextOutput(BaseInvocationOutput):
+ """The conditioning output of a FLUX Kontext invocation."""
+
+ kontext_cond: FluxKontextConditioningField = OutputField(
+ description=FieldDescriptions.flux_kontext_conditioning, title="Kontext Conditioning"
+ )
+
+
+@invocation(
+ "flux_kontext",
+ title="Kontext Conditioning - FLUX",
+ tags=["conditioning", "kontext", "flux"],
+ category="conditioning",
+ version="1.0.0",
+)
+class FluxKontextInvocation(BaseInvocation):
+ """Prepares a reference image for FLUX Kontext conditioning."""
+
+ image: ImageField = InputField(description="The Kontext reference image.")
+
+ def invoke(self, context: InvocationContext) -> FluxKontextOutput:
+ """Packages the provided image into a Kontext conditioning field."""
+ return FluxKontextOutput(kontext_cond=FluxKontextConditioningField(image=self.image))
diff --git a/invokeai/app/invocations/flux_lora_loader.py b/invokeai/app/invocations/flux_lora_loader.py
new file mode 100644
index 00000000000..0fd96e097d5
--- /dev/null
+++ b/invokeai/app/invocations/flux_lora_loader.py
@@ -0,0 +1,178 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import CLIPField, LoRAField, ModelIdentifierField, T5EncoderField, TransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("flux_lora_loader_output")
+class FluxLoRALoaderOutput(BaseInvocationOutput):
+ """FLUX LoRA Loader Output"""
+
+ transformer: Optional[TransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="FLUX Transformer"
+ )
+ clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
+ t5_encoder: Optional[T5EncoderField] = OutputField(
+ default=None, description=FieldDescriptions.t5_encoder, title="T5 Encoder"
+ )
+
+
+@invocation(
+ "flux_lora_loader",
+ title="Apply LoRA - FLUX",
+ tags=["lora", "model", "flux"],
+ category="model",
+ version="1.2.1",
+)
+class FluxLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA model to a FLUX transformer and/or text encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ transformer: TransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="FLUX Transformer",
+ )
+ clip: CLIPField | None = InputField(
+ default=None,
+ title="CLIP",
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ )
+ t5_encoder: T5EncoderField | None = InputField(
+ default=None,
+ title="T5 Encoder",
+ description=FieldDescriptions.t5_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ # Check for existing LoRAs with the same key.
+ if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
+ if self.clip and any(lora.lora.key == lora_key for lora in self.clip.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to CLIP encoder.')
+ if self.t5_encoder and any(lora.lora.key == lora_key for lora in self.t5_encoder.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to T5 encoder.')
+
+ output = FluxLoRALoaderOutput()
+
+ # Attach LoRA layers to the models.
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ output.transformer.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+ output.clip.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+ if self.t5_encoder is not None:
+ output.t5_encoder = self.t5_encoder.model_copy(deep=True)
+ output.t5_encoder.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "flux_lora_collection_loader",
+ title="Apply LoRA Collection - FLUX",
+ tags=["lora", "model", "flux"],
+ category="model",
+ version="1.3.1",
+)
+class FLUXLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to a FLUX transformer."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+
+ transformer: Optional[TransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ clip: CLIPField | None = InputField(
+ default=None,
+ title="CLIP",
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ )
+ t5_encoder: T5EncoderField | None = InputField(
+ default=None,
+ title="T5 Encoder",
+ description=FieldDescriptions.t5_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
+ output = FluxLoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+
+ if self.t5_encoder is not None:
+ output.t5_encoder = self.t5_encoder.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ assert lora.lora.base in (BaseModelType.Flux, BaseModelType.Flux2)
+
+ added_loras.append(lora.lora.key)
+
+ if self.transformer is not None and output.transformer is not None:
+ output.transformer.loras.append(lora)
+
+ if self.clip is not None and output.clip is not None:
+ output.clip.loras.append(lora)
+
+ if self.t5_encoder is not None and output.t5_encoder is not None:
+ output.t5_encoder.loras.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/flux_model_loader.py b/invokeai/app/invocations/flux_model_loader.py
new file mode 100644
index 00000000000..c175ae7fedc
--- /dev/null
+++ b/invokeai/app/invocations/flux_model_loader.py
@@ -0,0 +1,93 @@
+from typing import Literal
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
+from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.t5_model_identifier import (
+ preprocess_t5_encoder_model_identifier,
+ preprocess_t5_tokenizer_model_identifier,
+)
+from invokeai.backend.flux.util import get_flux_max_seq_length
+from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+
+
+@invocation_output("flux_model_loader_output")
+class FluxModelLoaderOutput(BaseInvocationOutput):
+ """Flux base model loader output"""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ clip: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP")
+ t5_encoder: T5EncoderField = OutputField(description=FieldDescriptions.t5_encoder, title="T5 Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+ max_seq_len: Literal[256, 512] = OutputField(
+ description="The max sequence length to used for the T5 encoder. (256 for schnell transformer, 512 for dev transformer)",
+ title="Max Seq Length",
+ )
+
+
+@invocation(
+ "flux_model_loader",
+ title="Main Model - FLUX",
+ tags=["model", "flux"],
+ category="model",
+ version="1.0.7",
+)
+class FluxModelLoaderInvocation(BaseInvocation):
+ """Loads a flux base model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.flux_model,
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.Main,
+ )
+
+ t5_encoder_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.t5_encoder,
+ title="T5 Encoder",
+ ui_model_type=ModelType.T5Encoder,
+ )
+
+ clip_embed_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.clip_embed_model,
+ title="CLIP Embed",
+ ui_model_type=ModelType.CLIPEmbed,
+ )
+
+ vae_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.vae_model,
+ title="VAE",
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.VAE,
+ )
+
+ def invoke(self, context: InvocationContext) -> FluxModelLoaderOutput:
+ for key in [self.model.key, self.t5_encoder_model.key, self.clip_embed_model.key, self.vae_model.key]:
+ if not context.models.exists(key):
+ raise ValueError(f"Unknown model: {key}")
+
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+
+ tokenizer = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ clip_encoder = self.clip_embed_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+
+ tokenizer2 = preprocess_t5_tokenizer_model_identifier(self.t5_encoder_model)
+ t5_encoder = preprocess_t5_encoder_model_identifier(self.t5_encoder_model)
+
+ transformer_config = context.models.get_config(transformer)
+ assert isinstance(transformer_config, Checkpoint_Config_Base)
+
+ return FluxModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ clip=CLIPField(tokenizer=tokenizer, text_encoder=clip_encoder, loras=[], skipped_layers=0),
+ t5_encoder=T5EncoderField(tokenizer=tokenizer2, text_encoder=t5_encoder, loras=[]),
+ vae=VAEField(vae=vae),
+ max_seq_len=get_flux_max_seq_length(transformer_config.variant),
+ )
diff --git a/invokeai/app/invocations/flux_redux.py b/invokeai/app/invocations/flux_redux.py
new file mode 100644
index 00000000000..b68e9911c56
--- /dev/null
+++ b/invokeai/app/invocations/flux_redux.py
@@ -0,0 +1,166 @@
+import math
+from typing import Literal, Optional
+
+import torch
+from PIL import Image
+from transformers import SiglipImageProcessor, SiglipVisionModel
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ FluxReduxConditioningField,
+ InputField,
+ OutputField,
+ TensorField,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageField
+from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.redux.flux_redux_model import FluxReduxModel
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.starter_models import siglip
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+from invokeai.backend.sig_lip.sig_lip_pipeline import SigLipPipeline
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation_output("flux_redux_output")
+class FluxReduxOutput(BaseInvocationOutput):
+ """The conditioning output of a FLUX Redux invocation."""
+
+ redux_cond: FluxReduxConditioningField = OutputField(
+ description=FieldDescriptions.flux_redux_conditioning, title="Conditioning"
+ )
+
+
+DOWNSAMPLING_FUNCTIONS = Literal["nearest", "bilinear", "bicubic", "area", "nearest-exact"]
+
+
+@invocation(
+ "flux_redux",
+ title="FLUX Redux",
+ tags=["ip_adapter", "control"],
+ category="conditioning",
+ version="2.1.0",
+ classification=Classification.Beta,
+)
+class FluxReduxInvocation(BaseInvocation):
+ """Runs a FLUX Redux model to generate a conditioning tensor."""
+
+ image: ImageField = InputField(description="The FLUX Redux image prompt.")
+ mask: Optional[TensorField] = InputField(
+ default=None,
+ description="The bool mask associated with this FLUX Redux image prompt. Excluded regions should be set to "
+ "False, included regions should be set to True.",
+ )
+ redux_model: ModelIdentifierField = InputField(
+ description="The FLUX Redux model to use.",
+ title="FLUX Redux Model",
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.FluxRedux,
+ )
+ downsampling_factor: int = InputField(
+ ge=1,
+ le=9,
+ default=1,
+ description="Redux Downsampling Factor (1-9)",
+ )
+ downsampling_function: DOWNSAMPLING_FUNCTIONS = InputField(
+ default="area",
+ description="Redux Downsampling Function",
+ )
+ weight: float = InputField(
+ ge=0,
+ le=1,
+ default=1.0,
+ description="Redux weight (0.0-1.0)",
+ )
+
+ def invoke(self, context: InvocationContext) -> FluxReduxOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+
+ encoded_x = self._siglip_encode(context, image)
+ redux_conditioning = self._flux_redux_encode(context, encoded_x)
+ if self.downsampling_factor > 1 or self.weight != 1.0:
+ redux_conditioning = self._downsample_weight(context, redux_conditioning)
+
+ tensor_name = context.tensors.save(redux_conditioning)
+ return FluxReduxOutput(
+ redux_cond=FluxReduxConditioningField(conditioning=TensorField(tensor_name=tensor_name), mask=self.mask)
+ )
+
+ @torch.no_grad()
+ def _downsample_weight(self, context: InvocationContext, redux_conditioning: torch.Tensor) -> torch.Tensor:
+ # Downsampling derived from https://github.com/kaibioinfo/ComfyUI_AdvancedRefluxControl
+ (b, t, h) = redux_conditioning.shape
+ m = int(math.sqrt(t))
+ if self.downsampling_factor > 1:
+ redux_conditioning = redux_conditioning.view(b, m, m, h)
+ redux_conditioning = torch.nn.functional.interpolate(
+ redux_conditioning.transpose(1, -1),
+ size=(m // self.downsampling_factor, m // self.downsampling_factor),
+ mode=self.downsampling_function,
+ )
+ redux_conditioning = redux_conditioning.transpose(1, -1).reshape(b, -1, h)
+ if self.weight != 1.0:
+ redux_conditioning = redux_conditioning * self.weight * self.weight
+ return redux_conditioning
+
+ @torch.no_grad()
+ def _siglip_encode(self, context: InvocationContext, image: Image.Image) -> torch.Tensor:
+ siglip_model_config = self._get_siglip_model(context)
+ with context.models.load(siglip_model_config.key).model_on_device() as (_, model):
+ assert isinstance(model, SiglipVisionModel)
+
+ model_abs_path = context.models.get_absolute_path(siglip_model_config)
+ processor = SiglipImageProcessor.from_pretrained(model_abs_path, local_files_only=True)
+ assert isinstance(processor, SiglipImageProcessor)
+
+ siglip_pipeline = SigLipPipeline(processor, model)
+ return siglip_pipeline.encode_image(
+ x=image, device=TorchDevice.choose_torch_device(), dtype=TorchDevice.choose_torch_dtype()
+ )
+
+ @torch.no_grad()
+ def _flux_redux_encode(self, context: InvocationContext, encoded_x: torch.Tensor) -> torch.Tensor:
+ with context.models.load(self.redux_model).model_on_device() as (_, flux_redux):
+ assert isinstance(flux_redux, FluxReduxModel)
+ dtype = next(flux_redux.parameters()).dtype
+ encoded_x = encoded_x.to(dtype=dtype)
+ return flux_redux(encoded_x)
+
+ def _get_siglip_model(self, context: InvocationContext) -> AnyModelConfig:
+ siglip_models = context.models.search_by_attrs(name=siglip.name, base=BaseModelType.Any, type=ModelType.SigLIP)
+
+ if not len(siglip_models) > 0:
+ context.logger.warning(
+ f"The SigLIP model required by FLUX Redux ({siglip.name}) is not installed. Downloading and installing now. This may take a while."
+ )
+
+ # TODO(psyche): Can the probe reliably determine the type of the model? Just hardcoding it bc I don't want to experiment now
+ config_overrides = ModelRecordChanges(name=siglip.name, type=ModelType.SigLIP)
+
+ # Queue the job
+ job = context._services.model_manager.install.heuristic_import(siglip.source, config=config_overrides)
+
+ # Wait for up to 10 minutes - model is ~3.5GB
+ context._services.model_manager.install.wait_for_job(job, timeout=600)
+
+ siglip_models = context.models.search_by_attrs(
+ name=siglip.name,
+ base=BaseModelType.Any,
+ type=ModelType.SigLIP,
+ )
+
+ if len(siglip_models) == 0:
+ context.logger.error("Error while fetching SigLIP for FLUX Redux")
+ assert len(siglip_models) == 1
+
+ return siglip_models[0]
diff --git a/invokeai/app/invocations/flux_text_encoder.py b/invokeai/app/invocations/flux_text_encoder.py
new file mode 100644
index 00000000000..8b3b33fad1c
--- /dev/null
+++ b/invokeai/app/invocations/flux_text_encoder.py
@@ -0,0 +1,269 @@
+from contextlib import ExitStack
+from typing import Iterator, Literal, Optional, Tuple, Union
+
+import torch
+from transformers import CLIPTextModel, CLIPTokenizer, T5EncoderModel, T5Tokenizer, T5TokenizerFast
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ FluxConditioningField,
+ Input,
+ InputField,
+ TensorField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import CLIPField, T5EncoderField
+from invokeai.app.invocations.primitives import FluxConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.conditioner import HFEncoder
+from invokeai.backend.model_manager.taxonomy import ModelFormat
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX, FLUX_LORA_T5_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, FLUXConditioningInfo
+
+
+@invocation(
+ "flux_text_encoder",
+ title="Prompt - FLUX",
+ tags=["prompt", "conditioning", "flux"],
+ category="prompt",
+ version="1.1.2",
+)
+class FluxTextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for a flux image."""
+
+ clip: CLIPField = InputField(
+ title="CLIP",
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ )
+ t5_encoder: T5EncoderField = InputField(
+ title="T5Encoder",
+ description=FieldDescriptions.t5_encoder,
+ input=Input.Connection,
+ )
+ t5_max_seq_len: Literal[256, 512] = InputField(
+ description="Max sequence length for the T5 encoder. Expected to be 256 for FLUX schnell models and 512 for FLUX dev models."
+ )
+ prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
+ mask: Optional[TensorField] = InputField(
+ default=None, description="A mask defining the region that this conditioning prompt applies to."
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
+ # Note: The T5 and CLIP encoding are done in separate functions to ensure that all model references are locally
+ # scoped. This ensures that the T5 model can be freed and gc'd before loading the CLIP model (if necessary).
+ t5_embeddings = self._t5_encode(context)
+ clip_embeddings = self._clip_encode(context)
+
+ # Move embeddings to CPU for storage to save VRAM
+ # They will be moved to the appropriate device when used by the denoiser
+ t5_embeddings = t5_embeddings.detach().to("cpu")
+ clip_embeddings = clip_embeddings.detach().to("cpu")
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[FLUXConditioningInfo(clip_embeds=clip_embeddings, t5_embeds=t5_embeddings)]
+ )
+
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return FluxConditioningOutput(
+ conditioning=FluxConditioningField(conditioning_name=conditioning_name, mask=self.mask)
+ )
+
+ def _t5_encode(self, context: InvocationContext) -> torch.Tensor:
+ prompt = [self.prompt]
+
+ t5_encoder_info = context.models.load(self.t5_encoder.text_encoder)
+ t5_encoder_config = t5_encoder_info.config
+ assert t5_encoder_config is not None
+
+ with (
+ t5_encoder_info.model_on_device() as (cached_weights, t5_text_encoder),
+ context.models.load(self.t5_encoder.tokenizer) as t5_tokenizer,
+ ExitStack() as exit_stack,
+ ):
+ assert isinstance(t5_text_encoder, T5EncoderModel)
+ assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
+
+ # Determine if the model is quantized.
+ # If the model is quantized, then we need to apply the LoRA weights as sidecar layers. This results in
+ # slower inference than direct patching, but is agnostic to the quantization format.
+ if t5_encoder_config.format in [ModelFormat.T5Encoder, ModelFormat.Diffusers]:
+ model_is_quantized = False
+ elif t5_encoder_config.format in [
+ ModelFormat.BnbQuantizedLlmInt8b,
+ ModelFormat.BnbQuantizednf4b,
+ ModelFormat.GGUFQuantized,
+ ]:
+ model_is_quantized = True
+ else:
+ raise ValueError(f"Unsupported model format: {t5_encoder_config.format}")
+
+ # Apply LoRA models to the T5 encoder.
+ # Note: We apply the LoRA after the encoder has been moved to its target device for faster patching.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=t5_text_encoder,
+ patches=self._t5_lora_iterator(context),
+ prefix=FLUX_LORA_T5_PREFIX,
+ dtype=t5_text_encoder.dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=model_is_quantized,
+ )
+ )
+
+ t5_encoder = HFEncoder(t5_text_encoder, t5_tokenizer, False, self.t5_max_seq_len)
+
+ if context.config.get().log_tokenization:
+ self._log_t5_tokenization(context, t5_tokenizer)
+
+ context.util.signal_progress("Running T5 encoder")
+ prompt_embeds = t5_encoder(prompt)
+
+ assert isinstance(prompt_embeds, torch.Tensor)
+ return prompt_embeds
+
+ def _clip_encode(self, context: InvocationContext) -> torch.Tensor:
+ prompt = [self.prompt]
+
+ clip_text_encoder_info = context.models.load(self.clip.text_encoder)
+ clip_text_encoder_config = clip_text_encoder_info.config
+ assert clip_text_encoder_config is not None
+
+ with (
+ clip_text_encoder_info.model_on_device() as (cached_weights, clip_text_encoder),
+ context.models.load(self.clip.tokenizer) as clip_tokenizer,
+ ExitStack() as exit_stack,
+ ):
+ assert isinstance(clip_text_encoder, CLIPTextModel)
+ assert isinstance(clip_tokenizer, CLIPTokenizer)
+
+ # Apply LoRA models to the CLIP encoder.
+ # Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
+ if clip_text_encoder_config.format in [ModelFormat.Diffusers]:
+ # The model is non-quantized, so we can apply the LoRA weights directly into the model.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=clip_text_encoder,
+ patches=self._clip_lora_iterator(context),
+ prefix=FLUX_LORA_CLIP_PREFIX,
+ dtype=clip_text_encoder.dtype,
+ cached_weights=cached_weights,
+ )
+ )
+ else:
+ # There are currently no supported CLIP quantized models. Add support here if needed.
+ raise ValueError(f"Unsupported model format: {clip_text_encoder_config.format}")
+
+ clip_encoder = HFEncoder(clip_text_encoder, clip_tokenizer, True, 77)
+
+ if context.config.get().log_tokenization:
+ self._log_clip_tokenization(context, clip_tokenizer)
+
+ context.util.signal_progress("Running CLIP encoder")
+ pooled_prompt_embeds = clip_encoder(prompt)
+
+ assert isinstance(pooled_prompt_embeds, torch.Tensor)
+ return pooled_prompt_embeds
+
+ def _clip_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in self.clip.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+
+ def _t5_lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in self.t5_encoder.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+
+ def _log_t5_tokenization(
+ self,
+ context: InvocationContext,
+ tokenizer: Union[T5Tokenizer, T5TokenizerFast],
+ ) -> None:
+ """Logs the tokenization of a prompt for a T5-based model like FLUX."""
+
+ # Tokenize the prompt using the same parameters as the model's text encoder.
+ # T5 tokenizers add an EOS token () and then pad to max_length.
+ tokenized_output = tokenizer(
+ self.prompt,
+ padding="max_length",
+ max_length=self.t5_max_seq_len,
+ truncation=True,
+ add_special_tokens=True, # This is important for T5 to add the EOS token.
+ return_tensors="pt",
+ )
+
+ input_ids = tokenized_output.input_ids[0]
+ tokens = tokenizer.convert_ids_to_tokens(input_ids)
+
+ # The T5 tokenizer uses a space-like character ' ' (U+2581) to denote spaces.
+ # We'll replace it with a regular space for readability.
+ tokens = [t.replace("\u2581", " ") for t in tokens]
+
+ tokenized_str = ""
+ used_tokens = 0
+ for token in tokens:
+ if token == tokenizer.eos_token:
+ tokenized_str += f"\x1b[0;31m{token}\x1b[0m" # Red for EOS
+ used_tokens += 1
+ elif token == tokenizer.pad_token:
+ # tokenized_str += f"\x1b[0;34m{token}\x1b[0m" # Blue for PAD
+ continue
+ else:
+ color = (used_tokens % 6) + 1 # Cycle through 6 colors
+ tokenized_str += f"\x1b[0;3{color}m{token}\x1b[0m"
+ used_tokens += 1
+
+ context.logger.info(f">> [T5 TOKENLOG] Tokens ({used_tokens}/{self.t5_max_seq_len}):")
+ context.logger.info(f"{tokenized_str}\x1b[0m")
+
+ def _log_clip_tokenization(
+ self,
+ context: InvocationContext,
+ tokenizer: CLIPTokenizer,
+ ) -> None:
+ """Logs the tokenization of a prompt for a CLIP-based model."""
+ max_length = tokenizer.model_max_length
+
+ tokenized_output = tokenizer(
+ self.prompt,
+ padding="max_length",
+ max_length=max_length,
+ truncation=True,
+ return_tensors="pt",
+ )
+
+ input_ids = tokenized_output.input_ids[0]
+ attention_mask = tokenized_output.attention_mask[0]
+ tokens = tokenizer.convert_ids_to_tokens(input_ids)
+
+ # The CLIP tokenizer uses '' to denote spaces.
+ # We'll replace it with a regular space for readability.
+ tokens = [t.replace("", " ") for t in tokens]
+
+ tokenized_str = ""
+ used_tokens = 0
+ for i, token in enumerate(tokens):
+ if attention_mask[i] == 0:
+ # Do not log padding tokens.
+ continue
+
+ if token == tokenizer.bos_token:
+ tokenized_str += f"\x1b[0;32m{token}\x1b[0m" # Green for BOS
+ elif token == tokenizer.eos_token:
+ tokenized_str += f"\x1b[0;31m{token}\x1b[0m" # Red for EOS
+ else:
+ color = (used_tokens % 6) + 1 # Cycle through 6 colors
+ tokenized_str += f"\x1b[0;3{color}m{token}\x1b[0m"
+ used_tokens += 1
+
+ context.logger.info(f">> [CLIP TOKENLOG] Tokens ({used_tokens}/{max_length}):")
+ context.logger.info(f"{tokenized_str}\x1b[0m")
diff --git a/invokeai/app/invocations/flux_vae_decode.py b/invokeai/app/invocations/flux_vae_decode.py
new file mode 100644
index 00000000000..c55dfb539ac
--- /dev/null
+++ b/invokeai/app/invocations/flux_vae_decode.py
@@ -0,0 +1,67 @@
+import torch
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+
+@invocation(
+ "flux_vae_decode",
+ title="Latents to Image - FLUX",
+ tags=["latents", "image", "vae", "l2i", "flux"],
+ category="latents",
+ version="1.0.2",
+)
+class FluxVaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+
+ def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
+ assert isinstance(vae_info.model, AutoEncoder)
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="decode", image_tensor=latents, vae=vae_info.model
+ )
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoEncoder)
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+ img = vae.decode(latents)
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c") # noqa: F821
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+ return img_pil
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+ vae_info = context.models.load(self.vae.vae)
+ context.util.signal_progress("Running VAE")
+ image = self._vae_decode(vae_info=vae_info, latents=latents)
+
+ TorchDevice.empty_cache()
+ image_dto = context.images.save(image=image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/flux_vae_encode.py b/invokeai/app/invocations/flux_vae_encode.py
new file mode 100644
index 00000000000..4ec0365c2cb
--- /dev/null
+++ b/invokeai/app/invocations/flux_vae_encode.py
@@ -0,0 +1,72 @@
+import einops
+import torch
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+
+@invocation(
+ "flux_vae_encode",
+ title="Image to Latents - FLUX",
+ tags=["latents", "image", "vae", "i2l", "flux"],
+ category="latents",
+ version="1.0.1",
+)
+class FluxVaeEncodeInvocation(BaseInvocation):
+ """Encodes an image into latents."""
+
+ image: ImageField = InputField(
+ description="The image to encode.",
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ # TODO(ryand): Expose seed parameter at the invocation level.
+ # TODO(ryand): Write a util function for generating random tensors that is consistent across devices / dtypes.
+ # There's a starting point in get_noise(...), but it needs to be extracted and generalized. This function
+ # should be used for VAE encode sampling.
+ assert isinstance(vae_info.model, AutoEncoder)
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="encode", image_tensor=image_tensor, vae=vae_info.model
+ )
+ generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoEncoder)
+ vae_dtype = next(iter(vae.parameters())).dtype
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+ latents = vae.encode(image_tensor, sample=True, generator=generator)
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ vae_info = context.models.load(self.vae.vae)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ context.util.signal_progress("Running VAE")
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/grounding_dino.py b/invokeai/app/invocations/grounding_dino.py
new file mode 100644
index 00000000000..4d900c5034c
--- /dev/null
+++ b/invokeai/app/invocations/grounding_dino.py
@@ -0,0 +1,100 @@
+from pathlib import Path
+from typing import Literal
+
+import torch
+from PIL import Image
+from transformers import pipeline
+from transformers.pipelines import ZeroShotObjectDetectionPipeline
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import BoundingBoxField, ImageField, InputField
+from invokeai.app.invocations.primitives import BoundingBoxCollectionOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.grounding_dino.detection_result import DetectionResult
+from invokeai.backend.image_util.grounding_dino.grounding_dino_pipeline import GroundingDinoPipeline
+
+GroundingDinoModelKey = Literal["grounding-dino-tiny", "grounding-dino-base"]
+GROUNDING_DINO_MODEL_IDS: dict[GroundingDinoModelKey, str] = {
+ "grounding-dino-tiny": "IDEA-Research/grounding-dino-tiny",
+ "grounding-dino-base": "IDEA-Research/grounding-dino-base",
+}
+
+
+@invocation(
+ "grounding_dino",
+ title="Grounding DINO (Text Prompt Object Detection)",
+ tags=["prompt", "object detection"],
+ category="segmentation",
+ version="1.0.0",
+)
+class GroundingDinoInvocation(BaseInvocation):
+ """Runs a Grounding DINO model. Performs zero-shot bounding-box object detection from a text prompt."""
+
+ # Reference:
+ # - https://arxiv.org/pdf/2303.05499
+ # - https://huggingface.co/docs/transformers/v4.43.3/en/model_doc/grounding-dino#grounded-sam
+ # - https://github.com/NielsRogge/Transformers-Tutorials/blob/a39f33ac1557b02ebfb191ea7753e332b5ca933f/Grounding%20DINO/GroundingDINO_with_Segment_Anything.ipynb
+
+ model: GroundingDinoModelKey = InputField(description="The Grounding DINO model to use.")
+ prompt: str = InputField(description="The prompt describing the object to segment.")
+ image: ImageField = InputField(description="The image to segment.")
+ detection_threshold: float = InputField(
+ description="The detection threshold for the Grounding DINO model. All detected bounding boxes with scores above this threshold will be returned.",
+ ge=0.0,
+ le=1.0,
+ default=0.3,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> BoundingBoxCollectionOutput:
+ # The model expects a 3-channel RGB image.
+ image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
+
+ detections = self._detect(
+ context=context, image=image_pil, labels=[self.prompt], threshold=self.detection_threshold
+ )
+
+ # Convert detections to BoundingBoxCollectionOutput.
+ bounding_boxes: list[BoundingBoxField] = []
+ for detection in detections:
+ bounding_boxes.append(
+ BoundingBoxField(
+ x_min=detection.box.xmin,
+ x_max=detection.box.xmax,
+ y_min=detection.box.ymin,
+ y_max=detection.box.ymax,
+ score=detection.score,
+ )
+ )
+ return BoundingBoxCollectionOutput(collection=bounding_boxes)
+
+ @staticmethod
+ def _load_grounding_dino(model_path: Path):
+ grounding_dino_pipeline = pipeline(
+ model=str(model_path),
+ task="zero-shot-object-detection",
+ local_files_only=True,
+ # TODO(ryand): Setting the torch_dtype here doesn't work. Investigate whether fp16 is supported by the
+ # model, and figure out how to make it work in the pipeline.
+ # torch_dtype=TorchDevice.choose_torch_dtype(),
+ )
+ assert isinstance(grounding_dino_pipeline, ZeroShotObjectDetectionPipeline)
+ return GroundingDinoPipeline(grounding_dino_pipeline)
+
+ def _detect(
+ self,
+ context: InvocationContext,
+ image: Image.Image,
+ labels: list[str],
+ threshold: float = 0.3,
+ ) -> list[DetectionResult]:
+ """Use Grounding DINO to detect bounding boxes for a set of labels in an image."""
+ # TODO(ryand): I copied this "."-handling logic from the transformers example code. Test it and see if it
+ # actually makes a difference.
+ labels = [label if label.endswith(".") else label + "." for label in labels]
+
+ with context.models.load_remote_model(
+ source=GROUNDING_DINO_MODEL_IDS[self.model], loader=GroundingDinoInvocation._load_grounding_dino
+ ) as detector:
+ assert isinstance(detector, GroundingDinoPipeline)
+ return detector.detect(image=image, candidate_labels=labels, threshold=threshold)
diff --git a/invokeai/app/invocations/hed.py b/invokeai/app/invocations/hed.py
new file mode 100644
index 00000000000..e2b68143e52
--- /dev/null
+++ b/invokeai/app/invocations/hed.py
@@ -0,0 +1,33 @@
+from builtins import bool
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.hed import ControlNetHED_Apache2, HEDEdgeDetector
+
+
+@invocation(
+ "hed_edge_detection",
+ title="HED Edge Detection",
+ tags=["controlnet", "hed", "softedge"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class HEDEdgeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Geneartes an edge map using the HED (softedge) model."""
+
+ image: ImageField = InputField(description="The image to process")
+ scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ loaded_model = context.models.load_remote_model(HEDEdgeDetector.get_model_url(), HEDEdgeDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, ControlNetHED_Apache2)
+ hed_processor = HEDEdgeDetector(model)
+ edge_map = hed_processor.run(image=image, scribble=self.scribble)
+
+ image_dto = context.images.save(image=edge_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/ideal_size.py b/invokeai/app/invocations/ideal_size.py
new file mode 100644
index 00000000000..5cfa9c04d01
--- /dev/null
+++ b/invokeai/app/invocations/ideal_size.py
@@ -0,0 +1,76 @@
+import math
+from typing import Tuple
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
+from invokeai.app.invocations.model import UNetField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+
+
+@invocation_output("ideal_size_output")
+class IdealSizeOutput(BaseInvocationOutput):
+ """Base class for invocations that output an image"""
+
+ width: int = OutputField(description="The ideal width of the image (in pixels)")
+ height: int = OutputField(description="The ideal height of the image (in pixels)")
+
+
+@invocation(
+ "ideal_size",
+ title="Ideal Size - SD1.5, SDXL",
+ tags=["latents", "math", "ideal_size"],
+ category="latents",
+ version="1.0.6",
+)
+class IdealSizeInvocation(BaseInvocation):
+ """Calculates the ideal size for generation to avoid duplication"""
+
+ width: int = InputField(default=1024, description="Final image width")
+ height: int = InputField(default=576, description="Final image height")
+ unet: UNetField = InputField(description=FieldDescriptions.unet)
+ multiplier: float = InputField(
+ default=1.0,
+ description="Amount to multiply the model's dimensions by when calculating the ideal size (may result in "
+ "initial generation artifacts if too large)",
+ )
+
+ def trim_to_multiple_of(self, *args: int, multiple_of: int = LATENT_SCALE_FACTOR) -> Tuple[int, ...]:
+ return tuple((x - x % multiple_of) for x in args)
+
+ def invoke(self, context: InvocationContext) -> IdealSizeOutput:
+ unet_config = context.models.get_config(self.unet.unet.key)
+ aspect = self.width / self.height
+
+ if unet_config.base == BaseModelType.StableDiffusion1:
+ dimension = 512
+ elif unet_config.base == BaseModelType.StableDiffusion2:
+ dimension = 768
+ elif unet_config.base in (
+ BaseModelType.StableDiffusionXL,
+ BaseModelType.Flux,
+ BaseModelType.Flux2,
+ BaseModelType.StableDiffusion3,
+ ):
+ dimension = 1024
+ else:
+ raise ValueError(f"Unsupported model type: {unet_config.base}")
+
+ dimension = dimension * self.multiplier
+ min_dimension = math.floor(dimension * 0.5)
+ model_area = dimension * dimension # hardcoded for now since all models are trained on square images
+
+ if aspect > 1.0:
+ init_height = max(min_dimension, math.sqrt(model_area / aspect))
+ init_width = init_height * aspect
+ else:
+ init_width = max(min_dimension, math.sqrt(model_area * aspect))
+ init_height = init_width / aspect
+
+ scaled_width, scaled_height = self.trim_to_multiple_of(
+ math.floor(init_width),
+ math.floor(init_height),
+ )
+
+ return IdealSizeOutput(width=scaled_width, height=scaled_height)
diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py
new file mode 100644
index 00000000000..18709a25091
--- /dev/null
+++ b/invokeai/app/invocations/image.py
@@ -0,0 +1,1680 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+from pathlib import Path
+from typing import Literal, Optional
+
+import cv2
+import numpy
+import torch
+from PIL import Image, ImageChops, ImageFilter, ImageOps
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ Classification,
+ invocation,
+)
+from invokeai.app.invocations.constants import IMAGE_MODES
+from invokeai.app.invocations.fields import (
+ BoundingBoxField,
+ ColorField,
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.primitives import ImageOutput, StringOutput
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.misc import SEED_MAX
+from invokeai.backend.image_util.color_conversion import (
+ linear_srgb_from_oklab,
+ linear_srgb_from_oklch,
+ linear_srgb_from_srgb,
+ oklab_from_linear_srgb,
+ oklch_from_oklab,
+ srgb_from_linear_srgb,
+)
+from invokeai.backend.image_util.invisible_watermark import InvisibleWatermark
+from invokeai.backend.image_util.safety_checker import SafetyChecker
+
+
+def _extract_alpha_channel(image: Image.Image) -> Image.Image | None:
+ if image.mode in ("RGBA", "LA", "PA"):
+ return image.getchannel("A")
+ return None
+
+
+def _restore_original_mode(image: Image.Image, mode: str, alpha_channel: Image.Image | None) -> Image.Image:
+ if alpha_channel is None:
+ return image.convert(mode)
+
+ if mode == "RGBA":
+ image = image.convert("RGB")
+ elif mode == "LA":
+ image = image.convert("L")
+ elif mode == "PA":
+ image = image.convert("P")
+ else:
+ return image.convert(mode)
+
+ image.putalpha(alpha_channel)
+ return image
+
+
+@invocation("show_image", title="Show Image", tags=["image"], category="image", version="1.0.1")
+class ShowImageInvocation(BaseInvocation):
+ """Displays a provided image using the OS image viewer, and passes it forward in the pipeline."""
+
+ image: ImageField = InputField(description="The image to show")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ image.show()
+
+ # TODO: how to handle failure?
+
+ return ImageOutput(
+ image=ImageField(image_name=self.image.image_name),
+ width=image.width,
+ height=image.height,
+ )
+
+
+@invocation(
+ "blank_image",
+ title="Blank Image",
+ tags=["image"],
+ category="image",
+ version="1.2.2",
+)
+class BlankImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Creates a blank image and forwards it to the pipeline"""
+
+ width: int = InputField(default=512, description="The width of the image")
+ height: int = InputField(default=512, description="The height of the image")
+ mode: Literal["RGB", "RGBA"] = InputField(default="RGB", description="The mode of the image")
+ color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color of the image")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = Image.new(mode=self.mode, size=(self.width, self.height), color=self.color.tuple())
+
+ image_dto = context.images.save(image=image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_crop",
+ title="Crop Image",
+ tags=["image", "crop"],
+ category="image",
+ version="1.2.2",
+)
+class ImageCropInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Crops an image to a specified box. The box can be outside of the image."""
+
+ image: ImageField = InputField(description="The image to crop")
+ x: int = InputField(default=0, description="The left x coordinate of the crop rectangle")
+ y: int = InputField(default=0, description="The top y coordinate of the crop rectangle")
+ width: int = InputField(default=512, gt=0, description="The width of the crop rectangle")
+ height: int = InputField(default=512, gt=0, description="The height of the crop rectangle")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_crop = Image.new(mode="RGBA", size=(self.width, self.height), color=(0, 0, 0, 0))
+ image_crop.paste(image, (-self.x, -self.y))
+
+ image_dto = context.images.save(image=image_crop)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ invocation_type="img_pad_crop",
+ title="Center Pad or Crop Image",
+ category="image",
+ tags=["image", "pad", "crop"],
+ version="1.0.0",
+)
+class CenterPadCropInvocation(BaseInvocation):
+ """Pad or crop an image's sides from the center by specified pixels. Positive values are outside of the image."""
+
+ image: ImageField = InputField(description="The image to crop")
+ left: int = InputField(
+ default=0,
+ description="Number of pixels to pad/crop from the left (negative values crop inwards, positive values pad outwards)",
+ )
+ right: int = InputField(
+ default=0,
+ description="Number of pixels to pad/crop from the right (negative values crop inwards, positive values pad outwards)",
+ )
+ top: int = InputField(
+ default=0,
+ description="Number of pixels to pad/crop from the top (negative values crop inwards, positive values pad outwards)",
+ )
+ bottom: int = InputField(
+ default=0,
+ description="Number of pixels to pad/crop from the bottom (negative values crop inwards, positive values pad outwards)",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ # Calculate and create new image dimensions
+ new_width = image.width + self.right + self.left
+ new_height = image.height + self.top + self.bottom
+ image_crop = Image.new(mode="RGBA", size=(new_width, new_height), color=(0, 0, 0, 0))
+
+ # Paste new image onto input
+ image_crop.paste(image, (self.left, self.top))
+
+ image_dto = context.images.save(image=image_crop)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_paste",
+ title="Paste Image",
+ tags=["image", "paste"],
+ category="image",
+ version="1.2.2",
+)
+class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Pastes an image into another image."""
+
+ base_image: ImageField = InputField(description="The base image")
+ image: ImageField = InputField(description="The image to paste")
+ mask: Optional[ImageField] = InputField(
+ default=None,
+ description="The mask to use when pasting",
+ )
+ x: int = InputField(default=0, description="The left x coordinate at which to paste the image")
+ y: int = InputField(default=0, description="The top y coordinate at which to paste the image")
+ crop: bool = InputField(default=False, description="Crop to base image dimensions")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ base_image = context.images.get_pil(self.base_image.image_name, mode="RGBA")
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+ mask = None
+ if self.mask is not None:
+ mask = context.images.get_pil(self.mask.image_name, mode="L")
+ mask = ImageOps.invert(mask)
+ # TODO: probably shouldn't invert mask here... should user be required to do it?
+
+ min_x = min(0, self.x)
+ min_y = min(0, self.y)
+ max_x = max(base_image.width, image.width + self.x)
+ max_y = max(base_image.height, image.height + self.y)
+
+ new_image = Image.new(mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0))
+ new_image.paste(base_image, (abs(min_x), abs(min_y)))
+
+ # Create a temporary image to paste the image with transparency
+ temp_image = Image.new("RGBA", new_image.size)
+ temp_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask)
+ new_image = Image.alpha_composite(new_image, temp_image)
+
+ if self.crop:
+ base_w, base_h = base_image.size
+ new_image = new_image.crop((abs(min_x), abs(min_y), abs(min_x) + base_w, abs(min_y) + base_h))
+
+ image_dto = context.images.save(image=new_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "tomask",
+ title="Mask from Alpha",
+ tags=["image", "mask"],
+ category="mask",
+ version="1.2.2",
+)
+class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Extracts the alpha channel of an image as a mask."""
+
+ image: ImageField = InputField(description="The image to create the mask from")
+ invert: bool = InputField(default=False, description="Whether or not to invert the mask")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_mask = image.split()[-1]
+ if self.invert:
+ image_mask = ImageOps.invert(image_mask)
+
+ image_dto = context.images.save(image=image_mask, image_category=ImageCategory.MASK)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_mul",
+ title="Multiply Images",
+ tags=["image", "multiply"],
+ category="image",
+ version="1.2.2",
+)
+class ImageMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Multiplies two images together using `PIL.ImageChops.multiply()`."""
+
+ image1: ImageField = InputField(description="The first image to multiply")
+ image2: ImageField = InputField(description="The second image to multiply")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image1 = context.images.get_pil(self.image1.image_name)
+ image2 = context.images.get_pil(self.image2.image_name)
+
+ multiply_image = ImageChops.multiply(image1, image2)
+
+ image_dto = context.images.save(image=multiply_image)
+
+ return ImageOutput.build(image_dto)
+
+
+IMAGE_CHANNELS = Literal["A", "R", "G", "B"]
+
+
+@invocation(
+ "img_chan",
+ title="Extract Image Channel",
+ tags=["image", "channel"],
+ category="image",
+ version="1.2.2",
+)
+class ImageChannelInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Gets a channel from an image."""
+
+ image: ImageField = InputField(description="The image to get the channel from")
+ channel: IMAGE_CHANNELS = InputField(default="A", description="The channel to get")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ channel_image = image.getchannel(self.channel)
+
+ image_dto = context.images.save(image=channel_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_conv",
+ title="Convert Image Mode",
+ tags=["image", "convert"],
+ category="image",
+ version="1.2.2",
+)
+class ImageConvertInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Converts an image to a different mode."""
+
+ image: ImageField = InputField(description="The image to convert")
+ mode: IMAGE_MODES = InputField(default="L", description="The mode to convert to")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ converted_image = image.convert(self.mode)
+
+ image_dto = context.images.save(image=converted_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_blur",
+ title="Blur Image",
+ tags=["image", "blur"],
+ category="image",
+ version="1.2.2",
+)
+class ImageBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Blurs an image"""
+
+ image: ImageField = InputField(description="The image to blur")
+ radius: float = InputField(default=8.0, ge=0, description="The blur radius")
+ # Metadata
+ blur_type: Literal["gaussian", "box"] = InputField(default="gaussian", description="The type of blur")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+
+ # Split the image into RGBA channels
+ r, g, b, a = image.split()
+
+ # Premultiply RGB channels by alpha
+ premultiplied_image = ImageChops.multiply(image, a.convert("RGBA"))
+ premultiplied_image.putalpha(a)
+
+ # Apply the blur
+ blur = (
+ ImageFilter.GaussianBlur(self.radius) if self.blur_type == "gaussian" else ImageFilter.BoxBlur(self.radius)
+ )
+ blurred_image = premultiplied_image.filter(blur)
+
+ # Split the blurred image into RGBA channels
+ r, g, b, a_orig = blurred_image.split()
+
+ # Convert to float using NumPy. float 32/64 division are much faster than float 16
+ r = numpy.array(r, dtype=numpy.float32)
+ g = numpy.array(g, dtype=numpy.float32)
+ b = numpy.array(b, dtype=numpy.float32)
+ a = numpy.array(a_orig, dtype=numpy.float32) / 255.0 # Normalize alpha to [0, 1]
+
+ # Unpremultiply RGB channels by alpha
+ r /= a + 1e-6 # Add a small epsilon to avoid division by zero
+ g /= a + 1e-6
+ b /= a + 1e-6
+
+ # Convert back to PIL images
+ r = Image.fromarray(numpy.uint8(numpy.clip(r, 0, 255)))
+ g = Image.fromarray(numpy.uint8(numpy.clip(g, 0, 255)))
+ b = Image.fromarray(numpy.uint8(numpy.clip(b, 0, 255)))
+
+ # Merge back into a single image
+ result_image = Image.merge("RGBA", (r, g, b, a_orig))
+
+ image_dto = context.images.save(image=result_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "unsharp_mask",
+ title="Unsharp Mask",
+ tags=["image", "unsharp_mask"],
+ category="image",
+ version="1.2.2",
+)
+class UnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Applies an unsharp mask filter to an image"""
+
+ image: ImageField = InputField(description="The image to use")
+ radius: float = InputField(gt=0, description="Unsharp mask radius", default=2)
+ strength: float = InputField(ge=0, description="Unsharp mask strength", default=50)
+
+ def pil_from_array(self, arr):
+ return Image.fromarray((arr * 255).astype("uint8"))
+
+ def array_from_pil(self, img):
+ return numpy.array(img) / 255
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ mode = image.mode
+
+ alpha_channel = _extract_alpha_channel(image)
+ image = image.convert("RGB")
+ image_blurred = self.array_from_pil(image.filter(ImageFilter.GaussianBlur(radius=self.radius)))
+
+ image = self.array_from_pil(image)
+ image += (image - image_blurred) * (self.strength / 100.0)
+ image = numpy.clip(image, 0, 1)
+ image = self.pil_from_array(image)
+
+ image = image.convert(mode)
+
+ # Make the image RGBA if we had a source alpha channel
+ if alpha_channel is not None:
+ image.putalpha(alpha_channel)
+
+ image_dto = context.images.save(image=image)
+
+ return ImageOutput(
+ image=ImageField(image_name=image_dto.image_name),
+ width=image.width,
+ height=image.height,
+ )
+
+
+@invocation(
+ "unsharp_mask_oklab",
+ title="Unsharp Mask (Oklab)",
+ tags=["image", "unsharp_mask", "oklab"],
+ category="image",
+ version="1.0.0",
+)
+class OklabUnsharpMaskInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Applies an unsharp mask filter to an image in the Oklab color space"""
+
+ image: ImageField = InputField(description="The image to use")
+ radius: float = InputField(gt=0, description="Unsharp mask radius", default=2)
+ strength: float = InputField(ge=0, description="Unsharp mask strength", default=50)
+
+ def pil_from_tensor(self, tensor: torch.Tensor) -> Image.Image:
+ array = torch.clamp(tensor, 0.0, 1.0).permute(1, 2, 0).cpu().numpy()
+ return Image.fromarray((array * 255).astype("uint8"))
+
+ def tensor_from_pil(self, img: Image.Image) -> torch.Tensor:
+ return torch.from_numpy(numpy.array(img, dtype=numpy.float32) / 255.0).permute(2, 0, 1)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ mode = image.mode
+
+ alpha_channel = _extract_alpha_channel(image)
+ image = image.convert("RGB")
+
+ image_blurred = self.tensor_from_pil(image.filter(ImageFilter.GaussianBlur(radius=self.radius)))
+ image_tensor = self.tensor_from_pil(image)
+
+ image_oklab = oklab_from_linear_srgb(linear_srgb_from_srgb(image_tensor))
+ image_blurred_oklab = oklab_from_linear_srgb(linear_srgb_from_srgb(image_blurred))
+
+ image_oklab[0, ...] += (image_oklab[0, ...] - image_blurred_oklab[0, ...]) * (self.strength / 100.0)
+ image_oklab = torch.clamp(image_oklab, -1.0, 1.0)
+
+ image = _restore_original_mode(
+ self.pil_from_tensor(srgb_from_linear_srgb(linear_srgb_from_oklab(image_oklab))),
+ mode,
+ alpha_channel,
+ )
+
+ image_dto = context.images.save(image=image)
+ return ImageOutput.build(image_dto)
+
+
+PIL_RESAMPLING_MODES = Literal[
+ "nearest",
+ "box",
+ "bilinear",
+ "hamming",
+ "bicubic",
+ "lanczos",
+]
+
+
+PIL_RESAMPLING_MAP = {
+ "nearest": Image.Resampling.NEAREST,
+ "box": Image.Resampling.BOX,
+ "bilinear": Image.Resampling.BILINEAR,
+ "hamming": Image.Resampling.HAMMING,
+ "bicubic": Image.Resampling.BICUBIC,
+ "lanczos": Image.Resampling.LANCZOS,
+}
+
+
+@invocation(
+ "img_resize",
+ title="Resize Image",
+ tags=["image", "resize"],
+ category="image",
+ version="1.2.2",
+)
+class ImageResizeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Resizes an image to specific dimensions"""
+
+ image: ImageField = InputField(description="The image to resize")
+ 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)")
+ resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ resample_mode = PIL_RESAMPLING_MAP[self.resample_mode]
+
+ resize_image = image.resize(
+ (self.width, self.height),
+ resample=resample_mode,
+ )
+
+ image_dto = context.images.save(image=resize_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_scale",
+ title="Scale Image",
+ tags=["image", "scale"],
+ category="image",
+ version="1.2.2",
+)
+class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Scales an image by a factor"""
+
+ image: ImageField = InputField(description="The image to scale")
+ scale_factor: float = InputField(
+ default=2.0,
+ gt=0,
+ description="The factor by which to scale the image",
+ )
+ resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ resample_mode = PIL_RESAMPLING_MAP[self.resample_mode]
+ width = int(image.width * self.scale_factor)
+ height = int(image.height * self.scale_factor)
+
+ resize_image = image.resize(
+ (width, height),
+ resample=resample_mode,
+ )
+
+ image_dto = context.images.save(image=resize_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_lerp",
+ title="Lerp Image",
+ tags=["image", "lerp"],
+ category="image",
+ version="1.2.2",
+)
+class ImageLerpInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Linear interpolation of all pixels of an image"""
+
+ image: ImageField = InputField(description="The image to lerp")
+ min: int = InputField(default=0, ge=0, le=255, description="The minimum output value")
+ max: int = InputField(default=255, ge=0, le=255, description="The maximum output value")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_arr = numpy.asarray(image, dtype=numpy.float32) / 255
+ image_arr = image_arr * (self.max - self.min) + self.min
+
+ lerp_image = Image.fromarray(numpy.uint8(image_arr))
+
+ image_dto = context.images.save(image=lerp_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_ilerp",
+ title="Inverse Lerp Image",
+ tags=["image", "ilerp"],
+ category="image",
+ version="1.2.2",
+)
+class ImageInverseLerpInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Inverse linear interpolation of all pixels of an image"""
+
+ image: ImageField = InputField(description="The image to lerp")
+ min: int = InputField(default=0, ge=0, le=255, description="The minimum input value")
+ max: int = InputField(default=255, ge=0, le=255, description="The maximum input value")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ 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]
+
+ ilerp_image = Image.fromarray(numpy.uint8(image_arr))
+
+ image_dto = context.images.save(image=ilerp_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_nsfw",
+ title="Blur NSFW Image",
+ tags=["image", "nsfw"],
+ category="image",
+ version="1.2.3",
+)
+class ImageNSFWBlurInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Add blur to NSFW-flagged images"""
+
+ image: ImageField = InputField(description="The image to check")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ logger = context.logger
+ logger.debug("Running NSFW checker")
+ image = SafetyChecker.blur_if_nsfw(image)
+
+ image_dto = context.images.save(image=image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_watermark",
+ title="Add Invisible Watermark",
+ tags=["image", "watermark"],
+ category="image",
+ version="1.2.2",
+)
+class ImageWatermarkInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Add an invisible watermark to an image"""
+
+ image: ImageField = InputField(description="The image to check")
+ text: str = InputField(default="InvokeAI", description="Watermark text")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ new_image = InvisibleWatermark.add_watermark(image, self.text)
+ image_dto = context.images.save(image=new_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "decode_watermark",
+ title="Decode Invisible Watermark",
+ tags=["image", "watermark"],
+ category="image",
+ version="1.0.0",
+)
+class DecodeInvisibleWatermarkInvocation(BaseInvocation):
+ """Decode an invisible watermark from an image."""
+
+ image: ImageField = InputField(description="The image to decode the watermark from")
+ length: int = InputField(default=8, description="The expected watermark length in bytes")
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ image = context.images.get_pil(self.image.image_name)
+ watermark = InvisibleWatermark.decode_watermark(image, self.length)
+ return StringOutput(value=watermark)
+
+
+@invocation(
+ "mask_edge",
+ title="Mask Edge",
+ tags=["image", "mask", "inpaint"],
+ category="mask",
+ version="1.2.2",
+)
+class MaskEdgeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Applies an edge mask to an image"""
+
+ image: ImageField = InputField(description="The image to apply the mask to")
+ edge_size: int = InputField(description="The size of the edge")
+ edge_blur: int = InputField(description="The amount of blur on the edge")
+ low_threshold: int = InputField(description="First threshold for the hysteresis procedure in Canny edge detection")
+ high_threshold: int = InputField(
+ description="Second threshold for the hysteresis procedure in Canny edge detection"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ mask = context.images.get_pil(self.image.image_name).convert("L")
+
+ npimg = numpy.asarray(mask, dtype=numpy.uint8)
+ npgradient = numpy.uint8(255 * (1.0 - numpy.floor(numpy.abs(0.5 - numpy.float32(npimg) / 255.0) * 2.0)))
+ npedge = cv2.Canny(npimg, threshold1=self.low_threshold, threshold2=self.high_threshold)
+ npmask = npgradient + npedge
+ npmask = cv2.dilate(npmask, numpy.ones((3, 3), numpy.uint8), iterations=int(self.edge_size / 2))
+
+ new_mask = Image.fromarray(npmask)
+
+ if self.edge_blur > 0:
+ new_mask = new_mask.filter(ImageFilter.BoxBlur(self.edge_blur))
+
+ new_mask = ImageOps.invert(new_mask)
+
+ image_dto = context.images.save(image=new_mask, image_category=ImageCategory.MASK)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "mask_combine",
+ title="Combine Masks",
+ tags=["image", "mask", "multiply"],
+ category="mask",
+ version="1.2.2",
+)
+class MaskCombineInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Combine two masks together by multiplying them using `PIL.ImageChops.multiply()`."""
+
+ mask1: ImageField = InputField(description="The first mask to combine")
+ mask2: ImageField = InputField(description="The second image to combine")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ mask1 = context.images.get_pil(self.mask1.image_name).convert("L")
+ mask2 = context.images.get_pil(self.mask2.image_name).convert("L")
+
+ combined_mask = ImageChops.multiply(mask1, mask2)
+
+ image_dto = context.images.save(image=combined_mask, image_category=ImageCategory.MASK)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "color_correct",
+ title="Color Correct",
+ tags=["image", "color"],
+ category="image",
+ version="2.0.0",
+)
+class ColorCorrectInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """
+ Matches the color histogram of a base image to a reference image, optionally
+ using a mask to only color-correct certain regions of the base image.
+ """
+
+ base_image: ImageField = InputField(description="The image to color-correct")
+ color_reference: ImageField = InputField(description="Reference image for color-correction")
+ mask: Optional[ImageField] = InputField(default=None, description="Optional mask to limit color correction area")
+ colorspace: Literal["RGB", "YCbCr", "YCbCr-Chroma", "YCbCr-Luma"] = InputField(
+ default="RGB", description="Colorspace in which to apply histogram matching", title="Color Space"
+ )
+
+ def _match_histogram_channel(self, source: numpy.ndarray, reference: numpy.ndarray) -> numpy.ndarray:
+ """Match histogram of source channel to reference channel using cumulative distribution functions."""
+ # Compute histograms
+ source_hist, _ = numpy.histogram(source.flatten(), bins=256, range=(0, 256))
+ reference_hist, _ = numpy.histogram(reference.flatten(), bins=256, range=(0, 256))
+
+ # Compute cumulative distribution functions
+ source_cdf = source_hist.cumsum()
+ reference_cdf = reference_hist.cumsum()
+
+ # Normalize CDFs (avoid division by zero)
+ if source_cdf[-1] > 0:
+ source_cdf = source_cdf / source_cdf[-1]
+ if reference_cdf[-1] > 0:
+ reference_cdf = reference_cdf / reference_cdf[-1]
+
+ # Create lookup table using linear interpolation
+ lookup_table = numpy.interp(source_cdf, reference_cdf, numpy.arange(256))
+
+ # Apply lookup table to source image
+ return lookup_table[source].astype(numpy.uint8)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Load images as RGBA
+ base_image = context.images.get_pil(self.base_image.image_name, "RGBA")
+
+ # Store original alpha channel
+ original_alpha = base_image.getchannel("A")
+
+ # Convert to working colorspace
+ if self.colorspace == "RGB":
+ base_array = numpy.asarray(base_image.convert("RGB"), dtype=numpy.uint8)
+ ref_rgb = context.images.get_pil(self.color_reference.image_name, "RGB")
+ ref_array = numpy.asarray(ref_rgb, dtype=numpy.uint8)
+ channels_to_match = [0, 1, 2] # R, G, B
+ else:
+ # Convert to YCbCr colorspace
+ base_ycbcr = base_image.convert("YCbCr")
+ ref_ycbcr = context.images.get_pil(self.color_reference.image_name, "YCbCr")
+
+ base_array = numpy.asarray(base_ycbcr, dtype=numpy.uint8)
+ ref_array = numpy.asarray(ref_ycbcr, dtype=numpy.uint8)
+
+ # Determine which channels to match based on mode
+ if self.colorspace == "YCbCr":
+ channels_to_match = [0, 1, 2] # Y, Cb, Cr
+ elif self.colorspace == "YCbCr-Chroma":
+ channels_to_match = [1, 2] # Cb, Cr only
+ else: # YCbCr-Luma
+ channels_to_match = [0] # Y only
+
+ # Apply histogram matching to selected channels
+ corrected_array = base_array.copy()
+ for channel_idx in channels_to_match:
+ corrected_array[:, :, channel_idx] = self._match_histogram_channel(
+ base_array[:, :, channel_idx], ref_array[:, :, channel_idx]
+ )
+
+ # Convert back to RGB if we were in YCbCr
+ if self.colorspace != "RGB":
+ corrected_image = Image.fromarray(corrected_array, mode="YCbCr").convert("RGB")
+ else:
+ corrected_image = Image.fromarray(corrected_array, mode="RGB")
+
+ # Apply mask if provided (white = original, black = result)
+ if self.mask is not None:
+ # Load mask as grayscale
+ mask_image = context.images.get_pil(self.mask.image_name, "L")
+ # Start with corrected image, paste base image where mask is white
+ result = corrected_image.copy()
+ if mask_image.size != result.size:
+ raise ValueError("Mask size must match base image size.")
+ else:
+ result.paste(base_image.convert("RGB"), mask=mask_image)
+ else:
+ result = corrected_image
+
+ # Convert to RGBA and restore original alpha
+ result = result.convert("RGBA")
+ result.putalpha(original_alpha)
+
+ # Save and return
+ image_dto = context.images.save(image=result)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_hue_adjust",
+ title="Adjust Image Hue",
+ tags=["image", "hue"],
+ category="image",
+ version="1.2.2",
+)
+class ImageHueAdjustmentInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Adjusts the Hue of an image."""
+
+ image: ImageField = InputField(description="The image to adjust")
+ hue: int = InputField(default=0, description="The degrees by which to rotate the hue, 0-360")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ pil_image = context.images.get_pil(self.image.image_name)
+
+ # Convert image to HSV color space
+ hsv_image = numpy.array(pil_image.convert("HSV"))
+
+ # Convert hue from 0..360 to 0..256
+ hue = int(256 * ((self.hue % 360) / 360))
+
+ # Increment each hue and wrap around at 255
+ hsv_image[:, :, 0] = (hsv_image[:, :, 0] + hue) % 256
+
+ # Convert back to PIL format and to original color mode
+ pil_image = Image.fromarray(hsv_image, mode="HSV").convert("RGBA")
+
+ image_dto = context.images.save(image=pil_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_hue_adjust_oklch",
+ title="Adjust Image Hue (Oklch)",
+ tags=["image", "hue", "oklch"],
+ category="image",
+ version="1.0.0",
+)
+class OklchImageHueAdjustmentInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Adjusts the hue of an image in Oklch space."""
+
+ image: ImageField = InputField(description="The image to adjust")
+ hue: int = InputField(default=0, description="The degrees by which to rotate the hue, 0-360")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+ mode = image.mode
+ alpha_channel = _extract_alpha_channel(image)
+
+ rgb = torch.from_numpy(numpy.asarray(image.convert("RGB"), dtype=numpy.float32) / 255.0).permute(2, 0, 1)
+ oklch = oklch_from_oklab(oklab_from_linear_srgb(linear_srgb_from_srgb(rgb)))
+ oklch[2, ...] = (oklch[2, ...] + self.hue) % 360.0
+
+ image = _restore_original_mode(
+ Image.fromarray(
+ (
+ torch.clamp(srgb_from_linear_srgb(linear_srgb_from_oklch(oklch)), 0.0, 1.0)
+ .permute(1, 2, 0)
+ .cpu()
+ .numpy()
+ * 255.0
+ ).astype(numpy.uint8),
+ mode="RGB",
+ ),
+ mode,
+ alpha_channel,
+ )
+
+ image_dto = context.images.save(image=image)
+ return ImageOutput.build(image_dto)
+
+
+COLOR_CHANNELS = Literal[
+ "Red (RGBA)",
+ "Green (RGBA)",
+ "Blue (RGBA)",
+ "Alpha (RGBA)",
+ "Cyan (CMYK)",
+ "Magenta (CMYK)",
+ "Yellow (CMYK)",
+ "Black (CMYK)",
+ "Hue (HSV)",
+ "Saturation (HSV)",
+ "Value (HSV)",
+ "Luminosity (LAB)",
+ "A (LAB)",
+ "B (LAB)",
+ "Y (YCbCr)",
+ "Cb (YCbCr)",
+ "Cr (YCbCr)",
+]
+
+CHANNEL_FORMATS = {
+ "Red (RGBA)": ("RGBA", 0),
+ "Green (RGBA)": ("RGBA", 1),
+ "Blue (RGBA)": ("RGBA", 2),
+ "Alpha (RGBA)": ("RGBA", 3),
+ "Cyan (CMYK)": ("CMYK", 0),
+ "Magenta (CMYK)": ("CMYK", 1),
+ "Yellow (CMYK)": ("CMYK", 2),
+ "Black (CMYK)": ("CMYK", 3),
+ "Hue (HSV)": ("HSV", 0),
+ "Saturation (HSV)": ("HSV", 1),
+ "Value (HSV)": ("HSV", 2),
+ "Luminosity (LAB)": ("LAB", 0),
+ "A (LAB)": ("LAB", 1),
+ "B (LAB)": ("LAB", 2),
+ "Y (YCbCr)": ("YCbCr", 0),
+ "Cb (YCbCr)": ("YCbCr", 1),
+ "Cr (YCbCr)": ("YCbCr", 2),
+}
+
+
+@invocation(
+ "img_channel_offset",
+ title="Offset Image Channel",
+ tags=[
+ "image",
+ "offset",
+ "red",
+ "green",
+ "blue",
+ "alpha",
+ "cyan",
+ "magenta",
+ "yellow",
+ "black",
+ "hue",
+ "saturation",
+ "luminosity",
+ "value",
+ ],
+ category="image",
+ version="1.2.3",
+)
+class ImageChannelOffsetInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Add or subtract a value from a specific color channel of an image."""
+
+ image: ImageField = InputField(description="The image to adjust")
+ channel: COLOR_CHANNELS = InputField(description="Which channel to adjust")
+ offset: int = InputField(default=0, ge=-255, le=255, description="The amount to adjust the channel by")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGBA")
+
+ # extract the channel and mode from the input and reference tuple
+ mode = CHANNEL_FORMATS[self.channel][0]
+ channel_number = CHANNEL_FORMATS[self.channel][1]
+
+ # Convert PIL image to new format
+ converted_image = numpy.array(image.convert(mode)).astype(int)
+ image_channel = converted_image[:, :, channel_number]
+
+ if self.channel == "Hue (HSV)":
+ # loop around the values because hue is special
+ image_channel = (image_channel + self.offset) % 256
+ else:
+ # Adjust the value, clipping to 0..255
+ image_channel = numpy.clip(image_channel + self.offset, 0, 255)
+
+ # Put the channel back into the image
+ converted_image[:, :, channel_number] = image_channel
+
+ # Convert back to RGBA format and output
+ pil_image = Image.fromarray(converted_image.astype(numpy.uint8), mode=mode).convert("RGBA")
+
+ # restore the alpha channel
+ if self.channel != "Alpha (RGBA)":
+ pil_image.putalpha(image.getchannel("A"))
+
+ image_dto = context.images.save(image=pil_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_channel_multiply",
+ title="Multiply Image Channel",
+ tags=[
+ "image",
+ "invert",
+ "scale",
+ "multiply",
+ "red",
+ "green",
+ "blue",
+ "alpha",
+ "cyan",
+ "magenta",
+ "yellow",
+ "black",
+ "hue",
+ "saturation",
+ "luminosity",
+ "value",
+ ],
+ category="image",
+ version="1.2.3",
+)
+class ImageChannelMultiplyInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Scale a specific color channel of an image."""
+
+ image: ImageField = InputField(description="The image to adjust")
+ channel: COLOR_CHANNELS = InputField(description="Which channel to adjust")
+ scale: float = InputField(default=1.0, ge=0.0, description="The amount to scale the channel by.")
+ invert_channel: bool = InputField(default=False, description="Invert the channel after scaling")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGBA")
+
+ # extract the channel and mode from the input and reference tuple
+ mode = CHANNEL_FORMATS[self.channel][0]
+ channel_number = CHANNEL_FORMATS[self.channel][1]
+
+ # Convert PIL image to new format
+ converted_image = numpy.array(image.convert(mode)).astype(float)
+ image_channel = converted_image[:, :, channel_number]
+
+ # Adjust the value, clipping to 0..255
+ image_channel = numpy.clip(image_channel * self.scale, 0, 255)
+
+ # Invert the channel if requested
+ if self.invert_channel:
+ image_channel = 255 - image_channel
+
+ # Put the channel back into the image
+ converted_image[:, :, channel_number] = image_channel
+
+ # Convert back to RGBA format and output
+ pil_image = Image.fromarray(converted_image.astype(numpy.uint8), mode=mode).convert("RGBA")
+
+ # restore the alpha channel
+ if self.channel != "Alpha (RGBA)":
+ pil_image.putalpha(image.getchannel("A"))
+
+ image_dto = context.images.save(image=pil_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "save_image",
+ title="Save Image",
+ tags=["primitives", "image"],
+ category="image",
+ version="1.2.2",
+ use_cache=False,
+)
+class SaveImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Saves an image. Unlike an image primitive, this invocation stores a copy of the image."""
+
+ image: ImageField = InputField(description=FieldDescriptions.image)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_dto = context.images.save(image=image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "save_image_to_file",
+ title="Save Image (Gallery + File Export)",
+ tags=["image", "export", "file", "save"],
+ category="image",
+ version="1.0.0",
+ use_cache=False,
+)
+class SaveImageToFileInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Saves an image to the gallery (like the standard Save Image node) AND additionally exports a copy
+ to the filesystem with a custom filename.
+
+ Filename pattern: {prefix}{uuid}{suffix}.{file_format}
+ - The UUID is the same UUID used for the gallery entry, so the exported file can be matched to the gallery item.
+ - The gallery entry itself always uses the plain UUID (prefix/suffix apply only to the exported file on disk).
+ - Board and Metadata inputs behave exactly like the standard Save Image node.
+ - The export target is restricted to (subfolders of) the InvokeAI outputs folder — absolute paths are rejected.
+
+ Example: prefix="hero_", suffix="_final", file_format="png" → "hero__final.png"
+ """
+
+ image: ImageField = InputField(description="The image to save and export")
+ output_directory: str = InputField(
+ default="",
+ description=(
+ "Target subdirectory (relative to the configured InvokeAI outputs folder) for the exported file. "
+ "Leave empty to use the outputs folder directly. "
+ "Example: 'my-exports' → /my-exports/. Nested paths like 'exports/2026' are allowed. "
+ "Absolute paths and path traversal ('..') are not allowed for security reasons. "
+ "The directory is created automatically if it doesn't exist."
+ ),
+ )
+ prefix: str = InputField(
+ default="",
+ description="Text prepended to the UUID in the exported filename. Example: 'portrait_' → 'portrait_.png'",
+ )
+ suffix: str = InputField(
+ default="",
+ description="Text appended to the UUID (before the extension). Example: '_v2' → '_v2.png'",
+ )
+ file_format: Literal["png", "jpg", "webp"] = InputField(
+ default="png",
+ description="File format for the exported file. PNG is lossless; JPG/WEBP are lossy and respect 'quality'.",
+ )
+ quality: int = InputField(
+ default=95,
+ ge=1,
+ le=100,
+ description="Compression quality for JPG and WEBP (1-100, higher = better quality, larger file). Ignored for PNG.",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_dto = context.images.save(image=image)
+
+ uuid = Path(image_dto.image_name).stem
+
+ outputs_path = context.config.get().outputs_path
+ assert outputs_path is not None
+
+ if not self.output_directory:
+ target_dir = outputs_path
+ else:
+ raw_str = self.output_directory
+ raw = Path(raw_str)
+ has_windows_drive = len(raw_str) >= 2 and raw_str[0].isalpha() and raw_str[1] == ":"
+ starts_with_sep = raw_str.startswith("/") or raw_str.startswith("\\")
+ if raw.is_absolute() or raw.drive or has_windows_drive or starts_with_sep:
+ raise ValueError(
+ f"Absolute paths are not allowed in output_directory: {raw_str!r}. "
+ "Use a path relative to the InvokeAI outputs folder."
+ )
+ candidate = (outputs_path / raw).resolve()
+ outputs_resolved = outputs_path.resolve()
+ if outputs_resolved != candidate and outputs_resolved not in candidate.parents:
+ raise ValueError(f"output_directory must stay within the outputs folder: {raw_str!r}")
+ target_dir = candidate
+
+ target_dir.mkdir(parents=True, exist_ok=True)
+
+ filename = f"{self.prefix}{uuid}{self.suffix}.{self.file_format}"
+ target_path = target_dir / filename
+
+ if self.file_format == "png":
+ image.save(target_path, format="PNG")
+ elif self.file_format == "jpg":
+ if image.mode in ("RGBA", "LA", "P"):
+ image = image.convert("RGB")
+ image.save(target_path, format="JPEG", quality=self.quality)
+ else:
+ image.save(target_path, format="WEBP", quality=self.quality)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "canvas_paste_back",
+ title="Canvas Paste Back",
+ tags=["image", "combine"],
+ category="canvas",
+ version="1.0.1",
+)
+class CanvasPasteBackInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Combines two images by using the mask provided. Intended for use on the Unified Canvas."""
+
+ source_image: ImageField = InputField(description="The source image")
+ target_image: ImageField = InputField(description="The target image")
+ mask: ImageField = InputField(
+ description="The mask to use when pasting",
+ )
+ mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
+
+ def _prepare_mask(self, mask: Image.Image) -> Image.Image:
+ mask_array = numpy.array(mask)
+ kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8)
+ dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3)
+ dilated_mask = Image.fromarray(dilated_mask_array)
+ if self.mask_blur > 0:
+ mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur))
+ return ImageOps.invert(mask.convert("L"))
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ source_image = context.images.get_pil(self.source_image.image_name)
+ target_image = context.images.get_pil(self.target_image.image_name)
+ mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
+
+ source_image.paste(target_image, (0, 0), mask)
+
+ image_dto = context.images.save(image=source_image)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "mask_from_id",
+ title="Mask from Segmented Image",
+ tags=["image", "mask", "id"],
+ category="mask",
+ version="1.0.1",
+)
+class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generate a mask for a particular color in an ID Map"""
+
+ image: ImageField = InputField(description="The image to create the mask from")
+ color: ColorField = InputField(description="ID color to mask")
+ threshold: int = InputField(default=100, description="Threshold for color detection")
+ invert: bool = InputField(default=False, description="Whether or not to invert the mask")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+
+ np_color = numpy.array(self.color.tuple())
+
+ # Maybe there's a faster way to calculate this distance but I can't think of any right now.
+ color_distance = numpy.linalg.norm(image - np_color, axis=-1)
+
+ # Create a mask based on the threshold and the distance calculated above
+ binary_mask = (color_distance < self.threshold).astype(numpy.uint8) * 255
+
+ # Convert the mask back to PIL
+ binary_mask_pil = Image.fromarray(binary_mask)
+
+ if self.invert:
+ binary_mask_pil = ImageOps.invert(binary_mask_pil)
+
+ image_dto = context.images.save(image=binary_mask_pil, image_category=ImageCategory.MASK)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "canvas_v2_mask_and_crop",
+ title="Canvas V2 Mask and Crop",
+ tags=["image", "mask", "id"],
+ category="canvas",
+ version="1.0.0",
+ classification=Classification.Deprecated,
+)
+class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Handles Canvas V2 image output masking and cropping"""
+
+ source_image: ImageField | None = InputField(
+ default=None,
+ description="The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.",
+ )
+ generated_image: ImageField = InputField(description="The image to apply the mask to")
+ mask: ImageField = InputField(description="The mask to apply")
+ mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
+
+ def _prepare_mask(self, mask: Image.Image) -> Image.Image:
+ mask_array = numpy.array(mask)
+ kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8)
+ dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3)
+ dilated_mask = Image.fromarray(dilated_mask_array)
+ if self.mask_blur > 0:
+ mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur))
+ return ImageOps.invert(mask.convert("L"))
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
+
+ if self.source_image:
+ generated_image = context.images.get_pil(self.generated_image.image_name)
+ source_image = context.images.get_pil(self.source_image.image_name)
+ source_image.paste(generated_image, (0, 0), mask)
+ image_dto = context.images.save(image=source_image)
+ else:
+ generated_image = context.images.get_pil(self.generated_image.image_name)
+ generated_image.putalpha(mask)
+ image_dto = context.images.save(image=generated_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "expand_mask_with_fade", title="Expand Mask with Fade", tags=["image", "mask"], category="mask", version="1.0.1"
+)
+class ExpandMaskWithFadeInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Expands a mask with a fade effect. The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
+ The mask is thresholded to create a binary mask, and then a distance transform is applied to create a fade effect.
+ The fade size is specified in pixels, and the mask is expanded by that amount. The result is a mask with a smooth transition from black to white.
+ If the fade size is 0, the mask is returned as-is.
+ """
+
+ mask: ImageField = InputField(description="The mask to expand")
+ threshold: int = InputField(default=0, ge=0, le=255, description="The threshold for the binary mask (0-255)")
+ fade_size_px: int = InputField(default=32, ge=0, description="The size of the fade in pixels")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ pil_mask = context.images.get_pil(self.mask.image_name, mode="L")
+
+ if self.fade_size_px == 0:
+ # If the fade size is 0, just return the mask as-is.
+ image_dto = context.images.save(image=pil_mask, image_category=ImageCategory.MASK)
+ return ImageOutput.build(image_dto)
+
+ np_mask = numpy.array(pil_mask)
+
+ # Threshold the mask to create a binary mask - 0 for black, 255 for white
+ # If we don't threshold we can get some weird artifacts
+ np_mask = numpy.where(np_mask > self.threshold, 255, 0).astype(numpy.uint8)
+
+ # Create a mask for the black region (1 where black, 0 otherwise)
+ black_mask = (np_mask == 0).astype(numpy.uint8)
+
+ # Invert the black region
+ bg_mask = 1 - black_mask
+
+ # Create a distance transform of the inverted mask
+ dist = cv2.distanceTransform(bg_mask, cv2.DIST_L2, 5)
+
+ # Normalize distances so that pixels = 1.0, 1.0, feather)
+
+ # Clip any other values to ensure they're in the valid range [0,1]
+ feather = numpy.clip(feather, 0, 1)
+
+ # Build final image.
+ np_result = numpy.where(black_mask == 1, 0, (feather * 255).astype(numpy.uint8))
+
+ # Convert back to PIL, grayscale
+ pil_result = Image.fromarray(np_result.astype(numpy.uint8), mode="L")
+
+ image_dto = context.images.save(image=pil_result, image_category=ImageCategory.MASK)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "apply_mask_to_image",
+ title="Apply Mask to Image",
+ tags=["image", "mask", "blend"],
+ category="mask",
+ version="1.0.0",
+)
+class ApplyMaskToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """
+ Extracts a region from a generated image using a mask and blends it seamlessly onto a source image.
+ The mask uses black to indicate areas to keep from the generated image and white for areas to discard.
+ """
+
+ image: ImageField = InputField(description="The image from which to extract the masked region")
+ mask: ImageField = InputField(description="The mask defining the region (black=keep, white=discard)")
+ invert_mask: bool = InputField(
+ default=False,
+ description="Whether to invert the mask before applying it",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Load images
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+ mask = context.images.get_pil(self.mask.image_name, mode="L")
+
+ if self.invert_mask:
+ # Invert the mask if requested
+ mask = ImageOps.invert(mask.copy())
+
+ # Combine the mask as the alpha channel of the image
+ r, g, b, _ = image.split() # Split the image into RGB and alpha channels
+ result_image = Image.merge("RGBA", (r, g, b, mask)) # Use the mask as the new alpha channel
+
+ # Save the resulting image
+ image_dto = context.images.save(image=result_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "img_noise",
+ title="Add Image Noise",
+ tags=["image", "noise"],
+ category="image",
+ version="1.1.0",
+)
+class ImageNoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Add noise to an image"""
+
+ image: ImageField = InputField(description="The image to add noise to")
+ mask: Optional[ImageField] = InputField(
+ default=None, description="Optional mask determining where to apply noise (black=noise, white=no noise)"
+ )
+ seed: int = InputField(
+ default=0,
+ ge=0,
+ le=SEED_MAX,
+ description=FieldDescriptions.seed,
+ )
+ noise_type: Literal["gaussian", "salt_and_pepper"] = InputField(
+ default="gaussian",
+ description="The type of noise to add",
+ )
+ amount: float = InputField(default=0.1, ge=0, le=1, description="The amount of noise to add")
+ noise_color: bool = InputField(default=True, description="Whether to add colored noise")
+ size: int = InputField(default=1, ge=1, description="The size of the noise points")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+
+ # Save out the alpha channel
+ alpha = image.getchannel("A")
+
+ # Set the seed for numpy random
+ rs = numpy.random.RandomState(numpy.random.MT19937(numpy.random.SeedSequence(self.seed)))
+
+ if self.noise_type == "gaussian":
+ if self.noise_color:
+ noise = rs.normal(0, 1, (image.height // self.size, image.width // self.size, 3)) * 255
+ else:
+ noise = rs.normal(0, 1, (image.height // self.size, image.width // self.size)) * 255
+ noise = numpy.stack([noise] * 3, axis=-1)
+ elif self.noise_type == "salt_and_pepper":
+ if self.noise_color:
+ noise = rs.choice(
+ [0, 255], (image.height // self.size, image.width // self.size, 3), p=[1 - self.amount, self.amount]
+ )
+ else:
+ noise = rs.choice(
+ [0, 255], (image.height // self.size, image.width // self.size), p=[1 - self.amount, self.amount]
+ )
+ noise = numpy.stack([noise] * 3, axis=-1)
+
+ noise = Image.fromarray(noise.astype(numpy.uint8), mode="RGB").resize(
+ (image.width, image.height), Image.Resampling.NEAREST
+ )
+
+ # Create a noisy version of the input image
+ noisy_image = Image.blend(image.convert("RGB"), noise, self.amount).convert("RGBA")
+
+ # Apply mask if provided
+ if self.mask is not None:
+ mask_image = context.images.get_pil(self.mask.image_name, mode="L")
+
+ if mask_image.size != image.size:
+ mask_image = mask_image.resize(image.size, Image.Resampling.LANCZOS)
+
+ result_image = image.copy()
+ mask_image = ImageOps.invert(mask_image)
+ result_image.paste(noisy_image, (0, 0), mask=mask_image)
+ else:
+ result_image = noisy_image
+
+ # Paste back the alpha channel from the original image
+ result_image.putalpha(alpha)
+
+ image_dto = context.images.save(image=result_image)
+
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "crop_image_to_bounding_box",
+ title="Crop Image to Bounding Box",
+ category="image",
+ version="1.0.0",
+ tags=["image", "crop"],
+)
+class CropImageToBoundingBoxInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Crop an image to the given bounding box. If the bounding box is omitted, the image is cropped to the non-transparent pixels."""
+
+ image: ImageField = InputField(description="The image to crop")
+ bounding_box: BoundingBoxField | None = InputField(
+ default=None, description="The bounding box to crop the image to"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ bounding_box = self.bounding_box.tuple() if self.bounding_box is not None else image.getbbox()
+
+ cropped_image = image.crop(bounding_box)
+
+ image_dto = context.images.save(image=cropped_image)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "paste_image_into_bounding_box",
+ title="Paste Image into Bounding Box",
+ category="image",
+ version="1.0.0",
+ tags=["image", "crop"],
+)
+class PasteImageIntoBoundingBoxInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Paste the source image into the target image at the given bounding box.
+
+ The source image must be the same size as the bounding box, and the bounding box must fit within the target image."""
+
+ source_image: ImageField = InputField(description="The image to paste")
+ target_image: ImageField = InputField(description="The image to paste into")
+ bounding_box: BoundingBoxField = InputField(description="The bounding box to paste the image into")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ source_image = context.images.get_pil(self.source_image.image_name, mode="RGBA")
+ target_image = context.images.get_pil(self.target_image.image_name, mode="RGBA")
+
+ bounding_box = self.bounding_box.tuple()
+
+ target_image.paste(source_image, bounding_box, source_image)
+
+ image_dto = context.images.save(image=target_image)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "flux_kontext_image_prep",
+ title="FLUX Kontext Image Prep",
+ tags=["image", "concatenate", "flux", "kontext"],
+ category="conditioning",
+ version="1.0.0",
+)
+class FluxKontextConcatenateImagesInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Prepares an image or images for use with FLUX Kontext. The first/single image is resized to the nearest
+ preferred Kontext resolution. All other images are concatenated horizontally, maintaining their aspect ratio."""
+
+ images: list[ImageField] = InputField(
+ description="The images to concatenate",
+ min_length=1,
+ max_length=10,
+ )
+
+ use_preferred_resolution: bool = InputField(
+ default=True, description="Use FLUX preferred resolutions for the first image"
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ from invokeai.backend.flux.util import PREFERED_KONTEXT_RESOLUTIONS
+
+ # Step 1: Load all images
+ pil_images = []
+ for image_field in self.images:
+ image = context.images.get_pil(image_field.image_name, mode="RGBA")
+ pil_images.append(image)
+
+ # Step 2: Determine target resolution for the first image
+ first_image = pil_images[0]
+ width, height = first_image.size
+
+ if self.use_preferred_resolution:
+ aspect_ratio = width / height
+
+ # Find the closest preferred resolution for the first image
+ _, target_width, target_height = min(
+ ((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS), key=lambda x: x[0]
+ )
+
+ # Apply BFL's scaling formula
+ scaled_height = 2 * int(target_height / 16)
+ final_height = 8 * scaled_height # This will be consistent for all images
+ scaled_width = 2 * int(target_width / 16)
+ first_width = 8 * scaled_width
+ else:
+ # Use original dimensions of first image, ensuring divisibility by 16
+ final_height = 16 * (height // 16)
+ first_width = 16 * (width // 16)
+ # Ensure minimum dimensions
+ if final_height < 16:
+ final_height = 16
+ if first_width < 16:
+ first_width = 16
+
+ # Step 3: Process and resize all images with consistent height
+ processed_images = []
+ total_width = 0
+
+ for i, image in enumerate(pil_images):
+ if i == 0:
+ # First image uses the calculated dimensions
+ final_width = first_width
+ else:
+ # Subsequent images maintain aspect ratio with the same height
+ img_aspect_ratio = image.width / image.height
+ # Calculate width that maintains aspect ratio at the target height
+ calculated_width = int(final_height * img_aspect_ratio)
+ # Ensure width is divisible by 16 for proper VAE encoding
+ final_width = 16 * (calculated_width // 16)
+ # Ensure minimum width
+ if final_width < 16:
+ final_width = 16
+
+ # Resize image to calculated dimensions
+ resized_image = image.resize((final_width, final_height), Image.Resampling.LANCZOS)
+ processed_images.append(resized_image)
+ total_width += final_width
+
+ # Step 4: Concatenate images horizontally
+ concatenated_image = Image.new("RGB", (total_width, final_height))
+ x_offset = 0
+ for img in processed_images:
+ concatenated_image.paste(img, (x_offset, 0))
+ x_offset += img.width
+
+ # Save the concatenated image
+ image_dto = context.images.save(image=concatenated_image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/image_panels.py b/invokeai/app/invocations/image_panels.py
new file mode 100644
index 00000000000..71fefbd1c6a
--- /dev/null
+++ b/invokeai/app/invocations/image_panels.py
@@ -0,0 +1,59 @@
+from pydantic import ValidationInfo, field_validator
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import InputField, OutputField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("image_panel_coordinate_output")
+class ImagePanelCoordinateOutput(BaseInvocationOutput):
+ x_left: int = OutputField(description="The left x-coordinate of the panel.")
+ y_top: int = OutputField(description="The top y-coordinate of the panel.")
+ width: int = OutputField(description="The width of the panel.")
+ height: int = OutputField(description="The height of the panel.")
+
+
+@invocation(
+ "image_panel_layout",
+ title="Image Panel Layout",
+ tags=["image", "panel", "layout"],
+ category="canvas",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class ImagePanelLayoutInvocation(BaseInvocation):
+ """Get the coordinates of a single panel in a grid. (If the full image shape cannot be divided evenly into panels,
+ then the grid may not cover the entire image.)
+ """
+
+ width: int = InputField(description="The width of the entire grid.")
+ height: int = InputField(description="The height of the entire grid.")
+ num_cols: int = InputField(ge=1, default=1, description="The number of columns in the grid.")
+ num_rows: int = InputField(ge=1, default=1, description="The number of rows in the grid.")
+ panel_col_idx: int = InputField(ge=0, default=0, description="The column index of the panel to be processed.")
+ panel_row_idx: int = InputField(ge=0, default=0, description="The row index of the panel to be processed.")
+
+ @field_validator("panel_col_idx")
+ def validate_panel_col_idx(cls, v: int, info: ValidationInfo) -> int:
+ if v < 0 or v >= info.data["num_cols"]:
+ raise ValueError(f"panel_col_idx must be between 0 and {info.data['num_cols'] - 1}")
+ return v
+
+ @field_validator("panel_row_idx")
+ def validate_panel_row_idx(cls, v: int, info: ValidationInfo) -> int:
+ if v < 0 or v >= info.data["num_rows"]:
+ raise ValueError(f"panel_row_idx must be between 0 and {info.data['num_rows'] - 1}")
+ return v
+
+ def invoke(self, context: InvocationContext) -> ImagePanelCoordinateOutput:
+ x_left = self.panel_col_idx * (self.width // self.num_cols)
+ y_top = self.panel_row_idx * (self.height // self.num_rows)
+ width = self.width // self.num_cols
+ height = self.height // self.num_rows
+ return ImagePanelCoordinateOutput(x_left=x_left, y_top=y_top, width=width, height=height)
diff --git a/invokeai/app/invocations/image_to_latents.py b/invokeai/app/invocations/image_to_latents.py
new file mode 100644
index 00000000000..8dc5ceba0b0
--- /dev/null
+++ b/invokeai/app/invocations/image_to_latents.py
@@ -0,0 +1,182 @@
+from contextlib import nullcontext
+from functools import singledispatchmethod
+from typing import Literal
+
+import einops
+import torch
+from diffusers.models.attention_processor import (
+ AttnProcessor2_0,
+ LoRAAttnProcessor2_0,
+ LoRAXFormersAttnProcessor,
+ XFormersAttnProcessor,
+)
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+)
+from invokeai.app.invocations.model import BaseModelType, VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
+
+"""
+SDXL VAE color compensation values determined experimentally to reduce color drift.
+If more reliable values are found in the future (e.g. individual color channels), they can be updated.
+SD1.5, TAESD, TAESDXL VAEs distort in less predictable ways, so no compensation is offered at this time.
+"""
+COMPENSATION_OPTIONS = Literal["None", "SDXL"]
+COLOR_COMPENSATION_MAP = {"None": [1, 0], "SDXL": [1.015, -0.002]}
+
+
+@invocation(
+ "i2l",
+ title="Image to Latents - SD1.5, SDXL",
+ tags=["latents", "image", "vae", "i2l"],
+ category="latents",
+ version="1.2.0",
+)
+class ImageToLatentsInvocation(BaseInvocation):
+ """Encodes an image into latents."""
+
+ image: ImageField = InputField(
+ description="The image to encode",
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+ tiled: bool = InputField(default=False, description=FieldDescriptions.tiled)
+ # NOTE: tile_size = 0 is a special value. We use this rather than `int | None`, because the workflow UI does not
+ # offer a way to directly set None values.
+ tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
+ fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
+ color_compensation: COMPENSATION_OPTIONS = InputField(
+ default="None",
+ description="Apply VAE scaling compensation when encoding images (reduces color drift).",
+ )
+
+ @classmethod
+ def vae_encode(
+ cls,
+ vae_info: LoadedModel,
+ upcast: bool,
+ tiled: bool,
+ image_tensor: torch.Tensor,
+ tile_size: int = 0,
+ ) -> torch.Tensor:
+ assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
+ estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
+ operation="encode",
+ image_tensor=image_tensor,
+ vae=vae_info.model,
+ tile_size=tile_size if tiled else None,
+ fp32=upcast,
+ )
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
+ orig_dtype = vae.dtype
+ if upcast:
+ vae.to(dtype=torch.float32)
+
+ use_torch_2_0_or_xformers = hasattr(vae.decoder, "mid_block") and isinstance(
+ vae.decoder.mid_block.attentions[0].processor,
+ (
+ AttnProcessor2_0,
+ XFormersAttnProcessor,
+ LoRAXFormersAttnProcessor,
+ LoRAAttnProcessor2_0,
+ ),
+ )
+ # if xformers or torch_2_0 is used attention block does not need
+ # to be in float32 which can save lots of memory
+ if use_torch_2_0_or_xformers:
+ vae.post_quant_conv.to(orig_dtype)
+ vae.decoder.conv_in.to(orig_dtype)
+ vae.decoder.mid_block.to(orig_dtype)
+ # else:
+ # latents = latents.float()
+
+ else:
+ vae.to(dtype=torch.float16)
+ # latents = latents.half()
+
+ if tiled:
+ vae.enable_tiling()
+ else:
+ vae.disable_tiling()
+
+ tiling_context = nullcontext()
+ if tile_size > 0:
+ tiling_context = patch_vae_tiling_params(
+ vae,
+ tile_sample_min_size=tile_size,
+ tile_latent_min_size=tile_size // LATENT_SCALE_FACTOR,
+ tile_overlap_factor=0.25,
+ )
+
+ # non_noised_latents_from_image
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype)
+ with torch.inference_mode(), tiling_context:
+ latents = ImageToLatentsInvocation._encode_to_tensor(vae, image_tensor)
+
+ latents = vae.config.scaling_factor * latents
+ latents = latents.to(dtype=orig_dtype)
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+
+ if self.color_compensation != "None" and vae_info.config.base == BaseModelType.StableDiffusionXL:
+ scale, bias = COLOR_COMPENSATION_MAP[self.color_compensation]
+ image_tensor = image_tensor * scale + bias
+
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ context.util.signal_progress("Running VAE encoder")
+ latents = self.vae_encode(
+ vae_info=vae_info,
+ upcast=self.fp32,
+ tiled=self.tiled or context.config.get().force_tiled_decode,
+ image_tensor=image_tensor,
+ tile_size=self.tile_size,
+ )
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ @singledispatchmethod
+ @staticmethod
+ def _encode_to_tensor(vae: AutoencoderKL, image_tensor: torch.FloatTensor) -> torch.FloatTensor:
+ assert isinstance(vae, torch.nn.Module)
+ image_tensor_dist = vae.encode(image_tensor).latent_dist
+ latents: torch.Tensor = image_tensor_dist.sample().to(
+ dtype=vae.dtype
+ ) # FIXME: uses torch.randn. make reproducible!
+ return latents
+
+ @_encode_to_tensor.register
+ @staticmethod
+ def _(vae: AutoencoderTiny, image_tensor: torch.FloatTensor) -> torch.FloatTensor:
+ assert isinstance(vae, torch.nn.Module)
+ latents: torch.FloatTensor = vae.encode(image_tensor).latents
+ return latents
diff --git a/invokeai/app/invocations/infill.py b/invokeai/app/invocations/infill.py
new file mode 100644
index 00000000000..dd7b2c87b0a
--- /dev/null
+++ b/invokeai/app/invocations/infill.py
@@ -0,0 +1,173 @@
+from abc import abstractmethod
+from typing import Literal, get_args
+
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ColorField, ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.image import PIL_RESAMPLING_MAP, PIL_RESAMPLING_MODES
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.misc import SEED_MAX
+from invokeai.backend.image_util.infill_methods.cv2_inpaint import cv2_inpaint
+from invokeai.backend.image_util.infill_methods.lama import LaMA
+from invokeai.backend.image_util.infill_methods.mosaic import infill_mosaic
+from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch, infill_patchmatch
+from invokeai.backend.image_util.infill_methods.tile import infill_tile
+from invokeai.backend.util.logging import InvokeAILogger
+
+logger = InvokeAILogger.get_logger()
+
+
+def get_infill_methods():
+ methods = Literal["tile", "color", "lama", "cv2"] # TODO: add mosaic back
+ if PatchMatch.patchmatch_available():
+ methods = Literal["patchmatch", "tile", "color", "lama", "cv2"] # TODO: add mosaic back
+ return methods
+
+
+INFILL_METHODS = get_infill_methods()
+DEFAULT_INFILL_METHOD = "patchmatch" if "patchmatch" in get_args(INFILL_METHODS) else "tile"
+
+
+class InfillImageProcessorInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Base class for invocations that preprocess images for Infilling"""
+
+ image: ImageField = InputField(description="The image to process")
+
+ @abstractmethod
+ def infill(self, image: Image.Image) -> Image.Image:
+ """Infill the image with the specified method"""
+ pass
+
+ def load_image(self) -> tuple[Image.Image, bool]:
+ """Process the image to have an alpha channel before being infilled"""
+ image = self._context.images.get_pil(self.image.image_name)
+ has_alpha = True if image.mode == "RGBA" else False
+ return image, has_alpha
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ self._context = context
+ # Retrieve and process image to be infilled
+ input_image, has_alpha = self.load_image()
+
+ # If the input image has no alpha channel, return it
+ if has_alpha is False:
+ return ImageOutput.build(context.images.get_dto(self.image.image_name))
+
+ # Perform Infill action
+ infilled_image = self.infill(input_image)
+
+ # Create ImageDTO for Infilled Image
+ infilled_image_dto = context.images.save(image=infilled_image)
+
+ # Return Infilled Image
+ return ImageOutput.build(infilled_image_dto)
+
+
+@invocation("infill_rgba", title="Solid Color Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2")
+class InfillColorInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image with a solid color"""
+
+ color: ColorField = InputField(
+ default=ColorField(r=127, g=127, b=127, a=255),
+ description="The color to use to infill",
+ )
+
+ def infill(self, image: Image.Image):
+ solid_bg = Image.new("RGBA", image.size, self.color.tuple())
+ infilled = Image.alpha_composite(solid_bg, image.convert("RGBA"))
+ infilled.paste(image, (0, 0), image.split()[-1])
+ return infilled
+
+
+@invocation("infill_tile", title="Tile Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.3")
+class InfillTileInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image with tiles of the image"""
+
+ tile_size: int = InputField(default=32, ge=1, description="The tile size (px)")
+ seed: int = InputField(
+ default=0,
+ ge=0,
+ le=SEED_MAX,
+ description="The seed to use for tile generation (omit for random)",
+ )
+
+ def infill(self, image: Image.Image):
+ output = infill_tile(image, seed=self.seed, tile_size=self.tile_size)
+ return output.infilled
+
+
+@invocation(
+ "infill_patchmatch", title="PatchMatch Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2"
+)
+class InfillPatchMatchInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image using the PatchMatch algorithm"""
+
+ downscale: float = InputField(default=2.0, gt=0, description="Run patchmatch on downscaled image to speedup infill")
+ resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode")
+
+ def infill(self, image: Image.Image):
+ resample_mode = PIL_RESAMPLING_MAP[self.resample_mode]
+
+ width = int(image.width / self.downscale)
+ height = int(image.height / self.downscale)
+
+ infilled = image.resize(
+ (width, height),
+ resample=resample_mode,
+ )
+ infilled = infill_patchmatch(image)
+ infilled = infilled.resize(
+ (image.width, image.height),
+ resample=resample_mode,
+ )
+ infilled.paste(image, (0, 0), mask=image.split()[-1])
+
+ return infilled
+
+
+LAMA_MODEL_URL = "https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt"
+
+
+@invocation("infill_lama", title="LaMa Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2")
+class LaMaInfillInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image using the LaMa model"""
+
+ def infill(self, image: Image.Image):
+ with self._context.models.load_remote_model(
+ source=LAMA_MODEL_URL,
+ loader=LaMA.load_jit_model,
+ ) as model:
+ lama = LaMA(model)
+ return lama(image)
+
+
+@invocation("infill_cv2", title="CV2 Infill", tags=["image", "inpaint"], category="inpaint", version="1.2.2")
+class CV2InfillInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image using OpenCV Inpainting"""
+
+ def infill(self, image: Image.Image):
+ return cv2_inpaint(image)
+
+
+# @invocation(
+# "infill_mosaic", title="Mosaic Infill", tags=["image", "inpaint", "outpaint"], category="inpaint", version="1.0.0"
+# )
+class MosaicInfillInvocation(InfillImageProcessorInvocation):
+ """Infills transparent areas of an image with a mosaic pattern drawing colors from the rest of the image"""
+
+ image: ImageField = InputField(description="The image to infill")
+ tile_width: int = InputField(default=64, description="Width of the tile")
+ tile_height: int = InputField(default=64, description="Height of the tile")
+ min_color: ColorField = InputField(
+ default=ColorField(r=0, g=0, b=0, a=255),
+ description="The min threshold for color",
+ )
+ max_color: ColorField = InputField(
+ default=ColorField(r=255, g=255, b=255, a=255),
+ description="The max threshold for color",
+ )
+
+ def infill(self, image: Image.Image):
+ return infill_mosaic(image, (self.tile_width, self.tile_height), self.min_color.tuple(), self.max_color.tuple())
diff --git a/invokeai/app/invocations/ip_adapter.py b/invokeai/app/invocations/ip_adapter.py
new file mode 100644
index 00000000000..711f910d587
--- /dev/null
+++ b/invokeai/app/invocations/ip_adapter.py
@@ -0,0 +1,232 @@
+from builtins import float
+from typing import List, Literal, Optional, Union
+
+from pydantic import BaseModel, Field, field_validator, model_validator
+from typing_extensions import Self
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageField
+from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
+from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.configs.ip_adapter import (
+ IPAdapter_Checkpoint_Config_Base,
+ IPAdapter_InvokeAI_Config_Base,
+)
+from invokeai.backend.model_manager.starter_models import (
+ StarterModel,
+ clip_vit_l_image_encoder,
+ ip_adapter_sd_image_encoder,
+ ip_adapter_sdxl_image_encoder,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+class IPAdapterField(BaseModel):
+ image: Union[ImageField, List[ImageField]] = Field(description="The IP-Adapter image prompt(s).")
+ ip_adapter_model: ModelIdentifierField = Field(description="The IP-Adapter model to use.")
+ image_encoder_model: ModelIdentifierField = Field(description="The name of the CLIP image encoder model.")
+ weight: Union[float, List[float]] = Field(default=1, description="The weight given to the IP-Adapter.")
+ target_blocks: List[str] = Field(default=[], description="The IP Adapter blocks to apply")
+ method: str = Field(default="full", description="Weight apply method")
+ 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)"
+ )
+ mask: Optional[TensorField] = Field(
+ default=None,
+ description="The bool mask associated with this IP-Adapter. Excluded regions should be set to False, included "
+ "regions should be set to True.",
+ )
+
+ @field_validator("weight")
+ @classmethod
+ def validate_ip_adapter_weight(cls, v: float) -> float:
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self) -> Self:
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+
+@invocation_output("ip_adapter_output")
+class IPAdapterOutput(BaseInvocationOutput):
+ # Outputs
+ ip_adapter: IPAdapterField = OutputField(description=FieldDescriptions.ip_adapter, title="IP-Adapter")
+
+
+CLIP_VISION_MODEL_MAP: dict[Literal["ViT-L", "ViT-H", "ViT-G"], StarterModel] = {
+ "ViT-L": clip_vit_l_image_encoder,
+ "ViT-H": ip_adapter_sd_image_encoder,
+ "ViT-G": ip_adapter_sdxl_image_encoder,
+}
+
+
+@invocation(
+ "ip_adapter",
+ title="IP-Adapter - SD1.5, SDXL",
+ tags=["ip_adapter", "control"],
+ category="conditioning",
+ version="1.5.1",
+)
+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).", ui_order=1)
+ ip_adapter_model: ModelIdentifierField = InputField(
+ description="The IP-Adapter model.",
+ title="IP-Adapter Model",
+ ui_order=-1,
+ ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL],
+ ui_model_type=ModelType.IPAdapter,
+ )
+ clip_vision_model: Literal["ViT-H", "ViT-G", "ViT-L"] = InputField(
+ description="CLIP Vision model to use. Overrides model settings. Mandatory for checkpoint models.",
+ default="ViT-H",
+ ui_order=2,
+ )
+ weight: Union[float, List[float]] = InputField(
+ default=1, description="The weight given to the IP-Adapter", title="Weight"
+ )
+ method: Literal["full", "style", "composition", "style_strong", "style_precise"] = InputField(
+ default="full", description="The method to apply the IP-Adapter"
+ )
+ begin_step_percent: float = InputField(
+ default=0, ge=0, le=1, 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)"
+ )
+ mask: Optional[TensorField] = InputField(
+ default=None, description="A mask defining the region that this IP-Adapter applies to."
+ )
+
+ @field_validator("weight")
+ @classmethod
+ def validate_ip_adapter_weight(cls, v: float) -> float:
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self) -> Self:
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+ 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.models.get_config(self.ip_adapter_model.key)
+ assert isinstance(ip_adapter_info, (IPAdapter_InvokeAI_Config_Base, IPAdapter_Checkpoint_Config_Base))
+
+ if isinstance(ip_adapter_info, IPAdapter_InvokeAI_Config_Base):
+ image_encoder_model_id = ip_adapter_info.image_encoder_model_id
+ image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip()
+ else:
+ image_encoder_starter_model = CLIP_VISION_MODEL_MAP[self.clip_vision_model]
+ image_encoder_model_id = image_encoder_starter_model.source
+ image_encoder_model_name = image_encoder_starter_model.name
+
+ image_encoder_model = self.get_clip_image_encoder(context, image_encoder_model_id, image_encoder_model_name)
+
+ if self.method == "style":
+ if ip_adapter_info.base == "sd-1":
+ target_blocks = ["up_blocks.1"]
+ elif ip_adapter_info.base == "sdxl":
+ target_blocks = ["up_blocks.0.attentions.1"]
+ else:
+ raise ValueError(f"Unsupported IP-Adapter base type: '{ip_adapter_info.base}'.")
+ elif self.method == "composition":
+ if ip_adapter_info.base == "sd-1":
+ target_blocks = ["down_blocks.2", "mid_block"]
+ elif ip_adapter_info.base == "sdxl":
+ target_blocks = ["down_blocks.2.attentions.1"]
+ else:
+ raise ValueError(f"Unsupported IP-Adapter base type: '{ip_adapter_info.base}'.")
+ elif self.method == "style_precise":
+ if ip_adapter_info.base == "sd-1":
+ target_blocks = ["up_blocks.1", "down_blocks.2", "mid_block"]
+ elif ip_adapter_info.base == "sdxl":
+ target_blocks = ["up_blocks.0.attentions.1", "down_blocks.2.attentions.1"]
+ else:
+ raise ValueError(f"Unsupported IP-Adapter base type: '{ip_adapter_info.base}'.")
+ elif self.method == "style_strong":
+ if ip_adapter_info.base == "sd-1":
+ target_blocks = ["up_blocks.0", "up_blocks.1", "up_blocks.2", "down_blocks.0", "down_blocks.1"]
+ elif ip_adapter_info.base == "sdxl":
+ target_blocks = [
+ "up_blocks.0.attentions.1",
+ "up_blocks.1.attentions.1",
+ "up_blocks.2.attentions.1",
+ "up_blocks.0.attentions.2",
+ "up_blocks.1.attentions.2",
+ "up_blocks.2.attentions.2",
+ "up_blocks.0.attentions.0",
+ "up_blocks.1.attentions.0",
+ "up_blocks.2.attentions.0",
+ "down_blocks.0.attentions.0",
+ "down_blocks.0.attentions.1",
+ "down_blocks.0.attentions.2",
+ "down_blocks.1.attentions.0",
+ "down_blocks.1.attentions.1",
+ "down_blocks.1.attentions.2",
+ "down_blocks.2.attentions.0",
+ "down_blocks.2.attentions.2",
+ ]
+ else:
+ raise ValueError(f"Unsupported IP-Adapter base type: '{ip_adapter_info.base}'.")
+ elif self.method == "full":
+ target_blocks = ["block"]
+ else:
+ raise ValueError(f"Unexpected IP-Adapter method: '{self.method}'.")
+
+ return IPAdapterOutput(
+ ip_adapter=IPAdapterField(
+ image=self.image,
+ ip_adapter_model=self.ip_adapter_model,
+ image_encoder_model=ModelIdentifierField.from_config(image_encoder_model),
+ weight=self.weight,
+ target_blocks=target_blocks,
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ mask=self.mask,
+ method=self.method,
+ ),
+ )
+
+ @classmethod
+ def get_clip_image_encoder(
+ cls, context: InvocationContext, image_encoder_model_id: str, image_encoder_model_name: str
+ ) -> AnyModelConfig:
+ image_encoder_models = context.models.search_by_attrs(
+ name=image_encoder_model_name, base=BaseModelType.Any, type=ModelType.CLIPVision
+ )
+
+ if not len(image_encoder_models) > 0:
+ context.logger.warning(
+ f"The image encoder required by this IP Adapter ({image_encoder_model_name}) is not installed. \
+ Downloading and installing now. This may take a while."
+ )
+
+ installer = context._services.model_manager.install
+ # Note: We hard-code the type to CLIPVision here because if the model contains both a CLIPVision and a
+ # CLIPText model, the probe may treat it as a CLIPText model.
+ job = installer.heuristic_import(
+ image_encoder_model_id, ModelRecordChanges(name=image_encoder_model_name, type=ModelType.CLIPVision)
+ )
+ installer.wait_for_job(job, timeout=600) # Wait for up to 10 minutes
+ image_encoder_models = context.models.search_by_attrs(
+ name=image_encoder_model_name, base=BaseModelType.Any, type=ModelType.CLIPVision
+ )
+
+ if len(image_encoder_models) == 0:
+ context.logger.error("Error while fetching CLIP Vision Image Encoder")
+ assert len(image_encoder_models) == 1
+
+ return image_encoder_models[0]
diff --git a/invokeai/app/invocations/latent_noise.py b/invokeai/app/invocations/latent_noise.py
new file mode 100644
index 00000000000..815effe972c
--- /dev/null
+++ b/invokeai/app/invocations/latent_noise.py
@@ -0,0 +1,136 @@
+from typing import Literal
+
+import torch
+
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.backend.util.devices import TorchDevice
+
+LatentNoiseType = Literal["SD", "FLUX", "FLUX.2", "SD3", "CogView4", "Z-Image", "Anima"]
+
+
+def validate_noise_dimensions(noise_type: LatentNoiseType, width: int, height: int) -> None:
+ multiple_of = 8
+ if noise_type in ("FLUX", "FLUX.2", "SD3", "Z-Image"):
+ multiple_of = 16
+ elif noise_type == "CogView4":
+ multiple_of = 32
+
+ if width % multiple_of != 0 or height % multiple_of != 0:
+ raise ValueError(f"{noise_type} noise width and height must be a multiple of {multiple_of}")
+
+
+def get_expected_noise_shape(
+ noise_type: LatentNoiseType,
+ width: int,
+ height: int,
+) -> tuple[int, ...]:
+ validate_noise_dimensions(noise_type, width, height)
+
+ if noise_type == "SD":
+ return (1, 4, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "FLUX":
+ return (1, 16, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "FLUX.2":
+ return (1, 32, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "SD3":
+ return (1, 16, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "CogView4":
+ return (1, 16, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "Z-Image":
+ return (1, 16, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ if noise_type == "Anima":
+ return (1, 16, 1, height // LATENT_SCALE_FACTOR, width // LATENT_SCALE_FACTOR)
+ raise ValueError(f"Unsupported noise type: {noise_type}")
+
+
+def validate_noise_tensor_shape(noise: torch.Tensor, noise_type: LatentNoiseType, width: int, height: int) -> None:
+ expected_shape = get_expected_noise_shape(noise_type, width, height)
+ if tuple(noise.shape) != expected_shape:
+ raise ValueError(f"Expected noise with shape {expected_shape}, got {tuple(noise.shape)}")
+
+
+def generate_noise_tensor(
+ noise_type: LatentNoiseType,
+ width: int,
+ height: int,
+ seed: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ use_cpu: bool = True,
+) -> torch.Tensor:
+ validate_noise_dimensions(noise_type, width, height)
+ rand_device = "cpu" if use_cpu else device.type
+ rand_dtype = TorchDevice.choose_torch_dtype(device=device)
+
+ if noise_type == "SD":
+ return torch.randn(
+ 1,
+ 4,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ dtype=rand_dtype,
+ device=rand_device,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "FLUX":
+ return torch.randn(
+ 1,
+ 16,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "FLUX.2":
+ return torch.randn(
+ 1,
+ 32,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "SD3":
+ return torch.randn(
+ 1,
+ 16,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "CogView4":
+ return torch.randn(
+ 1,
+ 16,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "Z-Image":
+ return torch.randn(
+ 1,
+ 16,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=torch.float32,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ if noise_type == "Anima":
+ return torch.randn(
+ 1,
+ 16,
+ 1,
+ height // LATENT_SCALE_FACTOR,
+ width // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=torch.float32,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to("cpu")
+ raise ValueError(f"Unsupported noise type: {noise_type}")
diff --git a/invokeai/app/invocations/latents_to_image.py b/invokeai/app/invocations/latents_to_image.py
new file mode 100644
index 00000000000..608485a078b
--- /dev/null
+++ b/invokeai/app/invocations/latents_to_image.py
@@ -0,0 +1,112 @@
+from contextlib import nullcontext
+
+import torch
+from diffusers.image_processor import VaeImageProcessor
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
+
+
+@invocation(
+ "l2i",
+ title="Latents to Image - SD1.5, SDXL",
+ tags=["latents", "image", "vae", "l2i"],
+ category="latents",
+ version="1.3.2",
+)
+class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+ tiled: bool = InputField(default=False, description=FieldDescriptions.tiled)
+ # NOTE: tile_size = 0 is a special value. We use this rather than `int | None`, because the workflow UI does not
+ # offer a way to directly set None values.
+ tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
+ fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ use_tiling = self.tiled or context.config.get().force_tiled_decode
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny))
+ estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
+ operation="decode",
+ image_tensor=latents,
+ vae=vae_info.model,
+ tile_size=self.tile_size if use_tiling else None,
+ fp32=self.fp32,
+ )
+ with (
+ SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
+ vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),
+ ):
+ context.util.signal_progress("Running VAE decoder")
+ assert isinstance(vae, (AutoencoderKL, AutoencoderTiny))
+ latents = latents.to(TorchDevice.choose_torch_device())
+ if self.fp32:
+ # FP32 mode: convert everything to float32 for maximum precision
+ vae.to(dtype=torch.float32)
+ latents = latents.float()
+ else:
+ vae.to(dtype=torch.float16)
+ latents = latents.half()
+
+ if use_tiling:
+ vae.enable_tiling()
+ else:
+ vae.disable_tiling()
+
+ tiling_context = nullcontext()
+ if self.tile_size > 0:
+ tiling_context = patch_vae_tiling_params(
+ vae,
+ tile_sample_min_size=self.tile_size,
+ tile_latent_min_size=self.tile_size // LATENT_SCALE_FACTOR,
+ tile_overlap_factor=0.25,
+ )
+
+ # clear memory as vae decode can request a lot
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode(), tiling_context:
+ # copied from diffusers pipeline
+ latents = latents / vae.config.scaling_factor
+ image = vae.decode(latents, return_dict=False)[0]
+ image = (image / 2 + 0.5).clamp(0, 1) # denormalize
+ # we always cast to float32 as this does not cause significant overhead and is compatible with bfloat16
+ np_image = image.cpu().permute(0, 2, 3, 1).float().numpy()
+
+ image = VaeImageProcessor.numpy_to_pil(np_image)[0]
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=image)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/lineart.py b/invokeai/app/invocations/lineart.py
new file mode 100644
index 00000000000..3ffd51b5b68
--- /dev/null
+++ b/invokeai/app/invocations/lineart.py
@@ -0,0 +1,34 @@
+from builtins import bool
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.lineart import Generator, LineartEdgeDetector
+
+
+@invocation(
+ "lineart_edge_detection",
+ title="Lineart Edge Detection",
+ tags=["controlnet", "lineart"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class LineartEdgeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an edge map using the Lineart model."""
+
+ image: ImageField = InputField(description="The image to process")
+ coarse: bool = InputField(default=False, description="Whether to use coarse mode")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ model_url = LineartEdgeDetector.get_model_url(self.coarse)
+ loaded_model = context.models.load_remote_model(model_url, LineartEdgeDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, Generator)
+ detector = LineartEdgeDetector(model)
+ edge_map = detector.run(image=image)
+
+ image_dto = context.images.save(image=edge_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/lineart_anime.py b/invokeai/app/invocations/lineart_anime.py
new file mode 100644
index 00000000000..f07476491cb
--- /dev/null
+++ b/invokeai/app/invocations/lineart_anime.py
@@ -0,0 +1,31 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.lineart_anime import LineartAnimeEdgeDetector, UnetGenerator
+
+
+@invocation(
+ "lineart_anime_edge_detection",
+ title="Lineart Anime Edge Detection",
+ tags=["controlnet", "lineart"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class LineartAnimeEdgeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Geneartes an edge map using the Lineart model."""
+
+ image: ImageField = InputField(description="The image to process")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ model_url = LineartAnimeEdgeDetector.get_model_url()
+ loaded_model = context.models.load_remote_model(model_url, LineartAnimeEdgeDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, UnetGenerator)
+ detector = LineartAnimeEdgeDetector(model)
+ edge_map = detector.run(image=image)
+
+ image_dto = context.images.save(image=edge_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/llava_onevision_vllm.py b/invokeai/app/invocations/llava_onevision_vllm.py
new file mode 100644
index 00000000000..ff3b801d37e
--- /dev/null
+++ b/invokeai/app/invocations/llava_onevision_vllm.py
@@ -0,0 +1,76 @@
+from typing import Any
+
+import torch
+from PIL.Image import Image
+from pydantic import field_validator
+from transformers import AutoProcessor, LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, UIComponent
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import StringOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
+from invokeai.backend.model_manager.taxonomy import ModelType
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "llava_onevision_vllm",
+ title="LLaVA OneVision VLLM",
+ tags=["vllm"],
+ category="multimodal",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class LlavaOnevisionVllmInvocation(BaseInvocation):
+ """Run a LLaVA OneVision VLLM model."""
+
+ images: list[ImageField] | ImageField | None = InputField(default=None, max_length=3, description="Input image.")
+ prompt: str = InputField(
+ default="",
+ description="Input text prompt.",
+ ui_component=UIComponent.Textarea,
+ )
+ vllm_model: ModelIdentifierField = InputField(
+ title="LLaVA Model Type",
+ description=FieldDescriptions.vllm_model,
+ ui_model_type=ModelType.LlavaOnevision,
+ )
+
+ @field_validator("images", mode="before")
+ def listify_images(cls, v: Any) -> list:
+ if v is None:
+ return v
+ if not isinstance(v, list):
+ return [v]
+ return v
+
+ def _get_images(self, context: InvocationContext) -> list[Image]:
+ if self.images is None:
+ return []
+
+ image_fields = self.images if isinstance(self.images, list) else [self.images]
+ return [context.images.get_pil(image_field.image_name, "RGB") for image_field in image_fields]
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ images = self._get_images(context)
+ model_config = context.models.get_config(self.vllm_model)
+
+ with context.models.load(self.vllm_model).model_on_device() as (_, model):
+ assert isinstance(model, LlavaOnevisionForConditionalGeneration)
+
+ model_abs_path = context.models.get_absolute_path(model_config)
+ processor = AutoProcessor.from_pretrained(model_abs_path, local_files_only=True)
+ assert isinstance(processor, LlavaOnevisionProcessor)
+
+ model = LlavaOnevisionPipeline(model, processor)
+ output = model.run(
+ prompt=self.prompt,
+ images=images,
+ device=TorchDevice.choose_torch_device(),
+ dtype=TorchDevice.choose_torch_dtype(),
+ )
+
+ return StringOutput(value=output)
diff --git a/invokeai/app/invocations/load_custom_nodes.py b/invokeai/app/invocations/load_custom_nodes.py
new file mode 100644
index 00000000000..a3a8194a3b9
--- /dev/null
+++ b/invokeai/app/invocations/load_custom_nodes.py
@@ -0,0 +1,83 @@
+import logging
+import shutil
+import sys
+import traceback
+from importlib.util import module_from_spec, spec_from_file_location
+from pathlib import Path
+
+
+def load_custom_nodes(custom_nodes_path: Path, logger: logging.Logger):
+ """
+ Loads all custom nodes from the custom_nodes_path directory.
+
+ If custom_nodes_path does not exist, it creates it.
+
+ It also copies the custom_nodes/README.md file to the custom_nodes_path directory. Because this file may change,
+ it is _always_ copied to the custom_nodes_path directory.
+
+ Then, it crawls the custom_nodes_path directory and imports all top-level directories as python modules.
+
+ If the directory does not contain an __init__.py file or starts with an `_` or `.`, it is skipped.
+ """
+
+ # create the custom nodes directory if it does not exist
+ custom_nodes_path.mkdir(parents=True, exist_ok=True)
+
+ # Copy the README file to the custom nodes directory
+ source_custom_nodes_readme_path = Path(__file__).parent / "custom_nodes/README.md"
+ target_custom_nodes_readme_path = Path(custom_nodes_path) / "README.md"
+
+ # copy our custom nodes README to the custom nodes directory
+ shutil.copy(source_custom_nodes_readme_path, target_custom_nodes_readme_path)
+
+ loaded_packs: list[str] = []
+ failed_packs: list[str] = []
+
+ # Import custom nodes, see https://docs.python.org/3/library/importlib.html#importing-programmatically
+ for d in custom_nodes_path.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
+
+ # load the module
+ spec = spec_from_file_location(module_name, init.absolute())
+
+ if spec is None or spec.loader is None:
+ logger.warning(f"Could not load {init}")
+ continue
+
+ logger.info(f"Loading node pack {module_name}")
+
+ try:
+ module = module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+
+ loaded_packs.append(module_name)
+ except Exception:
+ failed_packs.append(module_name)
+ full_error = traceback.format_exc()
+ logger.error(f"Failed to load node pack {module_name} (may have partially loaded):\n{full_error}")
+
+ del init, module_name
+
+ loaded_count = len(loaded_packs)
+ if loaded_count > 0:
+ logger.info(
+ f"Loaded {loaded_count} node pack{'s' if loaded_count != 1 else ''} from {custom_nodes_path}: {', '.join(loaded_packs)}"
+ )
diff --git a/invokeai/app/invocations/logic.py b/invokeai/app/invocations/logic.py
new file mode 100644
index 00000000000..7cc98afbbcf
--- /dev/null
+++ b/invokeai/app/invocations/logic.py
@@ -0,0 +1,34 @@
+from typing import Any, Optional
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import InputField, OutputField, UIType
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("if_output")
+class IfInvocationOutput(BaseInvocationOutput):
+ value: Optional[Any] = OutputField(
+ default=None, description="The selected value", title="Output", ui_type=UIType.Any
+ )
+
+
+@invocation("if", title="If", tags=["logic", "conditional"], category="math", version="1.0.0")
+class IfInvocation(BaseInvocation):
+ """Selects between two optional inputs based on a boolean condition."""
+
+ condition: bool = InputField(default=False, description="The condition used to select an input", title="Condition")
+ true_input: Optional[Any] = InputField(
+ default=None,
+ description="Selected when the condition is true",
+ title="True Input",
+ ui_type=UIType.Any,
+ )
+ false_input: Optional[Any] = InputField(
+ default=None,
+ description="Selected when the condition is false",
+ title="False Input",
+ ui_type=UIType.Any,
+ )
+
+ def invoke(self, context: InvocationContext) -> IfInvocationOutput:
+ return IfInvocationOutput(value=self.true_input if self.condition else self.false_input)
diff --git a/invokeai/app/invocations/mask.py b/invokeai/app/invocations/mask.py
new file mode 100644
index 00000000000..49749f43b64
--- /dev/null
+++ b/invokeai/app/invocations/mask.py
@@ -0,0 +1,266 @@
+import numpy as np
+import torch
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ InvocationContext,
+ invocation,
+)
+from invokeai.app.invocations.fields import (
+ BoundingBoxField,
+ ColorField,
+ ImageField,
+ InputField,
+ TensorField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.primitives import BoundingBoxOutput, ImageOutput, MaskOutput
+from invokeai.backend.image_util.util import pil_to_np
+
+
+@invocation(
+ "rectangle_mask",
+ title="Create Rectangle Mask",
+ tags=["conditioning"],
+ category="mask",
+ version="1.0.1",
+)
+class RectangleMaskInvocation(BaseInvocation, WithMetadata):
+ """Create a rectangular mask."""
+
+ width: int = InputField(description="The width of the entire mask.")
+ height: int = InputField(description="The height of the entire mask.")
+ x_left: int = InputField(description="The left x-coordinate of the rectangular masked region (inclusive).")
+ y_top: int = InputField(description="The top y-coordinate of the rectangular masked region (inclusive).")
+ rectangle_width: int = InputField(description="The width of the rectangular masked region.")
+ rectangle_height: int = InputField(description="The height of the rectangular masked region.")
+
+ def invoke(self, context: InvocationContext) -> MaskOutput:
+ mask = torch.zeros((1, self.height, self.width), dtype=torch.bool)
+ mask[:, self.y_top : self.y_top + self.rectangle_height, self.x_left : self.x_left + self.rectangle_width] = (
+ True
+ )
+
+ mask_tensor_name = context.tensors.save(mask)
+ return MaskOutput(
+ mask=TensorField(tensor_name=mask_tensor_name),
+ width=self.width,
+ height=self.height,
+ )
+
+
+@invocation(
+ "alpha_mask_to_tensor",
+ title="Alpha Mask to Tensor",
+ tags=["conditioning"],
+ category="mask",
+ version="1.0.0",
+)
+class AlphaMaskToTensorInvocation(BaseInvocation):
+ """Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0."""
+
+ image: ImageField = InputField(description="The mask image to convert.")
+ invert: bool = InputField(default=False, description="Whether to invert the mask.")
+
+ def invoke(self, context: InvocationContext) -> MaskOutput:
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+ mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
+ if self.invert:
+ mask[0] = torch.tensor(np.array(image)[:, :, 3] == 0, dtype=torch.bool)
+ else:
+ mask[0] = torch.tensor(np.array(image)[:, :, 3] > 0, dtype=torch.bool)
+
+ return MaskOutput(
+ mask=TensorField(tensor_name=context.tensors.save(mask)),
+ height=mask.shape[1],
+ width=mask.shape[2],
+ )
+
+
+@invocation(
+ "invert_tensor_mask",
+ title="Invert Tensor Mask",
+ tags=["conditioning"],
+ category="mask",
+ version="1.1.0",
+)
+class InvertTensorMaskInvocation(BaseInvocation):
+ """Inverts a tensor mask."""
+
+ mask: TensorField = InputField(description="The tensor mask to convert.")
+
+ def invoke(self, context: InvocationContext) -> MaskOutput:
+ mask = context.tensors.load(self.mask.tensor_name)
+
+ # Verify dtype and shape.
+ assert mask.dtype == torch.bool
+ assert mask.dim() in [2, 3]
+
+ # Unsqueeze the channel dimension if it is missing. The MaskOutput type expects a single channel.
+ if mask.dim() == 2:
+ mask = mask.unsqueeze(0)
+
+ inverted = ~mask
+
+ return MaskOutput(
+ mask=TensorField(tensor_name=context.tensors.save(inverted)),
+ height=inverted.shape[1],
+ width=inverted.shape[2],
+ )
+
+
+@invocation(
+ "image_mask_to_tensor",
+ title="Image Mask to Tensor",
+ tags=["conditioning"],
+ category="mask",
+ version="1.0.0",
+)
+class ImageMaskToTensorInvocation(BaseInvocation, WithMetadata):
+ """Convert a mask image to a tensor. Converts the image to grayscale and uses thresholding at the specified value."""
+
+ image: ImageField = InputField(description="The mask image to convert.")
+ cutoff: int = InputField(ge=0, le=255, description="Cutoff (<)", default=128)
+ invert: bool = InputField(default=False, description="Whether to invert the mask.")
+
+ def invoke(self, context: InvocationContext) -> MaskOutput:
+ image = context.images.get_pil(self.image.image_name, mode="L")
+
+ mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
+ if self.invert:
+ mask[0] = torch.tensor(np.array(image)[:, :] >= self.cutoff, dtype=torch.bool)
+ else:
+ mask[0] = torch.tensor(np.array(image)[:, :] < self.cutoff, dtype=torch.bool)
+
+ return MaskOutput(
+ mask=TensorField(tensor_name=context.tensors.save(mask)),
+ height=mask.shape[1],
+ width=mask.shape[2],
+ )
+
+
+@invocation(
+ "tensor_mask_to_image",
+ title="Tensor Mask to Image",
+ tags=["mask"],
+ category="mask",
+ version="1.1.0",
+)
+class MaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Convert a mask tensor to an image."""
+
+ mask: TensorField = InputField(description="The mask tensor to convert.")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ mask = context.tensors.load(self.mask.tensor_name)
+
+ # Squeeze the channel dimension if it exists.
+ if mask.dim() == 3:
+ mask = mask.squeeze(0)
+
+ # Ensure that the mask is binary.
+ if mask.dtype != torch.bool:
+ mask = mask > 0.5
+ mask_np = (mask.float() * 255).byte().cpu().numpy()
+
+ mask_pil = Image.fromarray(mask_np, mode="L")
+ image_dto = context.images.save(image=mask_pil)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "apply_tensor_mask_to_image",
+ title="Apply Tensor Mask to Image",
+ tags=["mask"],
+ category="mask",
+ version="1.0.0",
+)
+class ApplyMaskTensorToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Applies a tensor mask to an image.
+
+ The image is converted to RGBA and the mask is applied to the alpha channel."""
+
+ mask: TensorField = InputField(description="The mask tensor to apply.")
+ image: ImageField = InputField(description="The image to apply the mask to.")
+ invert: bool = InputField(default=False, description="Whether to invert the mask.")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, mode="RGBA")
+ mask = context.tensors.load(self.mask.tensor_name)
+
+ # Squeeze the channel dimension if it exists.
+ if mask.dim() == 3:
+ mask = mask.squeeze(0)
+
+ # Ensure that the mask is binary.
+ if mask.dtype != torch.bool:
+ mask = mask > 0.5
+ mask_np = (mask.float() * 255).byte().cpu().numpy().astype(np.uint8)
+
+ if self.invert:
+ mask_np = 255 - mask_np
+
+ # Apply the mask only to the alpha channel where the original alpha is non-zero. This preserves the original
+ # image's transparency - else the transparent regions would end up as opaque black.
+
+ # Separate the image into R, G, B, and A channels
+ image_np = pil_to_np(image)
+ r, g, b, a = np.split(image_np, 4, axis=-1)
+
+ # Apply the mask to the alpha channel
+ new_alpha = np.where(a.squeeze() > 0, mask_np, a.squeeze())
+
+ # Stack the RGB channels with the modified alpha
+ masked_image_np = np.dstack([r.squeeze(), g.squeeze(), b.squeeze(), new_alpha])
+
+ # Convert back to an image (RGBA)
+ masked_image = Image.fromarray(masked_image_np.astype(np.uint8), "RGBA")
+ image_dto = context.images.save(image=masked_image)
+
+ return ImageOutput.build(image_dto)
+
+
+WHITE = ColorField(r=255, g=255, b=255, a=255)
+
+
+@invocation(
+ "get_image_mask_bounding_box",
+ title="Get Image Mask Bounding Box",
+ tags=["mask"],
+ category="mask",
+ version="1.0.0",
+)
+class GetMaskBoundingBoxInvocation(BaseInvocation):
+ """Gets the bounding box of the given mask image."""
+
+ mask: ImageField = InputField(description="The mask to crop.")
+ margin: int = InputField(default=0, description="Margin to add to the bounding box.")
+ mask_color: ColorField = InputField(default=WHITE, description="Color of the mask in the image.")
+
+ def invoke(self, context: InvocationContext) -> BoundingBoxOutput:
+ mask = context.images.get_pil(self.mask.image_name, mode="RGBA")
+ mask_np = np.array(mask)
+
+ # Convert mask_color to RGBA tuple
+ mask_color_rgb = self.mask_color.tuple()
+
+ # Find the bounding box of the mask color
+ y, x = np.where(np.all(mask_np == mask_color_rgb, axis=-1))
+
+ if len(x) == 0 or len(y) == 0:
+ # No pixels found with the given color
+ return BoundingBoxOutput(bounding_box=BoundingBoxField(x_min=0, y_min=0, x_max=0, y_max=0))
+
+ left, upper, right, lower = x.min(), y.min(), x.max(), y.max()
+
+ # Add the margin
+ left = max(0, left - self.margin)
+ upper = max(0, upper - self.margin)
+ right = min(mask_np.shape[1], right + self.margin)
+ lower = min(mask_np.shape[0], lower + self.margin)
+
+ bounding_box = BoundingBoxField(x_min=left, y_min=upper, x_max=right, y_max=lower)
+
+ return BoundingBoxOutput(bounding_box=bounding_box)
diff --git a/invokeai/app/invocations/math.py b/invokeai/app/invocations/math.py
new file mode 100644
index 00000000000..5d3988031ba
--- /dev/null
+++ b/invokeai/app/invocations/math.py
@@ -0,0 +1,292 @@
+# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
+
+from typing import Literal
+
+import numpy as np
+from pydantic import ValidationInfo, field_validator
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, InputField
+from invokeai.app.invocations.primitives import FloatOutput, IntegerOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation("add", title="Add Integers", tags=["math", "add"], category="math", version="1.0.1")
+class AddInvocation(BaseInvocation):
+ """Adds two numbers"""
+
+ a: int = InputField(default=0, description=FieldDescriptions.num_1)
+ b: int = InputField(default=0, description=FieldDescriptions.num_2)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ return IntegerOutput(value=self.a + self.b)
+
+
+@invocation("sub", title="Subtract Integers", tags=["math", "subtract"], category="math", version="1.0.1")
+class SubtractInvocation(BaseInvocation):
+ """Subtracts two numbers"""
+
+ a: int = InputField(default=0, description=FieldDescriptions.num_1)
+ b: int = InputField(default=0, description=FieldDescriptions.num_2)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ return IntegerOutput(value=self.a - self.b)
+
+
+@invocation("mul", title="Multiply Integers", tags=["math", "multiply"], category="math", version="1.0.1")
+class MultiplyInvocation(BaseInvocation):
+ """Multiplies two numbers"""
+
+ a: int = InputField(default=0, description=FieldDescriptions.num_1)
+ b: int = InputField(default=0, description=FieldDescriptions.num_2)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ return IntegerOutput(value=self.a * self.b)
+
+
+@invocation("div", title="Divide Integers", tags=["math", "divide"], category="math", version="1.0.1")
+class DivideInvocation(BaseInvocation):
+ """Divides two numbers"""
+
+ a: int = InputField(default=0, description=FieldDescriptions.num_1)
+ b: int = InputField(default=0, description=FieldDescriptions.num_2)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ return IntegerOutput(value=int(self.a / self.b))
+
+
+@invocation(
+ "rand_int",
+ title="Random Integer",
+ tags=["math", "random"],
+ category="math",
+ version="1.0.1",
+ use_cache=False,
+)
+class RandomIntInvocation(BaseInvocation):
+ """Outputs a single random integer."""
+
+ low: int = InputField(default=0, description=FieldDescriptions.inclusive_low)
+ high: int = InputField(default=np.iinfo(np.int32).max, description=FieldDescriptions.exclusive_high)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ 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(
+ "float_to_int",
+ title="Float To Integer",
+ tags=["math", "round", "integer", "float", "convert"],
+ category="math",
+ version="1.0.1",
+)
+class FloatToIntegerInvocation(BaseInvocation):
+ """Rounds a float number to (a multiple of) an integer."""
+
+ value: float = InputField(default=0, description="The value to round")
+ multiple: int = InputField(default=1, ge=1, title="Multiple of", description="The multiple to round to")
+ method: Literal["Nearest", "Floor", "Ceiling", "Truncate"] = InputField(
+ default="Nearest", description="The method to use for rounding"
+ )
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ if self.method == "Nearest":
+ return IntegerOutput(value=round(self.value / self.multiple) * self.multiple)
+ elif self.method == "Floor":
+ return IntegerOutput(value=np.floor(self.value / self.multiple) * self.multiple)
+ elif self.method == "Ceiling":
+ return IntegerOutput(value=np.ceil(self.value / self.multiple) * self.multiple)
+ else: # self.method == "Truncate"
+ return IntegerOutput(value=int(self.value / self.multiple) * self.multiple)
+
+
+@invocation("round_float", title="Round Float", tags=["math", "round"], category="math", version="1.0.1")
+class RoundInvocation(BaseInvocation):
+ """Rounds a float to a specified number of decimal places."""
+
+ value: float = InputField(default=0, description="The float value")
+ decimals: int = InputField(default=0, description="The number of decimal places")
+
+ def invoke(self, context: InvocationContext) -> FloatOutput:
+ return FloatOutput(value=round(self.value, self.decimals))
+
+
+INTEGER_OPERATIONS = Literal[
+ "ADD",
+ "SUB",
+ "MUL",
+ "DIV",
+ "EXP",
+ "MOD",
+ "ABS",
+ "MIN",
+ "MAX",
+]
+
+
+INTEGER_OPERATIONS_LABELS = {
+ "ADD": "Add A+B",
+ "SUB": "Subtract A-B",
+ "MUL": "Multiply A*B",
+ "DIV": "Divide A/B",
+ "EXP": "Exponentiate A^B",
+ "MOD": "Modulus A%B",
+ "ABS": "Absolute Value of A",
+ "MIN": "Minimum(A,B)",
+ "MAX": "Maximum(A,B)",
+}
+
+
+@invocation(
+ "integer_math",
+ title="Integer Math",
+ tags=[
+ "math",
+ "integer",
+ "add",
+ "subtract",
+ "multiply",
+ "divide",
+ "modulus",
+ "power",
+ "absolute value",
+ "min",
+ "max",
+ ],
+ category="math",
+ version="1.0.1",
+)
+class IntegerMathInvocation(BaseInvocation):
+ """Performs integer math."""
+
+ operation: INTEGER_OPERATIONS = InputField(
+ default="ADD", description="The operation to perform", ui_choice_labels=INTEGER_OPERATIONS_LABELS
+ )
+ a: int = InputField(default=1, description=FieldDescriptions.num_1)
+ b: int = InputField(default=1, description=FieldDescriptions.num_2)
+
+ @field_validator("b")
+ def no_unrepresentable_results(cls, v: int, info: ValidationInfo):
+ if info.data["operation"] == "DIV" and v == 0:
+ raise ValueError("Cannot divide by zero")
+ elif info.data["operation"] == "MOD" and v == 0:
+ raise ValueError("Cannot divide by zero")
+ elif info.data["operation"] == "EXP" and v < 0:
+ raise ValueError("Result of exponentiation is not an integer")
+ return v
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9
+ if self.operation == "ADD":
+ return IntegerOutput(value=self.a + self.b)
+ elif self.operation == "SUB":
+ return IntegerOutput(value=self.a - self.b)
+ elif self.operation == "MUL":
+ return IntegerOutput(value=self.a * self.b)
+ elif self.operation == "DIV":
+ return IntegerOutput(value=int(self.a / self.b))
+ elif self.operation == "EXP":
+ return IntegerOutput(value=self.a**self.b)
+ elif self.operation == "MOD":
+ return IntegerOutput(value=self.a % self.b)
+ elif self.operation == "ABS":
+ return IntegerOutput(value=abs(self.a))
+ elif self.operation == "MIN":
+ return IntegerOutput(value=min(self.a, self.b))
+ else: # self.operation == "MAX":
+ return IntegerOutput(value=max(self.a, self.b))
+
+
+FLOAT_OPERATIONS = Literal[
+ "ADD",
+ "SUB",
+ "MUL",
+ "DIV",
+ "EXP",
+ "ABS",
+ "SQRT",
+ "MIN",
+ "MAX",
+]
+
+
+FLOAT_OPERATIONS_LABELS = {
+ "ADD": "Add A+B",
+ "SUB": "Subtract A-B",
+ "MUL": "Multiply A*B",
+ "DIV": "Divide A/B",
+ "EXP": "Exponentiate A^B",
+ "ABS": "Absolute Value of A",
+ "SQRT": "Square Root of A",
+ "MIN": "Minimum(A,B)",
+ "MAX": "Maximum(A,B)",
+}
+
+
+@invocation(
+ "float_math",
+ title="Float Math",
+ tags=["math", "float", "add", "subtract", "multiply", "divide", "power", "root", "absolute value", "min", "max"],
+ category="math",
+ version="1.0.1",
+)
+class FloatMathInvocation(BaseInvocation):
+ """Performs floating point math."""
+
+ operation: FLOAT_OPERATIONS = InputField(
+ default="ADD", description="The operation to perform", ui_choice_labels=FLOAT_OPERATIONS_LABELS
+ )
+ a: float = InputField(default=1, description=FieldDescriptions.num_1)
+ b: float = InputField(default=1, description=FieldDescriptions.num_2)
+
+ @field_validator("b")
+ def no_unrepresentable_results(cls, v: float, info: ValidationInfo):
+ if info.data["operation"] == "DIV" and v == 0:
+ raise ValueError("Cannot divide by zero")
+ elif info.data["operation"] == "EXP" and info.data["a"] == 0 and v < 0:
+ raise ValueError("Cannot raise zero to a negative power")
+ elif info.data["operation"] == "EXP" and isinstance(info.data["a"] ** v, complex):
+ raise ValueError("Root operation resulted in a complex number")
+ return v
+
+ def invoke(self, context: InvocationContext) -> FloatOutput:
+ # Python doesn't support switch statements until 3.10, but InvokeAI supports back to 3.9
+ if self.operation == "ADD":
+ return FloatOutput(value=self.a + self.b)
+ elif self.operation == "SUB":
+ return FloatOutput(value=self.a - self.b)
+ elif self.operation == "MUL":
+ return FloatOutput(value=self.a * self.b)
+ elif self.operation == "DIV":
+ return FloatOutput(value=self.a / self.b)
+ elif self.operation == "EXP":
+ return FloatOutput(value=self.a**self.b)
+ elif self.operation == "SQRT":
+ return FloatOutput(value=np.sqrt(self.a))
+ elif self.operation == "ABS":
+ return FloatOutput(value=abs(self.a))
+ elif self.operation == "MIN":
+ return FloatOutput(value=min(self.a, self.b))
+ else: # self.operation == "MAX":
+ return FloatOutput(value=max(self.a, self.b))
diff --git a/invokeai/app/invocations/mediapipe_face.py b/invokeai/app/invocations/mediapipe_face.py
new file mode 100644
index 00000000000..e81326463ce
--- /dev/null
+++ b/invokeai/app/invocations/mediapipe_face.py
@@ -0,0 +1,26 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.mediapipe_face import detect_faces
+
+
+@invocation(
+ "mediapipe_face_detection",
+ title="MediaPipe Face Detection",
+ tags=["controlnet", "face"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class MediaPipeFaceDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Detects faces using MediaPipe."""
+
+ image: ImageField = InputField(description="The image to process")
+ max_faces: int = InputField(default=1, ge=1, description="Maximum number of faces to detect")
+ min_confidence: float = InputField(default=0.5, ge=0, le=1, description="Minimum confidence for face detection")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ detected_faces = detect_faces(image=image, max_faces=self.max_faces, min_confidence=self.min_confidence)
+ image_dto = context.images.save(image=detected_faces)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/metadata.py b/invokeai/app/invocations/metadata.py
new file mode 100644
index 00000000000..da24d8802bb
--- /dev/null
+++ b/invokeai/app/invocations/metadata.py
@@ -0,0 +1,335 @@
+from typing import Any, Literal, Optional, Union
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ MetadataField,
+ OutputField,
+ UIType,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import StringOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
+from invokeai.version.invokeai_version import __version__
+
+
+class MetadataItemField(BaseModel):
+ label: str = Field(description=FieldDescriptions.metadata_item_label)
+ value: Any = Field(description=FieldDescriptions.metadata_item_value)
+
+
+class LoRAMetadataField(BaseModel):
+ """LoRA Metadata Field"""
+
+ model: ModelIdentifierField = Field(description=FieldDescriptions.lora_model)
+ weight: float = Field(description=FieldDescriptions.lora_weight)
+
+
+class IPAdapterMetadataField(BaseModel):
+ """IP Adapter Field, minus the CLIP Vision Encoder model"""
+
+ image: ImageField = Field(description="The IP-Adapter image prompt.")
+ ip_adapter_model: ModelIdentifierField = Field(description="The IP-Adapter model.")
+ clip_vision_model: Literal["ViT-L", "ViT-H", "ViT-G"] = Field(description="The CLIP Vision model")
+ method: Literal["full", "style", "composition", "style_strong", "style_precise"] = Field(
+ description="Method to apply IP Weights with"
+ )
+ 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)")
+
+
+class T2IAdapterMetadataField(BaseModel):
+ image: ImageField = Field(description="The control image.")
+ processed_image: Optional[ImageField] = Field(default=None, description="The control image, after processing.")
+ t2i_adapter_model: ModelIdentifierField = Field(description="The T2I-Adapter model to use.")
+ weight: Union[float, list[float]] = Field(default=1, description="The weight given to the T2I-Adapter")
+ begin_step_percent: float = Field(
+ default=0, ge=0, le=1, description="When the T2I-Adapter is first applied (% of total steps)"
+ )
+ end_step_percent: float = Field(
+ default=1, ge=0, le=1, description="When the T2I-Adapter is last applied (% of total steps)"
+ )
+ resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
+
+
+class ControlNetMetadataField(BaseModel):
+ image: ImageField = Field(description="The control image")
+ processed_image: Optional[ImageField] = Field(default=None, description="The control image, after processing.")
+ control_model: ModelIdentifierField = Field(description="The ControlNet model to use")
+ control_weight: Union[float, list[float]] = Field(default=1, description="The weight given to the ControlNet")
+ begin_step_percent: float = Field(
+ default=0, ge=0, le=1, description="When the ControlNet is first applied (% of total steps)"
+ )
+ end_step_percent: float = Field(
+ default=1, ge=0, le=1, description="When the ControlNet is last applied (% of total steps)"
+ )
+ control_mode: CONTROLNET_MODE_VALUES = Field(default="balanced", description="The control mode to use")
+ resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
+
+
+@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.1")
+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.1")
+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.1")
+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",
+ "flux_txt2img",
+ "flux_img2img",
+ "flux_inpaint",
+ "flux_outpaint",
+ "flux2_txt2img",
+ "flux2_img2img",
+ "flux2_inpaint",
+ "flux2_outpaint",
+ "sd3_txt2img",
+ "sd3_img2img",
+ "sd3_inpaint",
+ "sd3_outpaint",
+ "cogview4_txt2img",
+ "cogview4_img2img",
+ "cogview4_inpaint",
+ "cogview4_outpaint",
+ "z_image_txt2img",
+ "z_image_img2img",
+ "z_image_inpaint",
+ "z_image_outpaint",
+ "qwen_image_txt2img",
+ "qwen_image_img2img",
+ "qwen_image_inpaint",
+ "qwen_image_outpaint",
+ "anima_txt2img",
+ "anima_img2img",
+ "anima_inpaint",
+ "anima_outpaint",
+]
+
+
+@invocation(
+ "core_metadata",
+ title="Core Metadata",
+ tags=["metadata"],
+ category="metadata",
+ version="2.1.0",
+ classification=Classification.Internal,
+)
+class CoreMetadataInvocation(BaseInvocation):
+ """Used internally by Invoke to collect metadata for generations."""
+
+ generation_mode: Optional[GENERATION_MODES] = InputField(
+ default=None,
+ description="The generation mode that output this image",
+ )
+ positive_prompt: Optional[str] = InputField(default=None, description="The positive prompt parameter")
+ negative_prompt: Optional[str] = InputField(default=None, description="The negative prompt parameter")
+ width: Optional[int] = InputField(default=None, description="The width parameter")
+ height: Optional[int] = InputField(default=None, description="The height parameter")
+ seed: Optional[int] = InputField(default=None, description="The seed used for noise generation")
+ rand_device: Optional[str] = InputField(default=None, description="The device used for random number generation")
+ cfg_scale: Optional[float] = InputField(default=None, description="The classifier-free guidance scale parameter")
+ cfg_rescale_multiplier: Optional[float] = InputField(
+ default=None, description=FieldDescriptions.cfg_rescale_multiplier
+ )
+ steps: Optional[int] = InputField(default=None, description="The number of steps used for inference")
+ scheduler: Optional[str] = InputField(default=None, description="The scheduler used for inference")
+ seamless_x: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the X axis")
+ seamless_y: Optional[bool] = InputField(default=None, description="Whether seamless tiling was used on the Y axis")
+ clip_skip: Optional[int] = InputField(
+ default=None,
+ description="The number of skipped CLIP layers",
+ )
+ model: Optional[ModelIdentifierField] = InputField(default=None, description="The main model used for inference")
+ controlnets: Optional[list[ControlNetMetadataField]] = InputField(
+ default=None, description="The ControlNets used for inference"
+ )
+ ipAdapters: Optional[list[IPAdapterMetadataField]] = InputField(
+ default=None, description="The IP Adapters used for inference"
+ )
+ t2iAdapters: Optional[list[T2IAdapterMetadataField]] = InputField(
+ default=None, description="The IP Adapters used for inference"
+ )
+ loras: Optional[list[LoRAMetadataField]] = InputField(default=None, description="The LoRAs used for inference")
+ strength: Optional[float] = InputField(
+ default=None,
+ description="The strength used for latents-to-latents",
+ )
+ init_image: Optional[str] = InputField(
+ default=None,
+ description="The name of the initial image",
+ )
+ vae: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="The VAE used for decoding, if the main model's default was not used",
+ )
+ qwen3_encoder: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="The Qwen3 text encoder model used for Z-Image inference",
+ )
+
+ # High resolution fix metadata.
+ hrf_enabled: Optional[bool] = 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
+ positive_style_prompt: Optional[str] = InputField(
+ default=None,
+ description="The positive style prompt parameter",
+ )
+ negative_style_prompt: Optional[str] = InputField(
+ default=None,
+ description="The negative style prompt parameter",
+ )
+
+ # SDXL Refiner
+ refiner_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="The SDXL Refiner model used",
+ )
+ refiner_cfg_scale: Optional[float] = InputField(
+ default=None,
+ description="The classifier-free guidance scale parameter used for the refiner",
+ )
+ refiner_steps: Optional[int] = InputField(
+ default=None,
+ description="The number of steps used for the refiner",
+ )
+ refiner_scheduler: Optional[str] = InputField(
+ default=None,
+ description="The scheduler used for the refiner",
+ )
+ refiner_positive_aesthetic_score: Optional[float] = InputField(
+ default=None,
+ description="The aesthetic score used for the refiner",
+ )
+ refiner_negative_aesthetic_score: Optional[float] = InputField(
+ default=None,
+ description="The aesthetic score used for the refiner",
+ )
+ refiner_start: Optional[float] = InputField(
+ default=None,
+ description="The start value used for refiner denoising",
+ )
+
+ def invoke(self, context: InvocationContext) -> MetadataOutput:
+ """Collects and outputs a CoreMetadata object"""
+
+ as_dict = self.model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
+ as_dict["app_version"] = __version__
+
+ return MetadataOutput(metadata=MetadataField.model_validate(as_dict))
+
+ model_config = ConfigDict(extra="allow")
+
+
+@invocation(
+ "metadata_field_extractor",
+ title="Metadata Field Extractor",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Deprecated,
+)
+class MetadataFieldExtractorInvocation(BaseInvocation):
+ """Extracts the text value from an image's metadata given a key.
+ Raises an error if the image has no metadata or if the value is not a string (nesting not permitted)."""
+
+ image: ImageField = InputField(description="The image to extract metadata from")
+ key: str = InputField(description="The key in the image's metadata to extract the value from")
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ image_name = self.image.image_name
+
+ metadata = context.images.get_metadata(image_name=image_name)
+ if not metadata:
+ raise ValueError(f"No metadata found on image {image_name}")
+
+ try:
+ val = metadata.root[self.key]
+ if not isinstance(val, str):
+ raise ValueError(f"Metadata at key '{self.key}' must be a string")
+ return StringOutput(value=val)
+ except KeyError as e:
+ raise ValueError(f"No key '{self.key}' found in the metadata for {image_name}") from e
diff --git a/invokeai/app/invocations/metadata_linked.py b/invokeai/app/invocations/metadata_linked.py
new file mode 100644
index 00000000000..cd733fab648
--- /dev/null
+++ b/invokeai/app/invocations/metadata_linked.py
@@ -0,0 +1,1361 @@
+# Adopted from @skunworkxdark's metadata nodes (MIT License)
+# https://github.com/skunkworxdark/metadata-linked-nodes
+# Thanks to @skunworkxdark for the original implementation!
+
+import copy
+from typing import Any, Dict, Literal, Optional, TypeVar, Union
+
+from pydantic import model_validator
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.controlnet import ControlField, ControlNetInvocation
+from invokeai.app.invocations.denoise_latents import DenoiseLatentsInvocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ MetadataField,
+ OutputField,
+ UIType,
+ WithMetadata,
+)
+from invokeai.app.invocations.flux_denoise import FluxDenoiseInvocation
+from invokeai.app.invocations.ip_adapter import IPAdapterField, IPAdapterInvocation
+from invokeai.app.invocations.metadata import LoRAMetadataField, MetadataOutput
+from invokeai.app.invocations.model import (
+ CLIPField,
+ LoRAField,
+ LoRALoaderOutput,
+ ModelIdentifierField,
+ SDXLLoRALoaderOutput,
+ UNetField,
+ VAEField,
+ VAEOutput,
+)
+from invokeai.app.invocations.primitives import (
+ BooleanCollectionOutput,
+ BooleanOutput,
+ FloatCollectionOutput,
+ FloatOutput,
+ IntegerCollectionOutput,
+ IntegerOutput,
+ LatentsOutput,
+ StringCollectionOutput,
+ StringOutput,
+)
+from invokeai.app.invocations.scheduler import SchedulerOutput
+from invokeai.app.invocations.t2i_adapter import T2IAdapterField, T2IAdapterInvocation
+from invokeai.app.invocations.z_image_denoise import ZImageDenoiseInvocation
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
+from invokeai.version import __version__
+
+CUSTOM_LABEL: str = "* CUSTOM LABEL *"
+
+CORE_LABELS = Literal[
+ f"{CUSTOM_LABEL}",
+ "positive_prompt",
+ "positive_style_prompt",
+ "negative_prompt",
+ "negative_style_prompt",
+ "width",
+ "height",
+ "seed",
+ "cfg_scale",
+ "cfg_rescale_multiplier",
+ "steps",
+ "scheduler",
+ "clip_skip",
+ "model",
+ "vae",
+ "seamless_x",
+ "seamless_y",
+ "guidance",
+ "cfg_scale_start_step",
+ "cfg_scale_end_step",
+]
+
+CORE_LABELS_STRING = Literal[
+ f"{CUSTOM_LABEL}",
+ "positive_prompt",
+ "positive_style_prompt",
+ "negative_prompt",
+ "negative_style_prompt",
+]
+
+CORE_LABELS_INTEGER = Literal[
+ f"{CUSTOM_LABEL}",
+ "width",
+ "height",
+ "seed",
+ "steps",
+ "clip_skip",
+ "cfg_scale_start_step",
+ "cfg_scale_end_step",
+]
+
+CORE_LABELS_FLOAT = Literal[
+ f"{CUSTOM_LABEL}",
+ "cfg_scale",
+ "cfg_rescale_multiplier",
+ "guidance",
+]
+
+CORE_LABELS_BOOL = Literal[
+ f"{CUSTOM_LABEL}",
+ "seamless_x",
+ "seamless_y",
+]
+
+CORE_LABELS_SCHEDULER = Literal[
+ f"{CUSTOM_LABEL}",
+ "scheduler",
+]
+
+CORE_LABELS_MODEL = Literal[
+ f"{CUSTOM_LABEL}",
+ "model",
+]
+
+CORE_LABELS_VAE = Literal[
+ f"{CUSTOM_LABEL}",
+ "vae",
+]
+
+T = TypeVar("T")
+
+
+def append_list(item_cls: type[T], new_item: T, items: Union[T, list[T], None] = None) -> list[T]:
+ """Combines any number of items or lists into a single list,
+ ensuring consistency in type.
+
+ Args:
+ item_cls: The expected type of elements in the list.
+ items: An existing list or single item of type `item_cls`.
+ new_items: Additional item(s) to append. (default=None)
+
+ Returns:
+ The updated list containing valid items.
+
+ Raises:
+ ValueError: If any item in the list or new_item is not of the expected type.
+ """
+
+ if not isinstance(new_item, item_cls):
+ raise ValueError(f"Invalid new_item type in: {new_item}, expected {item_cls}")
+
+ if items is None:
+ return [new_item]
+
+ result: list[T] = []
+
+ if isinstance(items, item_cls):
+ result.append(items)
+ elif isinstance(items, list) and all(isinstance(i, item_cls) for i in items):
+ result.extend(items)
+ else:
+ raise ValueError(f"Invalid items type in: {items}, expected {item_cls}")
+
+ result.append(new_item)
+ return result
+
+
+def validate_custom_label(
+ model: Union[
+ "MetadataItemLinkedInvocation",
+ "MetadataToStringInvocation",
+ "MetadataToIntegerInvocation",
+ "MetadataToFloatInvocation",
+ "MetadataToBoolInvocation",
+ "MetadataToSchedulerInvocation",
+ "MetadataToModelInvocation",
+ "MetadataToSDXLModelInvocation",
+ "MetadataToVAEInvocation",
+ ],
+):
+ if model.label == CUSTOM_LABEL:
+ if model.custom_label is None or model.custom_label.strip() == "":
+ raise ValueError("You must enter a Custom Label")
+ return model
+
+
+def extract_model_key(
+ metadata: dict[str, Any],
+ label: Union[str, None],
+ default_key: str,
+ model_type: ModelType,
+ context: InvocationContext,
+) -> str:
+ """
+ Extracts a model key from the metadata based on the given label.
+
+ Args:
+ metadata (dict): The metadata root dictionary.
+ label (str): The label to search for.
+ default_key (str): The default model key to return if not found.
+ model_type (ModelType): model_type to use in the search if a model name_is found in the metadata
+ context (object): The context object containing models.
+
+ Returns:
+ Model key
+ """
+
+ if label in metadata:
+ if "key" in metadata[label]:
+ if context.models.exists(metadata[label]["key"]):
+ return metadata[label]["key"]
+ if "name" in metadata[label]:
+ search_model = context.models.search_by_attrs(name=metadata[label]["name"], type=model_type)
+ if len(search_model) > 0:
+ return search_model[0].key
+ if "model_name" in metadata[label]:
+ search_model = context.models.search_by_attrs(name=metadata[label]["model_name"], type=model_type)
+ if len(search_model) > 0:
+ return search_model[0].key
+
+ return default_key
+
+
+def get_model(
+ model_key: str,
+ context: InvocationContext,
+) -> ModelIdentifierField:
+ """
+ Gets a model based upon a model_key
+
+ Args:
+ mode_key (str): The model key to get
+ context (object): The context object containing models.
+
+ Returns:
+ ModelIdentifierField
+ """
+ if not context.models.exists(model_key):
+ raise Exception(f"Unknown model: {model_key}")
+
+ x = context.models.get_config(model_key)
+ return ModelIdentifierField.from_config(x)
+
+
+@invocation(
+ "metadata_item_linked",
+ title="Metadata Item Linked",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.1",
+ classification=Classification.Beta,
+)
+class MetadataItemLinkedInvocation(BaseInvocation, WithMetadata):
+ """Used to Create/Add/Update a value into a metadata label"""
+
+ label: CORE_LABELS = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ value: Any = InputField(description=FieldDescriptions.metadata_item_value, ui_type=UIType.Any)
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> MetadataOutput:
+ k = self.custom_label if self.label == CUSTOM_LABEL else self.label
+ v = self.value.vae if isinstance(self.value, VAEField) else self.value
+
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ data.update({str(k): v})
+ data.update({"app_version": __version__})
+
+ return MetadataOutput(metadata=MetadataField.model_validate(data))
+
+
+@invocation(
+ "metadata_from_image",
+ title="Metadata From Image",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.1",
+ classification=Classification.Beta,
+)
+class MetadataFromImageInvocation(BaseInvocation):
+ """Used to create a core metadata item then Add/Update it to the provided metadata"""
+
+ image: ImageField = InputField(description=FieldDescriptions.image)
+
+ def invoke(self, context: InvocationContext) -> MetadataOutput:
+ data: Dict[str, Any] = {}
+ image_metadata = context.images.get_metadata(self.image.image_name)
+ if image_metadata is not None:
+ data.update(image_metadata.root)
+
+ return MetadataOutput(metadata=MetadataField.model_validate(data))
+
+
+@invocation(
+ "metadata_to_string",
+ title="Metadata To String",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToStringInvocation(BaseInvocation, WithMetadata):
+ """Extracts a string value of a label from metadata"""
+
+ label: CORE_LABELS_STRING = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: str = InputField(description="The default string to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return StringOutput(value=str(output))
+
+
+@invocation(
+ "metadata_to_integer",
+ title="Metadata To Integer",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToIntegerInvocation(BaseInvocation, WithMetadata):
+ """Extracts an integer value of a label from metadata"""
+
+ label: CORE_LABELS_INTEGER = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: int = InputField(description="The default integer to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return IntegerOutput(value=int(output))
+
+
+@invocation(
+ "metadata_to_float",
+ title="Metadata To Float",
+ tags=["metadata"],
+ category="metadata",
+ version="1.1.0",
+ classification=Classification.Beta,
+)
+class MetadataToFloatInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Float value of a label from metadata"""
+
+ label: CORE_LABELS_FLOAT = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: float = InputField(description="The default float to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> FloatOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return FloatOutput(value=float(output))
+
+
+@invocation(
+ "metadata_to_bool",
+ title="Metadata To Bool",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToBoolInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Boolean value of a label from metadata"""
+
+ label: CORE_LABELS_BOOL = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: bool = InputField(description="The default bool to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> BooleanOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return BooleanOutput(value=bool(output))
+
+
+@invocation(
+ "metadata_to_scheduler",
+ title="Metadata To Scheduler",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.1",
+ classification=Classification.Beta,
+)
+class MetadataToSchedulerInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Scheduler value of a label from metadata"""
+
+ label: CORE_LABELS_SCHEDULER = InputField(
+ default="scheduler",
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description="The default scheduler to use if not found in the metadata",
+ ui_type=UIType.Scheduler,
+ )
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> SchedulerOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return SchedulerOutput(scheduler=output)
+
+
+@invocation_output("metadata_to_model_output")
+class MetadataToModelOutput(BaseInvocationOutput):
+ """String to main model output"""
+
+ model: ModelIdentifierField = OutputField(
+ description=FieldDescriptions.main_model,
+ title="Model",
+ )
+ name: str = OutputField(description="Model Name", title="Name")
+ unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+ clip: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP")
+
+
+@invocation_output("metadata_to_sdxl_model_output")
+class MetadataToSDXLModelOutput(BaseInvocationOutput):
+ """String to SDXL main model output"""
+
+ model: ModelIdentifierField = OutputField(
+ description=FieldDescriptions.main_model,
+ title="Model",
+ )
+ name: str = OutputField(description="Model Name", title="Name")
+ unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
+ clip: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP 1")
+ clip2: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP 2")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "metadata_to_model",
+ title="Metadata To Model",
+ tags=["metadata"],
+ category="metadata",
+ version="1.3.0",
+ classification=Classification.Beta,
+)
+class MetadataToModelInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Model value of a label from metadata"""
+
+ label: CORE_LABELS_MODEL = InputField(
+ default="model",
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: ModelIdentifierField = InputField(
+ description="The default model to use if not found in the metadata", ui_model_type=ModelType.Main
+ )
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> MetadataToModelOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ label = self.custom_label if self.label == CUSTOM_LABEL else self.label
+
+ model_key = extract_model_key(data, label, self.default_value.key, ModelType.Main, context)
+ model = get_model(model_key, context)
+
+ return MetadataToModelOutput(
+ model=model,
+ name=f"{model.base}: {model.name}",
+ unet=UNetField(
+ unet=model.model_copy(update={"submodel_type": SubModelType.UNet}),
+ scheduler=model.model_copy(update={"submodel_type": SubModelType.Scheduler}),
+ loras=[],
+ ),
+ clip=CLIPField(
+ tokenizer=model.model_copy(update={"submodel_type": SubModelType.Tokenizer}),
+ text_encoder=model.model_copy(update={"submodel_type": SubModelType.TextEncoder}),
+ loras=[],
+ skipped_layers=0,
+ ),
+ vae=VAEField(
+ vae=model.model_copy(update={"submodel_type": SubModelType.VAE}),
+ ),
+ )
+
+
+@invocation(
+ "metadata_to_sdxl_model",
+ title="Metadata To SDXL Model",
+ tags=["metadata"],
+ category="metadata",
+ version="1.3.0",
+ classification=Classification.Beta,
+)
+class MetadataToSDXLModelInvocation(BaseInvocation, WithMetadata):
+ """Extracts a SDXL Model value of a label from metadata"""
+
+ label: CORE_LABELS_MODEL = InputField(
+ default="model",
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: ModelIdentifierField = InputField(
+ description="The default SDXL Model to use if not found in the metadata",
+ ui_model_type=ModelType.Main,
+ ui_model_base=BaseModelType.StableDiffusionXL,
+ )
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> MetadataToSDXLModelOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ label = self.custom_label if self.label == CUSTOM_LABEL else self.label
+
+ model_key = extract_model_key(data, label, self.default_value.key, ModelType.Main, context)
+ model = get_model(model_key, context)
+
+ return MetadataToSDXLModelOutput(
+ model=model,
+ name=f"{model.base}: {model.name}",
+ unet=UNetField(
+ unet=model.model_copy(update={"submodel_type": SubModelType.UNet}),
+ scheduler=model.model_copy(update={"submodel_type": SubModelType.Scheduler}),
+ loras=[],
+ ),
+ clip=CLIPField(
+ tokenizer=model.model_copy(update={"submodel_type": SubModelType.Tokenizer}),
+ text_encoder=model.model_copy(update={"submodel_type": SubModelType.TextEncoder}),
+ loras=[],
+ skipped_layers=0,
+ ),
+ clip2=CLIPField(
+ tokenizer=model.model_copy(update={"submodel_type": SubModelType.Tokenizer2}),
+ text_encoder=model.model_copy(update={"submodel_type": SubModelType.TextEncoder2}),
+ loras=[],
+ skipped_layers=0,
+ ),
+ vae=VAEField(
+ vae=model.model_copy(update={"submodel_type": SubModelType.VAE}),
+ ),
+ )
+
+
+@invocation_output("latents_meta_output")
+class LatentsMetaOutput(LatentsOutput, MetadataOutput):
+ """Latents + metadata"""
+
+
+@invocation(
+ "denoise_latents_meta",
+ title=f"{DenoiseLatentsInvocation.UIConfig.title} + Metadata",
+ tags=["latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
+ category="metadata",
+ version="1.1.1",
+)
+class DenoiseLatentsMetaInvocation(DenoiseLatentsInvocation, WithMetadata):
+ def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
+ def _to_json(obj: Union[Any, list[Any]]):
+ if not isinstance(obj, list):
+ obj = [obj]
+
+ return [
+ item.model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
+ for item in obj
+ ]
+
+ def _loras_to_json(obj: Union[Any, list[Any]]):
+ if not isinstance(obj, list):
+ obj = [obj]
+
+ output: list[dict[str, Any]] = []
+ for item in obj:
+ output.append(
+ LoRAMetadataField(
+ model=item.lora,
+ weight=item.weight,
+ ).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
+ )
+ return output
+
+ obj = super().invoke(context)
+
+ md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ md.update({"width": obj.width})
+ md.update({"height": obj.height})
+ md.update({"steps": self.steps})
+ md.update({"cfg_scale": self.cfg_scale})
+ md.update({"cfg_rescale_multiplier": self.cfg_rescale_multiplier})
+ md.update({"denoising_start": self.denoising_start})
+ md.update({"denoising_end": self.denoising_end})
+ md.update({"scheduler": self.scheduler})
+ md.update({"model": self.unet.unet})
+ if isinstance(self.control, ControlField) or (isinstance(self.control, list) and len(self.control) > 0):
+ md.update({"controlnets": _to_json(self.control)})
+ if isinstance(self.ip_adapter, IPAdapterField) or (
+ isinstance(self.ip_adapter, list) and len(self.ip_adapter) > 0
+ ):
+ md.update({"ipAdapters": _to_json(self.ip_adapter)})
+ if isinstance(self.t2i_adapter, T2IAdapterField) or (
+ isinstance(self.t2i_adapter, list) and len(self.t2i_adapter) > 0
+ ):
+ md.update({"t2iAdapters": _to_json(self.t2i_adapter)})
+ if len(self.unet.loras) > 0:
+ md.update({"loras": _loras_to_json(self.unet.loras)})
+ if self.noise is not None:
+ md.update({"seed": self.noise.seed})
+
+ params = obj.__dict__.copy()
+ del params["type"]
+
+ return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
+
+
+@invocation(
+ "flux_denoise_meta",
+ title=f"{FluxDenoiseInvocation.UIConfig.title} + Metadata",
+ tags=["flux", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
+ category="metadata",
+ version="1.0.1",
+)
+class FluxDenoiseLatentsMetaInvocation(FluxDenoiseInvocation, WithMetadata):
+ """Run denoising process with a FLUX transformer model + metadata."""
+
+ def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
+ def _loras_to_json(obj: Union[Any, list[Any]]):
+ if not isinstance(obj, list):
+ obj = [obj]
+
+ output: list[dict[str, Any]] = []
+ for item in obj:
+ output.append(
+ LoRAMetadataField(
+ model=item.lora,
+ weight=item.weight,
+ ).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
+ )
+ return output
+
+ obj = super().invoke(context)
+
+ md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ md.update({"width": obj.width})
+ md.update({"height": obj.height})
+ md.update({"steps": self.num_steps})
+ md.update({"guidance": self.guidance})
+ md.update({"denoising_start": self.denoising_start})
+ md.update({"denoising_end": self.denoising_end})
+ md.update({"model": self.transformer.transformer})
+ md.update(
+ {
+ "seed": self.noise.seed
+ if self.noise is not None and self.noise.seed is not None and (self.latents is None or self.add_noise)
+ else self.seed
+ }
+ )
+ md.update({"cfg_scale": self.cfg_scale})
+ md.update({"cfg_scale_start_step": self.cfg_scale_start_step})
+ md.update({"cfg_scale_end_step": self.cfg_scale_end_step})
+ if len(self.transformer.loras) > 0:
+ md.update({"loras": _loras_to_json(self.transformer.loras)})
+
+ params = obj.__dict__.copy()
+ del params["type"]
+
+ return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
+
+
+@invocation(
+ "z_image_denoise_meta",
+ title=f"{ZImageDenoiseInvocation.UIConfig.title} + Metadata",
+ tags=["z-image", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
+ category="metadata",
+ version="1.1.0",
+)
+class ZImageDenoiseMetaInvocation(ZImageDenoiseInvocation, WithMetadata):
+ """Run denoising process with a Z-Image transformer model + metadata."""
+
+ def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
+ def _loras_to_json(obj: Union[Any, list[Any]]):
+ if not isinstance(obj, list):
+ obj = [obj]
+
+ output: list[dict[str, Any]] = []
+ for item in obj:
+ output.append(
+ LoRAMetadataField(
+ model=item.lora,
+ weight=item.weight,
+ ).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
+ )
+ return output
+
+ obj = super().invoke(context)
+
+ md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ md.update({"width": obj.width})
+ md.update({"height": obj.height})
+ md.update({"steps": self.steps})
+ md.update({"guidance": self.guidance_scale})
+ md.update({"denoising_start": self.denoising_start})
+ md.update({"denoising_end": self.denoising_end})
+ md.update({"scheduler": self.scheduler})
+ md.update({"model": self.transformer.transformer})
+ md.update(
+ {
+ "seed": self.noise.seed
+ if self.noise is not None and self.noise.seed is not None and (self.latents is None or self.add_noise)
+ else self.seed
+ }
+ )
+ if len(self.transformer.loras) > 0:
+ md.update({"loras": _loras_to_json(self.transformer.loras)})
+
+ params = obj.__dict__.copy()
+ del params["type"]
+
+ return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
+
+
+@invocation(
+ "metadata_to_vae",
+ title="Metadata To VAE",
+ tags=["metadata"],
+ category="metadata",
+ version="1.2.1",
+ classification=Classification.Beta,
+)
+class MetadataToVAEInvocation(BaseInvocation, WithMetadata):
+ """Extracts a VAE value of a label from metadata"""
+
+ label: CORE_LABELS_VAE = InputField(
+ default="vae",
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: VAEField = InputField(
+ description="The default VAE to use if not found in the metadata",
+ )
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> VAEOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ label = self.custom_label if self.label == CUSTOM_LABEL else self.label
+
+ model_key = extract_model_key(data, label, self.default_value.vae.key, ModelType.VAE, context)
+ model = get_model(model_key, context)
+ model.submodel_type = SubModelType.VAE
+
+ return VAEOutput(vae=VAEField(vae=model))
+
+
+@invocation_output("metadata_to_lora_collection_output")
+class MetadataToLorasCollectionOutput(BaseInvocationOutput):
+ """Model loader output"""
+
+ lora: list[LoRAField] = OutputField(description="Collection of LoRA model and weights", title="LoRAs")
+
+
+@invocation(
+ "metadata_to_lora_collection",
+ title="Metadata To LoRA Collection",
+ tags=["metadata"],
+ category="metadata",
+ version="1.1.0",
+ classification=Classification.Beta,
+)
+class MetadataToLorasCollectionInvocation(BaseInvocation, WithMetadata):
+ """Extracts Lora(s) from metadata into a collection"""
+
+ custom_label: str = InputField(
+ default="loras",
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=[], description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+
+ def invoke(self, context: InvocationContext) -> MetadataToLorasCollectionOutput:
+ metadata = {} if self.metadata is None else self.metadata.root
+ key: str = self.custom_label.strip()
+ if not key:
+ key = "loras"
+
+ if key in metadata:
+ loras = metadata[key]
+ else:
+ loras = []
+
+ input_loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ output = MetadataToLorasCollectionOutput(lora=[])
+ added_loras: list[str] = []
+
+ for lora in input_loras:
+ assert lora is LoRAField
+ if lora.lora.key in added_loras:
+ continue
+ output.lora.append(lora)
+ added_loras.append(lora.lora.key)
+
+ for lora in loras:
+ model_key = extract_model_key(lora, "model", "", ModelType.LoRA, context)
+ if not model_key:
+ model_key = extract_model_key(lora, "lora", "", ModelType.LoRA, context)
+ if model_key:
+ model = get_model(model_key, context)
+ weight = float(lora["weight"])
+ if model.key in added_loras:
+ continue
+ output.lora.append(LoRAField(lora=model, weight=weight))
+
+ return output
+
+
+@invocation(
+ "metadata_to_loras",
+ title="Metadata To LoRAs",
+ tags=["metadata"],
+ category="metadata",
+ version="1.1.1",
+ classification=Classification.Beta,
+)
+class MetadataToLorasInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Loras value of a label from metadata"""
+
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP",
+ )
+
+ def invoke(self, context: InvocationContext) -> LoRALoaderOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ key = "loras"
+ if key in data:
+ loras = data[key]
+ else:
+ loras = []
+
+ output = LoRALoaderOutput()
+
+ if self.unet is not None:
+ output.unet = copy.deepcopy(self.unet)
+
+ if self.clip is not None:
+ output.clip = copy.deepcopy(self.clip)
+
+ for lora in loras:
+ model_key = extract_model_key(lora, "model", "", ModelType.LoRA, context)
+ if model_key != "":
+ model = get_model(model_key, context)
+ weight = float(lora["weight"])
+
+ if output.unet is not None:
+ if any(lora.lora.key == model_key for lora in output.unet.loras):
+ context.logger.info(f'LoRA "{model_key}" already applied to unet')
+ else:
+ output.unet.loras.append(
+ LoRAField(
+ lora=model,
+ weight=weight,
+ )
+ )
+
+ if output.clip is not None:
+ if any(lora.lora.key == model_key for lora in output.clip.loras):
+ context.logger.info(f'LoRA "{model_key}" already applied to clip')
+ else:
+ output.clip.loras.append(
+ LoRAField(
+ lora=model,
+ weight=weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "metadata_to_sdlx_loras",
+ title="Metadata To SDXL LoRAs",
+ tags=["metadata"],
+ category="metadata",
+ version="1.1.1",
+ classification=Classification.Beta,
+)
+class MetadataToSDXLLorasInvocation(BaseInvocation, WithMetadata):
+ """Extracts a SDXL Loras value of a label from metadata"""
+
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP 1",
+ )
+ clip2: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP 2",
+ )
+
+ def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ key = "loras"
+ if key in data:
+ loras = data[key]
+ else:
+ loras = []
+
+ output = SDXLLoRALoaderOutput()
+
+ if self.unet is not None:
+ output.unet = copy.deepcopy(self.unet)
+
+ if self.clip is not None:
+ output.clip = copy.deepcopy(self.clip)
+
+ if self.clip2 is not None:
+ output.clip2 = copy.deepcopy(self.clip2)
+
+ for lora in loras:
+ model_key = extract_model_key(lora, "model", "", ModelType.LoRA, context)
+ if model_key != "":
+ model = get_model(model_key, context)
+ weight = float(lora["weight"])
+
+ if output.unet is not None:
+ if any(lora.lora.key == model_key for lora in output.unet.loras):
+ context.logger.info(f'LoRA "{model_key}" already applied to unet')
+ else:
+ output.unet.loras.append(
+ LoRAField(
+ lora=model,
+ weight=weight,
+ )
+ )
+
+ if output.clip is not None:
+ if any(lora.lora.key == model_key for lora in output.clip.loras):
+ context.logger.info(f'LoRA "{model_key}" already applied to clip')
+ else:
+ output.clip.loras.append(
+ LoRAField(
+ lora=model,
+ weight=weight,
+ )
+ )
+
+ if output.clip2 is not None:
+ if any(lora.lora.key == model_key for lora in output.clip2.loras):
+ context.logger.info(f'LoRA "{model_key}" already applied to clip')
+ else:
+ output.clip2.loras.append(
+ LoRAField(
+ lora=model,
+ weight=weight,
+ )
+ )
+
+ return output
+
+
+@invocation_output("md_control_list_output")
+class MDControlListOutput(BaseInvocationOutput):
+ # Outputs
+ control_list: Optional[Union[ControlField, list[ControlField]]] = OutputField(
+ description=FieldDescriptions.control,
+ title="ControlNet-List",
+ )
+
+
+@invocation(
+ "metadata_to_controlnets",
+ title="Metadata To ControlNets",
+ tags=["metadata"],
+ category="metadata",
+ version="1.2.0",
+ classification=Classification.Beta,
+)
+class MetadataToControlnetsInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Controlnets value of a label from metadata"""
+
+ control_list: Optional[Union[ControlField, list[ControlField]]] = InputField(
+ default=None,
+ title="ControlNet-List",
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> MDControlListOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ key = "controlnets"
+ if key in data:
+ md_controls = data[key]
+ else:
+ md_controls = []
+
+ controls: Optional[Union[ControlField, list[ControlField]]]
+
+ if self.control_list is not None:
+ controls = self.control_list
+ else:
+ controls = []
+
+ for x in md_controls:
+ model_key = extract_model_key(x, "control_model", "", ModelType.ControlNet, context)
+ model = get_model(model_key, context)
+
+ cn = ControlNetInvocation(
+ image=x["image"],
+ control_model=model,
+ control_weight=x["control_weight"],
+ begin_step_percent=x["begin_step_percent"],
+ end_step_percent=x["end_step_percent"],
+ control_mode=x["control_mode"],
+ resize_mode=x["resize_mode"],
+ )
+ i = cn.invoke(context)
+
+ controls = append_list(ControlField, i.control, controls)
+
+ return MDControlListOutput(control_list=controls)
+
+
+@invocation_output("md_ip_adapter_list_output")
+class MDIPAdapterListOutput(BaseInvocationOutput):
+ # Outputs
+ ip_adapter_list: Optional[Union[IPAdapterField, list[IPAdapterField]]] = OutputField(
+ description=FieldDescriptions.ip_adapter, title="IP-Adapter-List"
+ )
+
+
+@invocation(
+ "metadata_to_ip_adapters",
+ title="Metadata To IP-Adapters",
+ tags=["metadata"],
+ category="metadata",
+ version="1.2.0",
+ classification=Classification.Beta,
+)
+class MetadataToIPAdaptersInvocation(BaseInvocation, WithMetadata):
+ """Extracts a IP-Adapters value of a label from metadata"""
+
+ ip_adapter_list: Optional[Union[IPAdapterField, list[IPAdapterField]]] = InputField(
+ description=FieldDescriptions.ip_adapter,
+ title="IP-Adapter-List",
+ default=None,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> MDIPAdapterListOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ key = "ipAdapters"
+ if key in data:
+ md_adapters = data[key]
+ else:
+ md_adapters = []
+
+ adapters: Optional[Union[IPAdapterField, list[IPAdapterField]]]
+
+ if self.ip_adapter_list is not None:
+ adapters = self.ip_adapter_list
+ else:
+ adapters = []
+
+ for x in md_adapters:
+ model_key = extract_model_key(x, "ip_adapter_model", "", ModelType.IPAdapter, context)
+ model = get_model(model_key, context)
+
+ ipa = IPAdapterInvocation(
+ image=x["image"],
+ ip_adapter_model=model,
+ weight=x["weight"],
+ begin_step_percent=x["begin_step_percent"],
+ end_step_percent=x["end_step_percent"],
+ )
+ i = ipa.invoke(context)
+
+ adapters = append_list(IPAdapterField, i.ip_adapter, adapters)
+
+ return MDIPAdapterListOutput(ip_adapter_list=adapters)
+
+
+@invocation_output("md_ip_adapters_output")
+class MDT2IAdapterListOutput(BaseInvocationOutput):
+ # Outputs
+ t2i_adapter_list: Optional[Union[T2IAdapterField, list[T2IAdapterField]]] = OutputField(
+ description=FieldDescriptions.t2i_adapter, title="T2I Adapter-List"
+ )
+
+
+@invocation(
+ "metadata_to_t2i_adapters",
+ title="Metadata To T2I-Adapters",
+ tags=["metadata"],
+ category="metadata",
+ version="1.2.0",
+ classification=Classification.Beta,
+)
+class MetadataToT2IAdaptersInvocation(BaseInvocation, WithMetadata):
+ """Extracts a T2I-Adapters value of a label from metadata"""
+
+ t2i_adapter_list: Optional[Union[T2IAdapterField, list[T2IAdapterField]]] = InputField(
+ description=FieldDescriptions.ip_adapter,
+ title="T2I-Adapter",
+ default=None,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> MDT2IAdapterListOutput:
+ data = {} if self.metadata is None else self.metadata.root
+ key = "t2iAdapters"
+ if key in data:
+ md_adapters = data[key]
+ else:
+ md_adapters = []
+
+ adapters: Optional[Union[T2IAdapterField, list[T2IAdapterField]]]
+
+ if self.t2i_adapter_list is not None:
+ adapters = self.t2i_adapter_list
+ else:
+ adapters = []
+
+ for x in md_adapters:
+ model_key = extract_model_key(x, "t2i_adapter_model", "", ModelType.T2IAdapter, context)
+ model = get_model(model_key, context)
+
+ t2i = T2IAdapterInvocation(
+ image=x["image"],
+ t2i_adapter_model=model,
+ weight=x["weight"],
+ begin_step_percent=x["begin_step_percent"],
+ end_step_percent=x["end_step_percent"],
+ resize_mode=x["resize_mode"],
+ )
+ i = t2i.invoke(context)
+
+ adapters = append_list(T2IAdapterField, i.t2i_adapter, adapters)
+
+ return MDT2IAdapterListOutput(t2i_adapter_list=adapters)
+
+
+@invocation(
+ "metadata_to_string_collection",
+ title="Metadata To String Collection",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToStringCollectionInvocation(BaseInvocation, WithMetadata):
+ """Extracts a string collection value of a label from metadata"""
+
+ label: CORE_LABELS_STRING = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: list[str] = InputField(
+ description="The default string collection to use if not found in the metadata"
+ )
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> StringCollectionOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return StringCollectionOutput(collection=output)
+
+
+@invocation(
+ "metadata_to_integer_collection",
+ title="Metadata To Integer Collection",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToIntegerCollectionInvocation(BaseInvocation, WithMetadata):
+ """Extracts an integer value Collection of a label from metadata"""
+
+ label: CORE_LABELS_INTEGER = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: list[int] = InputField(description="The default integer to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return IntegerCollectionOutput(collection=output)
+
+
+@invocation(
+ "metadata_to_float_collection",
+ title="Metadata To Float Collection",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToFloatCollectionInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Float value Collection of a label from metadata"""
+
+ label: CORE_LABELS_FLOAT = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: list[float] = InputField(description="The default float to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> FloatCollectionOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return FloatCollectionOutput(collection=output)
+
+
+@invocation(
+ "metadata_to_bool_collection",
+ title="Metadata To Bool Collection",
+ tags=["metadata"],
+ category="metadata",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class MetadataToBoolCollectionInvocation(BaseInvocation, WithMetadata):
+ """Extracts a Boolean value Collection of a label from metadata"""
+
+ label: CORE_LABELS_BOOL = InputField(
+ default=CUSTOM_LABEL,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ custom_label: Optional[str] = InputField(
+ default=None,
+ description=FieldDescriptions.metadata_item_label,
+ input=Input.Direct,
+ )
+ default_value: list[bool] = InputField(description="The default bool to use if not found in the metadata")
+
+ _validate_custom_label = model_validator(mode="after")(validate_custom_label)
+
+ def invoke(self, context: InvocationContext) -> BooleanCollectionOutput:
+ data: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
+ output = data.get(str(self.custom_label if self.label == CUSTOM_LABEL else self.label), self.default_value)
+
+ return BooleanCollectionOutput(collection=output)
diff --git a/invokeai/app/invocations/mlsd.py b/invokeai/app/invocations/mlsd.py
new file mode 100644
index 00000000000..a2446876c88
--- /dev/null
+++ b/invokeai/app/invocations/mlsd.py
@@ -0,0 +1,39 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.mlsd import MLSDDetector
+from invokeai.backend.image_util.mlsd.models.mbv2_mlsd_large import MobileV2_MLSD_Large
+
+
+@invocation(
+ "mlsd_detection",
+ title="MLSD Detection",
+ tags=["controlnet", "mlsd", "edge"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class MLSDDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an line segment map using MLSD."""
+
+ image: ImageField = InputField(description="The image to process")
+ score_threshold: float = InputField(
+ default=0.1, ge=0, description="The threshold used to score points when determining line segments"
+ )
+ distance_threshold: float = InputField(
+ default=20.0,
+ ge=0,
+ description="Threshold for including a line segment - lines shorter than this distance will be discarded",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ loaded_model = context.models.load_remote_model(MLSDDetector.get_model_url(), MLSDDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, MobileV2_MLSD_Large)
+ detector = MLSDDetector(model)
+ edge_map = detector.run(image, self.score_threshold, self.distance_threshold)
+
+ image_dto = context.images.save(image=edge_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/model.py b/invokeai/app/invocations/model.py
new file mode 100644
index 00000000000..0c96cdb1d9d
--- /dev/null
+++ b/invokeai/app/invocations/model.py
@@ -0,0 +1,605 @@
+import copy
+from typing import List, Optional
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.shared.models import FreeUConfig
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+
+
+class ModelIdentifierField(BaseModel):
+ key: str = Field(description="The model's unique key")
+ hash: str = Field(description="The model's BLAKE3 hash")
+ name: str = Field(description="The model's name")
+ base: BaseModelType = Field(description="The model's base model type")
+ type: ModelType = Field(description="The model's type")
+ submodel_type: SubModelType | None = Field(
+ description="The submodel to load, if this is a main model",
+ default=None,
+ )
+
+ @classmethod
+ def from_config(
+ cls, config: "AnyModelConfig", submodel_type: Optional[SubModelType] = None
+ ) -> "ModelIdentifierField":
+ return cls(
+ key=config.key,
+ hash=config.hash,
+ name=config.name,
+ base=config.base,
+ type=config.type,
+ submodel_type=submodel_type,
+ )
+
+
+class LoRAField(BaseModel):
+ lora: ModelIdentifierField = Field(description="Info to load lora model")
+ weight: float = Field(description="Weight to apply to lora model")
+
+
+class UNetField(BaseModel):
+ unet: ModelIdentifierField = Field(description="Info to load unet submodel")
+ scheduler: ModelIdentifierField = Field(description="Info to load scheduler submodel")
+ loras: List[LoRAField] = 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')
+ freeu_config: Optional[FreeUConfig] = Field(default=None, description="FreeU configuration")
+
+
+class CLIPField(BaseModel):
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+ skipped_layers: int = Field(description="Number of skipped layers in text_encoder")
+ loras: List[LoRAField] = Field(description="LoRAs to apply on model loading")
+
+
+class T5EncoderField(BaseModel):
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+ loras: List[LoRAField] = Field(description="LoRAs to apply on model loading")
+
+
+class GlmEncoderField(BaseModel):
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+
+
+class QwenVLEncoderField(BaseModel):
+ """Field for Qwen2.5-VL encoder used by Qwen Image Edit models."""
+
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+
+
+class Qwen3EncoderField(BaseModel):
+ """Field for Qwen3 text encoder used by Z-Image models."""
+
+ tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
+ text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
+ loras: List[LoRAField] = Field(default_factory=list, description="LoRAs to apply on model loading")
+
+
+class VAEField(BaseModel):
+ vae: ModelIdentifierField = Field(description="Info to load vae submodel")
+ seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
+
+
+class ControlLoRAField(LoRAField):
+ img: ImageField = Field(description="Image to use in structural conditioning")
+
+
+class TransformerField(BaseModel):
+ transformer: ModelIdentifierField = Field(description="Info to load Transformer submodel")
+ loras: List[LoRAField] = Field(description="LoRAs to apply on model loading")
+
+
+@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")
+class ModelLoaderOutput(UNetOutput, CLIPOutput, VAEOutput):
+ """Model loader output"""
+
+ pass
+
+
+@invocation_output("model_identifier_output")
+class ModelIdentifierOutput(BaseInvocationOutput):
+ """Model identifier output"""
+
+ model: ModelIdentifierField = OutputField(description="Model identifier", title="Model")
+
+
+@invocation(
+ "model_identifier",
+ title="Any Model",
+ tags=["model"],
+ category="model",
+ version="1.0.1",
+)
+class ModelIdentifierInvocation(BaseInvocation):
+ """Selects any model, outputting it its identifier. Be careful with this one! The identifier will be accepted as
+ input for any model, even if the model types don't match. If you connect this to a mismatched input, you'll get an
+ error."""
+
+ model: ModelIdentifierField = InputField(description="The model to select", title="Model")
+
+ def invoke(self, context: InvocationContext) -> ModelIdentifierOutput:
+ if not context.models.exists(self.model.key):
+ raise Exception(f"Unknown model {self.model.key}")
+
+ return ModelIdentifierOutput(model=self.model)
+
+
+@invocation(
+ "main_model_loader",
+ title="Main Model - SD1.5, SD2",
+ tags=["model"],
+ category="model",
+ version="1.0.4",
+)
+class MainModelLoaderInvocation(BaseInvocation):
+ """Loads a main model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.main_model,
+ ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2],
+ ui_model_type=ModelType.Main,
+ )
+ # TODO: precision?
+
+ def invoke(self, context: InvocationContext) -> ModelLoaderOutput:
+ # TODO: not found exceptions
+ if not context.models.exists(self.model.key):
+ raise Exception(f"Unknown model {self.model.key}")
+
+ unet = self.model.model_copy(update={"submodel_type": SubModelType.UNet})
+ scheduler = self.model.model_copy(update={"submodel_type": SubModelType.Scheduler})
+ tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+
+ return ModelLoaderOutput(
+ unet=UNetField(unet=unet, scheduler=scheduler, loras=[]),
+ clip=CLIPField(tokenizer=tokenizer, text_encoder=text_encoder, loras=[], skipped_layers=0),
+ vae=VAEField(vae=vae),
+ )
+
+
+@invocation_output("lora_loader_output")
+class LoRALoaderOutput(BaseInvocationOutput):
+ """Model loader output"""
+
+ unet: Optional[UNetField] = OutputField(default=None, description=FieldDescriptions.unet, title="UNet")
+ clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP")
+
+
+@invocation("lora_loader", title="Apply LoRA - SD1.5", tags=["model"], category="model", version="1.0.4")
+class LoRALoaderInvocation(BaseInvocation):
+ """Apply selected lora to unet and text_encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.StableDiffusion1,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP",
+ )
+
+ def invoke(self, context: InvocationContext) -> LoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise Exception(f"Unknown lora: {lora_key}!")
+
+ if self.unet is not None and any(lora.lora.key == lora_key for lora in self.unet.loras):
+ raise Exception(f'LoRA "{lora_key}" already applied to unet')
+
+ if self.clip is not None and any(lora.lora.key == lora_key for lora in self.clip.loras):
+ raise Exception(f'LoRA "{lora_key}" already applied to clip')
+
+ output = LoRALoaderOutput()
+
+ if self.unet is not None:
+ output.unet = self.unet.model_copy(deep=True)
+ output.unet.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+ output.clip.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation_output("lora_selector_output")
+class LoRASelectorOutput(BaseInvocationOutput):
+ """Model loader output"""
+
+ lora: LoRAField = OutputField(description="LoRA model and weight", title="LoRA")
+
+
+@invocation("lora_selector", title="Select LoRA", tags=["model"], category="model", version="1.0.3")
+class LoRASelectorInvocation(BaseInvocation):
+ """Selects a LoRA model and weight."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+
+ def invoke(self, context: InvocationContext) -> LoRASelectorOutput:
+ return LoRASelectorOutput(lora=LoRAField(lora=self.lora, weight=self.weight))
+
+
+@invocation(
+ "lora_collection_loader", title="Apply LoRA Collection - SD1.5", tags=["model"], category="model", version="1.1.2"
+)
+class LoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to the provided UNet and CLIP models."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP",
+ )
+
+ def invoke(self, context: InvocationContext) -> LoRALoaderOutput:
+ output = LoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.unet is not None:
+ output.unet = self.unet.model_copy(deep=True)
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ assert lora.lora.base in (BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2)
+
+ added_loras.append(lora.lora.key)
+
+ if self.unet is not None and output.unet is not None:
+ output.unet.loras.append(lora)
+
+ if self.clip is not None and output.clip is not None:
+ output.clip.loras.append(lora)
+
+ return output
+
+
+@invocation_output("sdxl_lora_loader_output")
+class SDXLLoRALoaderOutput(BaseInvocationOutput):
+ """SDXL LoRA Loader Output"""
+
+ unet: Optional[UNetField] = OutputField(default=None, description=FieldDescriptions.unet, title="UNet")
+ clip: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP 1")
+ clip2: Optional[CLIPField] = OutputField(default=None, description=FieldDescriptions.clip, title="CLIP 2")
+
+
+@invocation(
+ "sdxl_lora_loader",
+ title="Apply LoRA - SDXL",
+ tags=["lora", "model"],
+ category="model",
+ version="1.0.5",
+)
+class SDXLLoRALoaderInvocation(BaseInvocation):
+ """Apply selected lora to unet and text_encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.StableDiffusionXL,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP 1",
+ )
+ clip2: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP 2",
+ )
+
+ def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise Exception(f"Unknown lora: {lora_key}!")
+
+ if self.unet is not None and any(lora.lora.key == lora_key for lora in self.unet.loras):
+ raise Exception(f'LoRA "{lora_key}" already applied to unet')
+
+ if self.clip is not None and any(lora.lora.key == lora_key for lora in self.clip.loras):
+ raise Exception(f'LoRA "{lora_key}" already applied to clip')
+
+ if self.clip2 is not None and any(lora.lora.key == lora_key for lora in self.clip2.loras):
+ raise Exception(f'LoRA "{lora_key}" already applied to clip2')
+
+ output = SDXLLoRALoaderOutput()
+
+ if self.unet is not None:
+ output.unet = self.unet.model_copy(deep=True)
+ output.unet.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+ output.clip.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ if self.clip2 is not None:
+ output.clip2 = self.clip2.model_copy(deep=True)
+ output.clip2.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "sdxl_lora_collection_loader",
+ title="Apply LoRA Collection - SDXL",
+ tags=["model"],
+ category="model",
+ version="1.1.2",
+)
+class SDXLLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of SDXL LoRAs to the provided UNet and CLIP models."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ clip: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP",
+ )
+ clip2: Optional[CLIPField] = InputField(
+ default=None,
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ title="CLIP 2",
+ )
+
+ def invoke(self, context: InvocationContext) -> SDXLLoRALoaderOutput:
+ output = SDXLLoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.unet is not None:
+ output.unet = self.unet.model_copy(deep=True)
+
+ if self.clip is not None:
+ output.clip = self.clip.model_copy(deep=True)
+
+ if self.clip2 is not None:
+ output.clip2 = self.clip2.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ assert lora.lora.base is BaseModelType.StableDiffusionXL
+
+ added_loras.append(lora.lora.key)
+
+ if self.unet is not None and output.unet is not None:
+ output.unet.loras.append(lora)
+
+ if self.clip is not None and output.clip is not None:
+ output.clip.loras.append(lora)
+
+ if self.clip2 is not None and output.clip2 is not None:
+ output.clip2.loras.append(lora)
+
+ return output
+
+
+@invocation(
+ "vae_loader",
+ title="VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
+ tags=["vae", "model"],
+ category="model",
+ version="1.0.4",
+)
+class VAELoaderInvocation(BaseInvocation):
+ """Loads a VAE model, outputting a VaeLoaderOutput"""
+
+ vae_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.vae_model,
+ title="VAE",
+ ui_model_base=[
+ BaseModelType.StableDiffusion1,
+ BaseModelType.StableDiffusion2,
+ BaseModelType.StableDiffusionXL,
+ BaseModelType.StableDiffusion3,
+ BaseModelType.Flux,
+ BaseModelType.Flux2,
+ ],
+ ui_model_type=ModelType.VAE,
+ )
+
+ def invoke(self, context: InvocationContext) -> VAEOutput:
+ key = self.vae_model.key
+
+ if not context.models.exists(key):
+ raise Exception(f"Unknown vae: {key}!")
+
+ return VAEOutput(vae=VAEField(vae=self.vae_model))
+
+
+@invocation_output("seamless_output")
+class SeamlessModeOutput(BaseInvocationOutput):
+ """Modified Seamless Model output"""
+
+ unet: Optional[UNetField] = OutputField(default=None, description=FieldDescriptions.unet, title="UNet")
+ vae: Optional[VAEField] = OutputField(default=None, description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "seamless",
+ title="Apply Seamless - SD1.5, SDXL",
+ tags=["seamless", "model"],
+ category="model",
+ version="1.0.2",
+)
+class SeamlessModeInvocation(BaseInvocation):
+ """Applies the seamless transformation to the Model UNet and VAE."""
+
+ unet: Optional[UNetField] = InputField(
+ default=None,
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ vae: Optional[VAEField] = InputField(
+ default=None,
+ 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_x: bool = InputField(default=True, input=Input.Any, description="Specify whether X axis is seamless")
+
+ def invoke(self, context: InvocationContext) -> SeamlessModeOutput:
+ # Conditionally append 'x' and 'y' based on seamless_x and seamless_y
+ unet = copy.deepcopy(self.unet)
+ vae = copy.deepcopy(self.vae)
+
+ seamless_axes_list = []
+
+ if self.seamless_x:
+ seamless_axes_list.append("x")
+ if self.seamless_y:
+ seamless_axes_list.append("y")
+
+ if unet is not None:
+ unet.seamless_axes = seamless_axes_list
+ if vae is not None:
+ vae.seamless_axes = seamless_axes_list
+
+ return SeamlessModeOutput(unet=unet, vae=vae)
+
+
+@invocation("freeu", title="Apply FreeU - SD1.5, SDXL", tags=["freeu"], category="model", version="1.0.2")
+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)
diff --git a/invokeai/app/invocations/noise.py b/invokeai/app/invocations/noise.py
new file mode 100644
index 00000000000..cfac3f112a9
--- /dev/null
+++ b/invokeai/app/invocations/noise.py
@@ -0,0 +1,84 @@
+import torch
+from pydantic import field_validator
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, LatentsField, OutputField
+from invokeai.app.invocations.latent_noise import (
+ LatentNoiseType,
+ generate_noise_tensor,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.misc import SEED_MAX
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation_output("noise_output")
+class NoiseOutput(BaseInvocationOutput):
+ """Invocation noise output"""
+
+ noise: LatentsField = OutputField(description=FieldDescriptions.noise)
+ width: int = OutputField(description=FieldDescriptions.width)
+ height: int = OutputField(description=FieldDescriptions.height)
+
+ @classmethod
+ def build(cls, latents_name: str, latents: torch.Tensor, seed: int) -> "NoiseOutput":
+ return cls(
+ noise=LatentsField(latents_name=latents_name, seed=seed),
+ width=latents.shape[-1] * LATENT_SCALE_FACTOR,
+ height=latents.shape[-2] * LATENT_SCALE_FACTOR,
+ )
+
+
+@invocation(
+ "noise",
+ title="Create Latent Noise",
+ tags=["latents", "noise"],
+ category="latents",
+ version="1.1.0",
+)
+class NoiseInvocation(BaseInvocation):
+ """Generates latent noise for supported denoiser architectures."""
+
+ noise_type: LatentNoiseType = InputField(default="SD", description="Architecture-specific noise type.")
+
+ seed: int = InputField(
+ default=0,
+ ge=0,
+ le=SEED_MAX,
+ description=FieldDescriptions.seed,
+ )
+ width: int = InputField(
+ default=512,
+ multiple_of=LATENT_SCALE_FACTOR,
+ gt=0,
+ description=FieldDescriptions.width,
+ )
+ height: int = InputField(
+ default=512,
+ multiple_of=LATENT_SCALE_FACTOR,
+ gt=0,
+ description=FieldDescriptions.height,
+ )
+ use_cpu: bool = InputField(
+ default=True,
+ description="Use CPU for noise generation (for reproducible results across platforms)",
+ )
+
+ @field_validator("seed", mode="before")
+ def modulo_seed(cls, v):
+ """Return the seed modulo (SEED_MAX + 1) to ensure it is within the valid range."""
+ return v % (SEED_MAX + 1)
+
+ def invoke(self, context: InvocationContext) -> NoiseOutput:
+ noise = generate_noise_tensor(
+ noise_type=self.noise_type,
+ width=self.width,
+ height=self.height,
+ device=TorchDevice.choose_torch_device(),
+ seed=self.seed,
+ dtype=TorchDevice.choose_torch_dtype(),
+ use_cpu=self.use_cpu,
+ )
+ name = context.tensors.save(tensor=noise)
+ return NoiseOutput.build(latents_name=name, latents=noise, seed=self.seed)
diff --git a/invokeai/app/invocations/normal_bae.py b/invokeai/app/invocations/normal_bae.py
new file mode 100644
index 00000000000..11599271500
--- /dev/null
+++ b/invokeai/app/invocations/normal_bae.py
@@ -0,0 +1,31 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.normal_bae import NormalMapDetector
+from invokeai.backend.image_util.normal_bae.nets.NNET import NNET
+
+
+@invocation(
+ "normal_map",
+ title="Normal Map",
+ tags=["controlnet", "normal"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class NormalMapInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates a normal map."""
+
+ image: ImageField = InputField(description="The image to process")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ loaded_model = context.models.load_remote_model(NormalMapDetector.get_model_url(), NormalMapDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, NNET)
+ detector = NormalMapDetector(model)
+ normal_map = detector.run(image=image)
+
+ image_dto = context.images.save(image=normal_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/param_easing.py b/invokeai/app/invocations/param_easing.py
new file mode 100644
index 00000000000..ed4318d95d1
--- /dev/null
+++ b/invokeai/app/invocations/param_easing.py
@@ -0,0 +1,28 @@
+import numpy as np
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import InputField
+from invokeai.app.invocations.primitives import FloatCollectionOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation(
+ "float_range",
+ title="Float Range",
+ tags=["math", "range"],
+ category="math",
+ version="1.0.1",
+)
+class FloatLinearRangeInvocation(BaseInvocation):
+ """Creates a range"""
+
+ start: float = InputField(default=5, description="The first value of the range")
+ stop: float = InputField(default=10, description="The last value of the range")
+ steps: int = InputField(
+ default=30,
+ description="number of values to interpolate over (including start and stop)",
+ )
+
+ def invoke(self, context: InvocationContext) -> FloatCollectionOutput:
+ param_list = list(np.linspace(self.start, self.stop, self.steps))
+ return FloatCollectionOutput(collection=param_list)
diff --git a/invokeai/app/invocations/pbr_maps.py b/invokeai/app/invocations/pbr_maps.py
new file mode 100644
index 00000000000..945c3cad598
--- /dev/null
+++ b/invokeai/app/invocations/pbr_maps.py
@@ -0,0 +1,61 @@
+import pathlib
+from typing import Literal
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.pbr_maps.architecture.pbr_rrdb_net import PBR_RRDB_Net
+from invokeai.backend.image_util.pbr_maps.pbr_maps import NORMAL_MAP_MODEL, OTHER_MAP_MODEL, PBRMapsGenerator
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation_output("pbr_maps-output")
+class PBRMapsOutput(BaseInvocationOutput):
+ normal_map: ImageField = OutputField(default=None, description="The generated normal map")
+ roughness_map: ImageField = OutputField(default=None, description="The generated roughness map")
+ displacement_map: ImageField = OutputField(default=None, description="The generated displacement map")
+
+
+@invocation(
+ "pbr_maps", title="PBR Maps", tags=["image", "material"], category="controlnet_preprocessors", version="1.0.0"
+)
+class PBRMapsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generate Normal, Displacement and Roughness Map from a given image"""
+
+ image: ImageField = InputField(description="Input image")
+ tile_size: int = InputField(default=512, description="Tile size")
+ border_mode: Literal["none", "seamless", "mirror", "replicate"] = InputField(
+ default="none", description="Border mode to apply to eliminate any artifacts or seams"
+ )
+
+ def invoke(self, context: InvocationContext) -> PBRMapsOutput:
+ image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
+
+ def loader(model_path: pathlib.Path):
+ return PBRMapsGenerator.load_model(model_path, TorchDevice.choose_torch_device())
+
+ torch_device = TorchDevice.choose_torch_device()
+
+ with (
+ context.models.load_remote_model(NORMAL_MAP_MODEL, loader) as normal_map_model,
+ context.models.load_remote_model(OTHER_MAP_MODEL, loader) as other_map_model,
+ ):
+ assert isinstance(normal_map_model, PBR_RRDB_Net)
+ assert isinstance(other_map_model, PBR_RRDB_Net)
+ pbr_pipeline = PBRMapsGenerator(normal_map_model, other_map_model, torch_device)
+ normal_map, roughness_map, displacement_map = pbr_pipeline.generate_maps(
+ image_pil, self.tile_size, self.border_mode
+ )
+
+ normal_map = context.images.save(normal_map)
+ normal_map_field = ImageField(image_name=normal_map.image_name)
+
+ roughness_map = context.images.save(roughness_map)
+ roughness_map_field = ImageField(image_name=roughness_map.image_name)
+
+ displacement_map = context.images.save(displacement_map)
+ displacement_map_field = ImageField(image_name=displacement_map.image_name)
+
+ return PBRMapsOutput(
+ normal_map=normal_map_field, roughness_map=roughness_map_field, displacement_map=displacement_map_field
+ )
diff --git a/invokeai/app/invocations/pidi.py b/invokeai/app/invocations/pidi.py
new file mode 100644
index 00000000000..5d8cab04589
--- /dev/null
+++ b/invokeai/app/invocations/pidi.py
@@ -0,0 +1,33 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.pidi import PIDINetDetector
+from invokeai.backend.image_util.pidi.model import PiDiNet
+
+
+@invocation(
+ "pidi_edge_detection",
+ title="PiDiNet Edge Detection",
+ tags=["controlnet", "edge"],
+ category="controlnet_preprocessors",
+ version="1.0.0",
+)
+class PiDiNetEdgeDetectionInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an edge map using PiDiNet."""
+
+ image: ImageField = InputField(description="The image to process")
+ quantize_edges: bool = InputField(default=False, description=FieldDescriptions.safe_mode)
+ scribble: bool = InputField(default=False, description=FieldDescriptions.scribble_mode)
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name, "RGB")
+ loaded_model = context.models.load_remote_model(PIDINetDetector.get_model_url(), PIDINetDetector.load_model)
+
+ with loaded_model as model:
+ assert isinstance(model, PiDiNet)
+ detector = PIDINetDetector(model)
+ edge_map = detector.run(image=image, quantize_edges=self.quantize_edges, scribble=self.scribble)
+
+ image_dto = context.images.save(image=edge_map)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/primitives.py b/invokeai/app/invocations/primitives.py
new file mode 100644
index 00000000000..7ec6c3dc149
--- /dev/null
+++ b/invokeai/app/invocations/primitives.py
@@ -0,0 +1,594 @@
+# Copyright (c) 2023 Kyle Schouviller (https://github.com/kyle0654)
+
+from typing import Optional
+
+import torch
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ AnimaConditioningField,
+ BoundingBoxField,
+ CogView4ConditioningField,
+ ColorField,
+ ConditioningField,
+ DenoiseMaskField,
+ FieldDescriptions,
+ FluxConditioningField,
+ ImageField,
+ Input,
+ InputField,
+ LatentsField,
+ OutputField,
+ QwenImageConditioningField,
+ SD3ConditioningField,
+ TensorField,
+ UIComponent,
+ ZImageConditioningField,
+)
+from invokeai.app.services.images.images_common import ImageDTO
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+"""
+Primitives: Boolean, Integer, Float, String, Image, Latents, Conditioning, Color
+- primitive nodes
+- primitive outputs
+- primitive collection outputs
+"""
+
+# region Boolean
+
+
+@invocation_output("boolean_output")
+class BooleanOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single boolean"""
+
+ value: bool = OutputField(description="The output boolean")
+
+
+@invocation_output("boolean_collection_output")
+class BooleanCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of booleans"""
+
+ collection: list[bool] = OutputField(
+ description="The output boolean collection",
+ )
+
+
+@invocation(
+ "boolean", title="Boolean Primitive", tags=["primitives", "boolean"], category="primitives", version="1.0.1"
+)
+class BooleanInvocation(BaseInvocation):
+ """A boolean primitive value"""
+
+ value: bool = InputField(default=False, description="The boolean value")
+
+ def invoke(self, context: InvocationContext) -> BooleanOutput:
+ return BooleanOutput(value=self.value)
+
+
+@invocation(
+ "boolean_collection",
+ title="Boolean Collection Primitive",
+ tags=["primitives", "boolean", "collection"],
+ category="primitives",
+ version="1.0.2",
+)
+class BooleanCollectionInvocation(BaseInvocation):
+ """A collection of boolean primitive values"""
+
+ collection: list[bool] = InputField(default=[], description="The collection of boolean values")
+
+ def invoke(self, context: InvocationContext) -> BooleanCollectionOutput:
+ return BooleanCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region Integer
+
+
+@invocation_output("integer_output")
+class IntegerOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single integer"""
+
+ value: int = OutputField(description="The output integer")
+
+
+@invocation_output("integer_collection_output")
+class IntegerCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of integers"""
+
+ collection: list[int] = OutputField(
+ description="The int collection",
+ )
+
+
+@invocation(
+ "integer", title="Integer Primitive", tags=["primitives", "integer"], category="primitives", version="1.0.1"
+)
+class IntegerInvocation(BaseInvocation):
+ """An integer primitive value"""
+
+ value: int = InputField(default=0, description="The integer value")
+
+ def invoke(self, context: InvocationContext) -> IntegerOutput:
+ return IntegerOutput(value=self.value)
+
+
+@invocation(
+ "integer_collection",
+ title="Integer Collection Primitive",
+ tags=["primitives", "integer", "collection"],
+ category="primitives",
+ version="1.0.2",
+)
+class IntegerCollectionInvocation(BaseInvocation):
+ """A collection of integer primitive values"""
+
+ collection: list[int] = InputField(default=[], description="The collection of integer values")
+
+ def invoke(self, context: InvocationContext) -> IntegerCollectionOutput:
+ return IntegerCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region Float
+
+
+@invocation_output("float_output")
+class FloatOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single float"""
+
+ value: float = OutputField(description="The output float")
+
+
+@invocation_output("float_collection_output")
+class FloatCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of floats"""
+
+ collection: list[float] = OutputField(
+ description="The float collection",
+ )
+
+
+@invocation("float", title="Float Primitive", tags=["primitives", "float"], category="primitives", version="1.0.1")
+class FloatInvocation(BaseInvocation):
+ """A float primitive value"""
+
+ value: float = InputField(default=0.0, description="The float value")
+
+ def invoke(self, context: InvocationContext) -> FloatOutput:
+ return FloatOutput(value=self.value)
+
+
+@invocation(
+ "float_collection",
+ title="Float Collection Primitive",
+ tags=["primitives", "float", "collection"],
+ category="primitives",
+ version="1.0.2",
+)
+class FloatCollectionInvocation(BaseInvocation):
+ """A collection of float primitive values"""
+
+ collection: list[float] = InputField(default=[], description="The collection of float values")
+
+ def invoke(self, context: InvocationContext) -> FloatCollectionOutput:
+ return FloatCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region String
+
+
+@invocation_output("string_output")
+class StringOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single string"""
+
+ value: str = OutputField(description="The output string")
+
+
+@invocation_output("string_collection_output")
+class StringCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of strings"""
+
+ collection: list[str] = OutputField(
+ description="The output strings",
+ )
+
+
+@invocation("string", title="String Primitive", tags=["primitives", "string"], category="primitives", version="1.0.1")
+class StringInvocation(BaseInvocation):
+ """A string primitive value"""
+
+ value: str = InputField(default="", description="The string value", ui_component=UIComponent.Textarea)
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ return StringOutput(value=self.value)
+
+
+@invocation(
+ "string_collection",
+ title="String Collection Primitive",
+ tags=["primitives", "string", "collection"],
+ category="primitives",
+ version="1.0.2",
+)
+class StringCollectionInvocation(BaseInvocation):
+ """A collection of string primitive values"""
+
+ collection: list[str] = InputField(default=[], description="The collection of string values")
+
+ def invoke(self, context: InvocationContext) -> StringCollectionOutput:
+ return StringCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region Image
+
+
+@invocation_output("image_output")
+class ImageOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single image"""
+
+ image: ImageField = OutputField(description="The output image")
+ width: int = OutputField(description="The width of the image in pixels")
+ height: int = OutputField(description="The height of the image in pixels")
+
+ @classmethod
+ def build(cls, image_dto: ImageDTO) -> "ImageOutput":
+ return cls(
+ image=ImageField(image_name=image_dto.image_name),
+ width=image_dto.width,
+ height=image_dto.height,
+ )
+
+
+@invocation_output("image_collection_output")
+class ImageCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of images"""
+
+ collection: list[ImageField] = OutputField(
+ description="The output images",
+ )
+
+
+@invocation("image", title="Image Primitive", tags=["primitives", "image"], category="primitives", version="1.0.2")
+class ImageInvocation(BaseInvocation):
+ """An image primitive value"""
+
+ image: ImageField = InputField(description="The image to load")
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image_dto = context.images.get_dto(self.image.image_name)
+
+ return ImageOutput.build(image_dto=image_dto)
+
+
+@invocation(
+ "image_collection",
+ title="Image Collection Primitive",
+ tags=["primitives", "image", "collection"],
+ category="primitives",
+ version="1.0.1",
+)
+class ImageCollectionInvocation(BaseInvocation):
+ """A collection of image primitive values"""
+
+ collection: list[ImageField] = InputField(description="The collection of image values")
+
+ def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
+ return ImageCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region DenoiseMask
+
+
+@invocation_output("denoise_mask_output")
+class DenoiseMaskOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single image"""
+
+ denoise_mask: DenoiseMaskField = OutputField(description="Mask for denoise model run")
+
+ @classmethod
+ def build(
+ cls, mask_name: str, masked_latents_name: Optional[str] = None, gradient: bool = False
+ ) -> "DenoiseMaskOutput":
+ return cls(
+ denoise_mask=DenoiseMaskField(
+ mask_name=mask_name, masked_latents_name=masked_latents_name, gradient=gradient
+ ),
+ )
+
+
+# endregion
+
+# region Latents
+
+
+@invocation_output("latents_output")
+class LatentsOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single latents tensor"""
+
+ latents: LatentsField = OutputField(description=FieldDescriptions.latents)
+ width: int = OutputField(description=FieldDescriptions.width)
+ height: int = OutputField(description=FieldDescriptions.height)
+
+ @classmethod
+ def build(cls, latents_name: str, latents: torch.Tensor, seed: Optional[int] = None) -> "LatentsOutput":
+ return cls(
+ latents=LatentsField(latents_name=latents_name, seed=seed),
+ width=latents.size()[3] * LATENT_SCALE_FACTOR,
+ height=latents.size()[2] * LATENT_SCALE_FACTOR,
+ )
+
+
+@invocation_output("latents_collection_output")
+class LatentsCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of latents tensors"""
+
+ collection: list[LatentsField] = OutputField(
+ description=FieldDescriptions.latents,
+ )
+
+
+@invocation(
+ "latents", title="Latents Primitive", tags=["primitives", "latents"], category="primitives", version="1.0.2"
+)
+class LatentsInvocation(BaseInvocation):
+ """A latents tensor primitive value"""
+
+ latents: LatentsField = InputField(description="The latents tensor", input=Input.Connection)
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ return LatentsOutput.build(self.latents.latents_name, latents)
+
+
+@invocation(
+ "latents_collection",
+ title="Latents Collection Primitive",
+ tags=["primitives", "latents", "collection"],
+ category="primitives",
+ version="1.0.1",
+)
+class LatentsCollectionInvocation(BaseInvocation):
+ """A collection of latents tensor primitive values"""
+
+ collection: list[LatentsField] = InputField(
+ description="The collection of latents tensors",
+ )
+
+ def invoke(self, context: InvocationContext) -> LatentsCollectionOutput:
+ return LatentsCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region Color
+
+
+@invocation_output("color_output")
+class ColorOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single color"""
+
+ color: ColorField = OutputField(description="The output color")
+
+
+@invocation_output("color_collection_output")
+class ColorCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of colors"""
+
+ collection: list[ColorField] = OutputField(
+ description="The output colors",
+ )
+
+
+@invocation("color", title="Color Primitive", tags=["primitives", "color"], category="primitives", version="1.0.1")
+class ColorInvocation(BaseInvocation):
+ """A color primitive value"""
+
+ color: ColorField = InputField(default=ColorField(r=0, g=0, b=0, a=255), description="The color value")
+
+ def invoke(self, context: InvocationContext) -> ColorOutput:
+ return ColorOutput(color=self.color)
+
+
+# endregion
+
+
+# region Conditioning
+
+
+@invocation_output("mask_output")
+class MaskOutput(BaseInvocationOutput):
+ """A torch mask tensor."""
+
+ # shape: [1, H, W], dtype: bool
+ mask: TensorField = OutputField(description="The mask.")
+ width: int = OutputField(description="The width of the mask in pixels.")
+ height: int = OutputField(description="The height of the mask in pixels.")
+
+
+@invocation_output("flux_conditioning_output")
+class FluxConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single conditioning tensor"""
+
+ conditioning: FluxConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "FluxConditioningOutput":
+ return cls(conditioning=FluxConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("flux_conditioning_collection_output")
+class FluxConditioningCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of conditioning tensors"""
+
+ collection: list[FluxConditioningField] = OutputField(
+ description="The output conditioning tensors",
+ )
+
+
+@invocation_output("sd3_conditioning_output")
+class SD3ConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single SD3 conditioning tensor"""
+
+ conditioning: SD3ConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "SD3ConditioningOutput":
+ return cls(conditioning=SD3ConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("cogview4_conditioning_output")
+class CogView4ConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a CogView text conditioning tensor."""
+
+ conditioning: CogView4ConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "CogView4ConditioningOutput":
+ return cls(conditioning=CogView4ConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("z_image_conditioning_output")
+class ZImageConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a Z-Image text conditioning tensor."""
+
+ conditioning: ZImageConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "ZImageConditioningOutput":
+ return cls(conditioning=ZImageConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("qwen_image_conditioning_output")
+class QwenImageConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a Qwen Image Edit conditioning tensor."""
+
+ conditioning: QwenImageConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "QwenImageConditioningOutput":
+ return cls(conditioning=QwenImageConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("anima_conditioning_output")
+class AnimaConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output an Anima text conditioning tensor."""
+
+ conditioning: AnimaConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "AnimaConditioningOutput":
+ return cls(conditioning=AnimaConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("conditioning_output")
+class ConditioningOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single conditioning tensor"""
+
+ conditioning: ConditioningField = OutputField(description=FieldDescriptions.cond)
+
+ @classmethod
+ def build(cls, conditioning_name: str) -> "ConditioningOutput":
+ return cls(conditioning=ConditioningField(conditioning_name=conditioning_name))
+
+
+@invocation_output("conditioning_collection_output")
+class ConditioningCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of conditioning tensors"""
+
+ collection: list[ConditioningField] = OutputField(
+ description="The output conditioning tensors",
+ )
+
+
+@invocation(
+ "conditioning",
+ title="Conditioning Primitive",
+ tags=["primitives", "conditioning"],
+ category="primitives",
+ version="1.0.1",
+)
+class ConditioningInvocation(BaseInvocation):
+ """A conditioning tensor primitive value"""
+
+ conditioning: ConditioningField = InputField(description=FieldDescriptions.cond, input=Input.Connection)
+
+ def invoke(self, context: InvocationContext) -> ConditioningOutput:
+ return ConditioningOutput(conditioning=self.conditioning)
+
+
+@invocation(
+ "conditioning_collection",
+ title="Conditioning Collection Primitive",
+ tags=["primitives", "conditioning", "collection"],
+ category="primitives",
+ version="1.0.2",
+)
+class ConditioningCollectionInvocation(BaseInvocation):
+ """A collection of conditioning tensor primitive values"""
+
+ collection: list[ConditioningField] = InputField(
+ default=[],
+ description="The collection of conditioning tensors",
+ )
+
+ def invoke(self, context: InvocationContext) -> ConditioningCollectionOutput:
+ return ConditioningCollectionOutput(collection=self.collection)
+
+
+# endregion
+
+# region BoundingBox
+
+
+@invocation_output("bounding_box_output")
+class BoundingBoxOutput(BaseInvocationOutput):
+ """Base class for nodes that output a single bounding box"""
+
+ bounding_box: BoundingBoxField = OutputField(description="The output bounding box.")
+
+
+@invocation_output("bounding_box_collection_output")
+class BoundingBoxCollectionOutput(BaseInvocationOutput):
+ """Base class for nodes that output a collection of bounding boxes"""
+
+ collection: list[BoundingBoxField] = OutputField(description="The output bounding boxes.", title="Bounding Boxes")
+
+
+@invocation(
+ "bounding_box",
+ title="Bounding Box",
+ tags=["primitives", "segmentation", "collection", "bounding box"],
+ category="primitives",
+ version="1.0.0",
+)
+class BoundingBoxInvocation(BaseInvocation):
+ """Create a bounding box manually by supplying box coordinates"""
+
+ x_min: int = InputField(default=0, description="x-coordinate of the bounding box's top left vertex")
+ y_min: int = InputField(default=0, description="y-coordinate of the bounding box's top left vertex")
+ x_max: int = InputField(default=0, description="x-coordinate of the bounding box's bottom right vertex")
+ y_max: int = InputField(default=0, description="y-coordinate of the bounding box's bottom right vertex")
+
+ def invoke(self, context: InvocationContext) -> BoundingBoxOutput:
+ bounding_box = BoundingBoxField(x_min=self.x_min, y_min=self.y_min, x_max=self.x_max, y_max=self.y_max)
+ return BoundingBoxOutput(bounding_box=bounding_box)
+
+
+# endregion
diff --git a/invokeai/app/invocations/prompt.py b/invokeai/app/invocations/prompt.py
new file mode 100644
index 00000000000..48eec0ac0ef
--- /dev/null
+++ b/invokeai/app/invocations/prompt.py
@@ -0,0 +1,102 @@
+from os.path import exists
+from typing import Optional, Union
+
+import numpy as np
+from dynamicprompts.generators import CombinatorialPromptGenerator, RandomPromptGenerator
+from pydantic import field_validator
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import InputField, UIComponent
+from invokeai.app.invocations.primitives import StringCollectionOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation(
+ "dynamic_prompt",
+ title="Dynamic Prompt",
+ tags=["prompt", "collection"],
+ category="prompt",
+ version="1.0.1",
+ use_cache=False,
+)
+class DynamicPromptInvocation(BaseInvocation):
+ """Parses a prompt using adieyal/dynamicprompts' random or combinatorial generator"""
+
+ prompt: str = InputField(
+ description="The prompt to parse with dynamicprompts",
+ ui_component=UIComponent.Textarea,
+ )
+ max_prompts: int = InputField(default=1, description="The number of prompts to generate")
+ combinatorial: bool = InputField(default=False, description="Whether to use the combinatorial generator")
+
+ def invoke(self, context: InvocationContext) -> StringCollectionOutput:
+ if self.combinatorial:
+ generator = CombinatorialPromptGenerator()
+ prompts = generator.generate(self.prompt, max_prompts=self.max_prompts)
+ else:
+ generator = RandomPromptGenerator()
+ prompts = generator.generate(self.prompt, num_images=self.max_prompts)
+
+ return StringCollectionOutput(collection=prompts)
+
+
+@invocation(
+ "prompt_from_file",
+ title="Prompts from File",
+ tags=["prompt", "file"],
+ category="prompt",
+ version="1.0.2",
+)
+class PromptsFromFileInvocation(BaseInvocation):
+ """Loads prompts from a text file"""
+
+ file_path: str = InputField(description="Path to prompt text file")
+ pre_prompt: Optional[str] = InputField(
+ default=None,
+ description="String to prepend to each prompt",
+ ui_component=UIComponent.Textarea,
+ )
+ post_prompt: Optional[str] = InputField(
+ default=None,
+ description="String to append to each prompt",
+ ui_component=UIComponent.Textarea,
+ )
+ start_line: int = InputField(default=1, ge=1, description="Line in the file to start start from")
+ max_prompts: int = InputField(default=1, ge=0, description="Max lines to read from file (0=all)")
+
+ @field_validator("file_path")
+ def file_path_exists(cls, v):
+ if not exists(v):
+ raise ValueError(FileNotFoundError)
+ return v
+
+ def promptsFromFile(
+ self,
+ file_path: str,
+ pre_prompt: Union[str, None],
+ post_prompt: Union[str, None],
+ start_line: int,
+ max_prompts: int,
+ ):
+ prompts = []
+ start_line -= 1
+ end_line = start_line + max_prompts
+ if max_prompts <= 0:
+ end_line = np.iinfo(np.int32).max
+ with open(file_path, encoding="utf-8") as f:
+ for i, line in enumerate(f):
+ if i >= start_line and i < end_line:
+ prompts.append((pre_prompt or "") + line.strip() + (post_prompt or ""))
+ if i >= end_line:
+ break
+ return prompts
+
+ def invoke(self, context: InvocationContext) -> StringCollectionOutput:
+ prompts = self.promptsFromFile(
+ self.file_path,
+ self.pre_prompt,
+ self.post_prompt,
+ self.start_line,
+ self.max_prompts,
+ )
+ return StringCollectionOutput(collection=prompts)
diff --git a/invokeai/app/invocations/prompt_template.py b/invokeai/app/invocations/prompt_template.py
new file mode 100644
index 00000000000..d2ac86358e5
--- /dev/null
+++ b/invokeai/app/invocations/prompt_template.py
@@ -0,0 +1,57 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import InputField, OutputField, StylePresetField, UIComponent
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("prompt_template_output")
+class PromptTemplateOutput(BaseInvocationOutput):
+ """Output for the Prompt Template node"""
+
+ positive_prompt: str = OutputField(description="The positive prompt with the template applied")
+ negative_prompt: str = OutputField(description="The negative prompt with the template applied")
+
+
+@invocation(
+ "prompt_template",
+ title="Prompt Template",
+ tags=["prompt", "template", "style", "preset"],
+ category="prompt",
+ version="1.0.0",
+)
+class PromptTemplateInvocation(BaseInvocation):
+ """Applies a Style Preset template to positive and negative prompts.
+
+ Select a Style Preset and provide positive/negative prompts. The node replaces
+ {prompt} placeholders in the template with your input prompts.
+ """
+
+ style_preset: StylePresetField = InputField(
+ description="The Style Preset to use as a template",
+ )
+ positive_prompt: str = InputField(
+ default="",
+ description="The positive prompt to insert into the template's {prompt} placeholder",
+ ui_component=UIComponent.Textarea,
+ )
+ negative_prompt: str = InputField(
+ default="",
+ description="The negative prompt to insert into the template's {prompt} placeholder",
+ ui_component=UIComponent.Textarea,
+ )
+
+ def invoke(self, context: InvocationContext) -> PromptTemplateOutput:
+ # Fetch the style preset from the database
+ style_preset = context._services.style_preset_records.get(self.style_preset.style_preset_id)
+
+ # Get the template prompts
+ positive_template = style_preset.preset_data.positive_prompt
+ negative_template = style_preset.preset_data.negative_prompt
+
+ # Replace {prompt} placeholder with the input prompts
+ rendered_positive = positive_template.replace("{prompt}", self.positive_prompt)
+ rendered_negative = negative_template.replace("{prompt}", self.negative_prompt)
+
+ return PromptTemplateOutput(
+ positive_prompt=rendered_positive,
+ negative_prompt=rendered_negative,
+ )
diff --git a/invokeai/app/invocations/qwen_image_denoise.py b/invokeai/app/invocations/qwen_image_denoise.py
new file mode 100644
index 00000000000..2dabc929bb1
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_denoise.py
@@ -0,0 +1,559 @@
+import math
+from contextlib import ExitStack
+from typing import Callable, ClassVar, Iterator, Optional, Tuple
+
+import torch
+import torchvision.transforms as tv_transforms
+from diffusers.models.transformers.transformer_qwenimage import QwenImageTransformer2DModel
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ QwenImageConditioningField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import TransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.qwen_image_lora_constants import (
+ QWEN_IMAGE_EDIT_LORA_TRANSFORMER_PREFIX,
+)
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import QwenImageConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "qwen_image_denoise",
+ title="Denoise - Qwen Image",
+ tags=["image", "qwen_image"],
+ category="image",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class QwenImageDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Run the denoising process with a Qwen Image model."""
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.latents, input=Input.Connection
+ )
+ # Reference image latents (encoded through VAE) to concatenate with noisy latents.
+ reference_latents: Optional[LatentsField] = InputField(
+ default=None,
+ description="Reference image latents to guide generation. Encoded through the VAE.",
+ input=Input.Connection,
+ )
+ # denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None, description=FieldDescriptions.denoise_mask, input=Input.Connection
+ )
+ 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)
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.qwen_image_model, input=Input.Connection, title="Transformer"
+ )
+ positive_conditioning: QwenImageConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: Optional[QwenImageConditioningField] = InputField(
+ default=None, description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ cfg_scale: float | list[float] = InputField(default=4.0, description=FieldDescriptions.cfg_scale, title="CFG Scale")
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ steps: int = InputField(default=40, gt=0, description=FieldDescriptions.steps)
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+ shift: Optional[float] = InputField(
+ default=None,
+ description="Override the sigma schedule shift. "
+ "When set, uses a fixed shift (e.g. 3.0 for Lightning LoRAs) instead of the default dynamic shifting. "
+ "Leave unset for the base model's default schedule.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask
+
+ def _load_text_conditioning(
+ self,
+ context: InvocationContext,
+ conditioning_name: str,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
+ cond_data = context.conditioning.load(conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ conditioning = cond_data.conditionings[0]
+ assert isinstance(conditioning, QwenImageConditioningInfo)
+ conditioning = conditioning.to(dtype=dtype, device=device)
+ return conditioning.prompt_embeds, conditioning.prompt_embeds_mask
+
+ def _get_noise(
+ self,
+ batch_size: int,
+ num_channels_latents: int,
+ height: int,
+ width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ seed: int,
+ ) -> torch.Tensor:
+ rand_device = "cpu"
+ rand_dtype = torch.float32
+
+ return torch.randn(
+ batch_size,
+ num_channels_latents,
+ int(height) // LATENT_SCALE_FACTOR,
+ int(width) // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+ def _prepare_cfg_scale(self, num_timesteps: int) -> list[float]:
+ if isinstance(self.cfg_scale, float):
+ cfg_scale = [self.cfg_scale] * num_timesteps
+ elif isinstance(self.cfg_scale, list):
+ assert len(self.cfg_scale) == num_timesteps
+ cfg_scale = self.cfg_scale
+ else:
+ raise ValueError(f"Invalid CFG scale type: {type(self.cfg_scale)}")
+ return cfg_scale
+
+ @staticmethod
+ def _pack_latents(
+ latents: torch.Tensor, batch_size: int, num_channels: int, height: int, width: int
+ ) -> torch.Tensor:
+ """Pack 4D latents (B, C, H, W) into 2x2-patched 3D (B, H/2*W/2, C*4)."""
+ latents = latents.view(batch_size, num_channels, height // 2, 2, width // 2, 2)
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
+ latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels * 4)
+ return latents
+
+ @staticmethod
+ def _unpack_latents(latents: torch.Tensor, height: int, width: int) -> torch.Tensor:
+ """Unpack 3D patched latents (B, seq, C*4) back to 4D (B, C, H, W)."""
+ batch_size, _num_patches, channels = latents.shape
+ # height/width are in latent space; they must be divisible by 2 for packing
+ h = 2 * (height // 2)
+ w = 2 * (width // 2)
+ latents = latents.view(batch_size, h // 2, w // 2, channels // 4, 2, 2)
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
+ latents = latents.reshape(batch_size, channels // 4, h, w)
+ return latents
+
+ @staticmethod
+ def _align_ref_latent_dims(rh: int, rw: int) -> tuple[int, int]:
+ """Trim reference latent spatial dims to even values for 2x2 packing.
+
+ Raises ValueError if the aligned dims would be < 2 (i.e., the reference
+ latent is too small to produce any valid tokens).
+ """
+ rh_aligned = rh - (rh % 2)
+ rw_aligned = rw - (rw % 2)
+ if rh_aligned < 2 or rw_aligned < 2:
+ raise ValueError(
+ f"Reference latent spatial dims must be >= 2 after even alignment; "
+ f"got ({rh_aligned}, {rw_aligned}) from input shape ({rh}, {rw}). "
+ "Ensure the reference image is at least 16 pixels in each dimension."
+ )
+ return rh_aligned, rw_aligned
+
+ @staticmethod
+ def _build_img_shapes(
+ latent_height: int,
+ latent_width: int,
+ ref_latent_height: int | None = None,
+ ref_latent_width: int | None = None,
+ ) -> list[list[tuple[int, int, int]]]:
+ """Build the img_shapes argument for the transformer.
+
+ The reference segment (if present) must use its own dims so QwenEmbedRope's
+ spatial frequencies position ref tokens distinctly from noisy tokens —
+ otherwise reference content bleeds into the generation as a ghost.
+ """
+ shapes: list[tuple[int, int, int]] = [(1, latent_height // 2, latent_width // 2)]
+ if ref_latent_height is not None and ref_latent_width is not None:
+ shapes.append((1, ref_latent_height // 2, ref_latent_width // 2))
+ return [shapes]
+
+ # diffusers' QwenImageEdit(Plus)Pipeline VAE_IMAGE_SIZE = 1024 * 1024 pixels;
+ # ref images are resized to this area (preserving aspect, snapped to multiples
+ # of 32) before VAE encoding. We mirror this clamp in latent space so direct
+ # backend callers — whose i2l may not pass explicit width/height — don't feed
+ # the transformer an out-of-distribution reference sequence length (which
+ # also causes a VRAM spike for large inputs).
+ _REF_TARGET_PIXEL_AREA: ClassVar[int] = 1024 * 1024
+ _VAE_SCALE_FACTOR: ClassVar[int] = 8
+
+ @classmethod
+ def _maybe_clamp_ref_latent_size(cls, ref_latents: torch.Tensor) -> torch.Tensor:
+ """Bilinear-downscale the reference latent if it exceeds diffusers'
+ VAE_IMAGE_SIZE budget.
+
+ Returns the latent unchanged if it's already within budget.
+ """
+ _, _, rh, rw = ref_latents.shape
+ target_cells = cls._REF_TARGET_PIXEL_AREA // (cls._VAE_SCALE_FACTOR**2)
+ if rh * rw <= target_cells:
+ return ref_latents
+ aspect = rw / rh
+ target_w_px = math.sqrt(cls._REF_TARGET_PIXEL_AREA * aspect)
+ target_h_px = target_w_px / aspect
+ target_w_px = max(32, round(target_w_px / 32) * 32)
+ target_h_px = max(32, round(target_h_px / 32) * 32)
+ target_rh = target_h_px // cls._VAE_SCALE_FACTOR
+ target_rw = target_w_px // cls._VAE_SCALE_FACTOR
+ return torch.nn.functional.interpolate(
+ ref_latents, size=(target_rh, target_rw), mode="bilinear", antialias=False
+ )
+
+ def _run_diffusion(self, context: InvocationContext):
+ inference_dtype = torch.bfloat16
+ device = TorchDevice.choose_torch_device()
+
+ transformer_info = context.models.load(self.transformer.transformer)
+ assert isinstance(transformer_info.model, QwenImageTransformer2DModel)
+
+ # Load conditioning
+ pos_prompt_embeds, pos_prompt_mask = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.positive_conditioning.conditioning_name,
+ dtype=inference_dtype,
+ device=device,
+ )
+
+ neg_prompt_embeds = None
+ neg_prompt_mask = None
+ # Match the diffusers pipeline: only enable CFG when cfg_scale > 1 AND negative conditioning is provided.
+ # With cfg_scale <= 1, the negative prediction is unused, so skip it entirely.
+ # For per-step arrays, enable CFG if any step has scale > 1.
+ if isinstance(self.cfg_scale, list):
+ any_cfg_above_one = any(v > 1.0 for v in self.cfg_scale)
+ else:
+ any_cfg_above_one = self.cfg_scale > 1.0
+ do_classifier_free_guidance = self.negative_conditioning is not None and any_cfg_above_one
+ if do_classifier_free_guidance:
+ neg_prompt_embeds, neg_prompt_mask = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.negative_conditioning.conditioning_name,
+ dtype=inference_dtype,
+ device=device,
+ )
+
+ # Prepare the timestep / sigma schedule
+ patch_size = transformer_info.model.config.patch_size
+ assert isinstance(patch_size, int)
+ # Output channels is 16 (the actual latent channels)
+ out_channels = transformer_info.model.config.out_channels
+ assert isinstance(out_channels, int)
+
+ latent_height = self.height // LATENT_SCALE_FACTOR
+ latent_width = self.width // LATENT_SCALE_FACTOR
+ image_seq_len = (latent_height * latent_width) // (patch_size**2)
+
+ # Use the actual FlowMatchEulerDiscreteScheduler to compute sigmas/timesteps,
+ # exactly matching the diffusers pipeline.
+ import math
+
+ import numpy as np
+ from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteScheduler
+
+ # Try to load the scheduler config from the model's directory (Diffusers models
+ # have a scheduler/ subdir). For GGUF models this path doesn't exist, so fall
+ # back to instantiating the scheduler with the known Qwen Image defaults.
+ model_path = context.models.get_absolute_path(context.models.get_config(self.transformer.transformer))
+ scheduler_path = model_path / "scheduler"
+ if scheduler_path.is_dir() and (scheduler_path / "scheduler_config.json").exists():
+ scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(str(scheduler_path), local_files_only=True)
+ else:
+ scheduler = FlowMatchEulerDiscreteScheduler(
+ use_dynamic_shifting=True,
+ base_shift=0.5,
+ max_shift=0.9,
+ base_image_seq_len=256,
+ max_image_seq_len=8192,
+ shift_terminal=0.02,
+ num_train_timesteps=1000,
+ time_shift_type="exponential",
+ )
+
+ if self.shift is not None:
+ # Lightning LoRA: fixed shift
+ mu = math.log(self.shift)
+ else:
+ # Default dynamic shifting
+ # Linear interpolation matching diffusers' calculate_shift
+ base_shift = scheduler.config.get("base_shift", 0.5)
+ max_shift = scheduler.config.get("max_shift", 0.9)
+ base_seq = scheduler.config.get("base_image_seq_len", 256)
+ max_seq = scheduler.config.get("max_image_seq_len", 4096)
+ m = (max_shift - base_shift) / (max_seq - base_seq)
+ b = base_shift - m * base_seq
+ mu = image_seq_len * m + b
+
+ init_sigmas = np.linspace(1.0, 1.0 / self.steps, self.steps).tolist()
+ scheduler.set_timesteps(sigmas=init_sigmas, mu=mu, device=device)
+
+ # Clip the schedule based on denoising_start/denoising_end to support img2img strength.
+ # The scheduler's sigmas go from high (noisy) to 0 (clean). We clip to the fractional range.
+ sigmas_sched = scheduler.sigmas # (N+1,) including terminal 0
+ if self.denoising_start > 0 or self.denoising_end < 1:
+ total_sigmas = len(sigmas_sched) - 1 # exclude terminal
+ start_idx = int(round(self.denoising_start * total_sigmas))
+ end_idx = int(round(self.denoising_end * total_sigmas))
+ sigmas_sched = sigmas_sched[start_idx : end_idx + 1] # +1 to include the next sigma for dt
+ # Rebuild timesteps from clipped sigmas (exclude terminal 0)
+ timesteps_sched = sigmas_sched[:-1] * scheduler.config.num_train_timesteps
+ else:
+ timesteps_sched = scheduler.timesteps
+
+ total_steps = len(timesteps_sched)
+
+ cfg_scale = self._prepare_cfg_scale(total_steps)
+
+ # Load initial latents if provided (for img2img)
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+ if init_latents.dim() == 5:
+ init_latents = init_latents.squeeze(2)
+
+ # Load reference image latents if provided
+ ref_latents = None
+ if self.reference_latents is not None:
+ ref_latents = context.tensors.load(self.reference_latents.latents_name)
+ ref_latents = ref_latents.to(device=device, dtype=inference_dtype)
+ # The VAE encoder produces 5D latents (B, C, 1, H, W); squeeze the frame dim
+ # so we have 4D (B, C, H, W) for packing.
+ if ref_latents.dim() == 5:
+ ref_latents = ref_latents.squeeze(2)
+
+ # Generate noise (16 channels - the output latent channels)
+ noise = self._get_noise(
+ batch_size=1,
+ num_channels_latents=out_channels,
+ height=self.height,
+ width=self.width,
+ dtype=inference_dtype,
+ device=device,
+ seed=self.seed,
+ )
+
+ # Prepare input latent image
+ if init_latents is not None:
+ s_0 = sigmas_sched[0].item()
+ latents = s_0 * noise + (1.0 - s_0) * init_latents
+ else:
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ latents = noise
+
+ if total_steps <= 0:
+ return latents
+
+ # Pack latents into 2x2 patches: (B, C, H, W) -> (B, H/2*W/2, C*4)
+ latents = self._pack_latents(latents, 1, out_channels, latent_height, latent_width)
+
+ # Determine whether the model uses reference latent conditioning (zero_cond_t).
+ # Edit models (zero_cond_t=True) expect [noisy_patches ; ref_patches] in the sequence.
+ # Txt2img models (zero_cond_t=False) only take noisy patches.
+ has_zero_cond_t = getattr(transformer_info.model, "zero_cond_t", False) or getattr(
+ transformer_info.model.config, "zero_cond_t", False
+ )
+ use_ref_latents = has_zero_cond_t
+
+ ref_latents_packed = None
+ ref_latent_height = latent_height
+ ref_latent_width = latent_width
+ if use_ref_latents:
+ if ref_latents is not None:
+ # Defense-in-depth: backend callers (direct API, older graph JSON)
+ # may wire qwen_image_i2l without explicit width/height, producing
+ # a native-resolution reference latent. Clamp here so the
+ # transformer always sees an in-distribution sequence length.
+ ref_latents = self._maybe_clamp_ref_latent_size(ref_latents)
+ _, _, rh, rw = ref_latents.shape
+ ref_latent_height, ref_latent_width = self._align_ref_latent_dims(rh, rw)
+ if ref_latent_height != rh or ref_latent_width != rw:
+ ref_latents = ref_latents[..., :ref_latent_height, :ref_latent_width]
+ else:
+ # No reference image provided — use zeros so the model still gets the
+ # expected sequence layout.
+ ref_latents = torch.zeros(
+ 1, out_channels, latent_height, latent_width, device=device, dtype=inference_dtype
+ )
+ ref_latents_packed = self._pack_latents(ref_latents, 1, out_channels, ref_latent_height, ref_latent_width)
+
+ # img_shapes tells the transformer the spatial layout of patches. The reference
+ # segment must use the reference latent's own dimensions so RoPE positions it
+ # distinctly from the noisy latent — otherwise the two segments share spatial
+ # positional encoding and the model can't disentangle them, producing a
+ # ghost/doubling artifact across the whole frame. Matches diffusers'
+ # QwenImageEditPipeline / QwenImageEditPlusPipeline.
+ if use_ref_latents:
+ img_shapes = self._build_img_shapes(latent_height, latent_width, ref_latent_height, ref_latent_width)
+ else:
+ img_shapes = self._build_img_shapes(latent_height, latent_width)
+
+ # Prepare inpaint extension (operates in 4D space, so unpack/repack around it)
+ inpaint_mask = self._prep_inpaint_mask(context, noise) # noise has the right 4D shape
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ assert init_latents is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents,
+ inpaint_mask=inpaint_mask,
+ noise=noise,
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=0,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(timesteps_sched[0].item()) if len(timesteps_sched) > 0 else 0,
+ latents=self._unpack_latents(latents, latent_height, latent_width),
+ ),
+ )
+
+ noisy_seq_len = latents.shape[1]
+
+ # Determine if the model is quantized — GGUF models need sidecar patching for LoRAs
+ transformer_config = context.models.get_config(self.transformer.transformer)
+ model_is_quantized = transformer_config.format in (ModelFormat.GGUFQuantized,)
+
+ with ExitStack() as exit_stack:
+ (cached_weights, transformer) = exit_stack.enter_context(transformer_info.model_on_device())
+ assert isinstance(transformer, QwenImageTransformer2DModel)
+
+ # Apply LoRA patches to the transformer
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=transformer,
+ patches=self._lora_iterator(context),
+ prefix=QWEN_IMAGE_EDIT_LORA_TRANSFORMER_PREFIX,
+ dtype=inference_dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=model_is_quantized,
+ )
+ )
+
+ for step_idx, t in enumerate(tqdm(timesteps_sched)):
+ # The pipeline passes timestep / 1000 to the transformer
+ timestep = t.expand(latents.shape[0]).to(inference_dtype)
+
+ # For edit models: concatenate noisy and reference patches along the sequence dim
+ # For txt2img models: just use noisy patches
+ if ref_latents_packed is not None:
+ model_input = torch.cat([latents, ref_latents_packed], dim=1)
+ else:
+ model_input = latents
+
+ noise_pred_cond = transformer(
+ hidden_states=model_input,
+ encoder_hidden_states=pos_prompt_embeds,
+ encoder_hidden_states_mask=pos_prompt_mask,
+ timestep=timestep / 1000,
+ img_shapes=img_shapes,
+ return_dict=False,
+ )[0]
+ # Only keep the noisy-latent portion of the output
+ noise_pred_cond = noise_pred_cond[:, :noisy_seq_len]
+
+ if do_classifier_free_guidance and neg_prompt_embeds is not None:
+ noise_pred_uncond = transformer(
+ hidden_states=model_input,
+ encoder_hidden_states=neg_prompt_embeds,
+ encoder_hidden_states_mask=neg_prompt_mask,
+ timestep=timestep / 1000,
+ img_shapes=img_shapes,
+ return_dict=False,
+ )[0]
+ noise_pred_uncond = noise_pred_uncond[:, :noisy_seq_len]
+
+ noise_pred = noise_pred_uncond + cfg_scale[step_idx] * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ # Euler step using the (possibly clipped) sigma schedule
+ sigma_curr = sigmas_sched[step_idx]
+ sigma_next = sigmas_sched[step_idx + 1]
+ dt = sigma_next - sigma_curr
+ latents = latents.to(torch.float32) + dt * noise_pred.to(torch.float32)
+ latents = latents.to(inference_dtype)
+
+ if inpaint_extension is not None:
+ sigma_next = sigmas_sched[step_idx + 1].item()
+ latents_4d = self._unpack_latents(latents, latent_height, latent_width)
+ latents_4d = inpaint_extension.merge_intermediate_latents_with_init_latents(latents_4d, sigma_next)
+ latents = self._pack_latents(latents_4d, 1, out_channels, latent_height, latent_width)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(t.item()),
+ latents=self._unpack_latents(latents, latent_height, latent_width),
+ ),
+ )
+
+ # Unpack back to 4D then add frame dim for the video-style VAE: (B, C, 1, H, W)
+ latents = self._unpack_latents(latents, latent_height, latent_width)
+ latents = latents.unsqueeze(2)
+ return latents
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.QwenImage)
+
+ return step_callback
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the transformer."""
+ for lora in self.transformer.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/qwen_image_image_to_latents.py b/invokeai/app/invocations/qwen_image_image_to_latents.py
new file mode 100644
index 00000000000..ef88e03082b
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_image_to_latents.py
@@ -0,0 +1,98 @@
+import einops
+import torch
+from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage
+from PIL import Image as PILImage
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "qwen_image_i2l",
+ title="Image to Latents - Qwen Image",
+ tags=["image", "latents", "vae", "i2l", "qwen_image"],
+ category="image",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class QwenImageImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates latents from an image using the Qwen Image VAE."""
+
+ image: ImageField = InputField(description="The image to encode.")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+ width: int | None = InputField(
+ default=None,
+ description="Resize the image to this width before encoding. If not set, encodes at the image's original size.",
+ )
+ height: int | None = InputField(
+ default=None,
+ description="Resize the image to this height before encoding. If not set, encodes at the image's original size.",
+ )
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ with vae_info.model_on_device() as (_, vae):
+ assert isinstance(vae, AutoencoderKLQwenImage)
+
+ vae.disable_tiling()
+
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype)
+ with torch.inference_mode():
+ # The Qwen Image VAE expects 5D input: (B, C, num_frames, H, W)
+ if image_tensor.dim() == 4:
+ image_tensor = image_tensor.unsqueeze(2)
+
+ posterior = vae.encode(image_tensor).latent_dist
+ # Use mode (argmax) for deterministic encoding, matching diffusers
+ latents: torch.Tensor = posterior.mode().to(dtype=vae.dtype)
+
+ # Normalize with per-channel latents_mean / latents_std
+ latents_mean = (
+ torch.tensor(vae.config.latents_mean)
+ .view(1, vae.config.z_dim, 1, 1, 1)
+ .to(latents.device, latents.dtype)
+ )
+ latents_std = (
+ torch.tensor(vae.config.latents_std)
+ .view(1, vae.config.z_dim, 1, 1, 1)
+ .to(latents.device, latents.dtype)
+ )
+ latents = (latents - latents_mean) / latents_std
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ # If target dimensions are specified, resize the image BEFORE encoding
+ # (matching the diffusers pipeline which resizes in pixel space, not latent space).
+ if self.width is not None and self.height is not None:
+ image = image.convert("RGB").resize((self.width, self.height), resample=PILImage.LANCZOS)
+
+ # multiple_of=16 ensures the post-VAE latents (vae_scale_factor=8) have even
+ # spatial dims, which the transformer's 2x2 patch packing requires.
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"), multiple_of=16)
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/qwen_image_latents_to_image.py b/invokeai/app/invocations/qwen_image_latents_to_image.py
new file mode 100644
index 00000000000..b3ea39c4bbf
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_latents_to_image.py
@@ -0,0 +1,85 @@
+from contextlib import nullcontext
+
+import torch
+from diffusers.models.autoencoders.autoencoder_kl_qwenimage import AutoencoderKLQwenImage
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "qwen_image_l2i",
+ title="Latents to Image - Qwen Image",
+ tags=["latents", "image", "vae", "l2i", "qwen_image"],
+ category="latents",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class QwenImageLatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents using the Qwen Image VAE."""
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, AutoencoderKLQwenImage)
+ with (
+ SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
+ vae_info.model_on_device() as (_, vae),
+ ):
+ context.util.signal_progress("Running VAE")
+ assert isinstance(vae, AutoencoderKLQwenImage)
+ latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype)
+
+ vae.disable_tiling()
+
+ tiling_context = nullcontext()
+
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode(), tiling_context:
+ # The Qwen Image VAE uses per-channel latents_mean / latents_std
+ # instead of a single scaling_factor.
+ # Latents are 5D: (B, C, num_frames, H, W) — the unpack from the
+ # denoise step already produces this shape.
+ latents_mean = (
+ torch.tensor(vae.config.latents_mean)
+ .view(1, vae.config.z_dim, 1, 1, 1)
+ .to(latents.device, latents.dtype)
+ )
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(1, vae.config.z_dim, 1, 1, 1).to(
+ latents.device, latents.dtype
+ )
+ latents = latents / latents_std + latents_mean
+
+ img = vae.decode(latents, return_dict=False)[0]
+ # Drop the temporal frame dimension: (B, C, 1, H, W) -> (B, C, H, W)
+ img = img[:, :, 0]
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c")
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/qwen_image_lora_loader.py b/invokeai/app/invocations/qwen_image_lora_loader.py
new file mode 100644
index 00000000000..f670b2d8954
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_lora_loader.py
@@ -0,0 +1,115 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, TransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("qwen_image_lora_loader_output")
+class QwenImageLoRALoaderOutput(BaseInvocationOutput):
+ """Qwen Image LoRA Loader Output"""
+
+ transformer: Optional[TransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="Transformer"
+ )
+
+
+@invocation(
+ "qwen_image_lora_loader",
+ title="Apply LoRA - Qwen Image",
+ tags=["lora", "model", "qwen_image"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class QwenImageLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA model to a Qwen Image transformer."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.QwenImage,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=1.0, description=FieldDescriptions.lora_weight)
+ transformer: TransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+
+ def invoke(self, context: InvocationContext) -> QwenImageLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
+
+ output = QwenImageLoRALoaderOutput()
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ output.transformer.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "qwen_image_lora_collection_loader",
+ title="Apply LoRA Collection - Qwen Image",
+ tags=["lora", "model", "qwen_image"],
+ category="model",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class QwenImageLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to a Qwen Image transformer."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+ transformer: Optional[TransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+
+ def invoke(self, context: InvocationContext) -> QwenImageLoRALoaderOutput:
+ output = QwenImageLoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ added_loras.append(lora.lora.key)
+
+ if self.transformer is not None and output.transformer is not None:
+ output.transformer.loras.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/qwen_image_model_loader.py b/invokeai/app/invocations/qwen_image_model_loader.py
new file mode 100644
index 00000000000..b3e86d1bf4a
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_model_loader.py
@@ -0,0 +1,147 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ ModelIdentifierField,
+ QwenVLEncoderField,
+ TransformerField,
+ VAEField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType
+
+
+@invocation_output("qwen_image_model_loader_output")
+class QwenImageModelLoaderOutput(BaseInvocationOutput):
+ """Qwen Image model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ qwen_vl_encoder: QwenVLEncoderField = OutputField(
+ description=FieldDescriptions.qwen_vl_encoder, title="Qwen VL Encoder"
+ )
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "qwen_image_model_loader",
+ title="Main Model - Qwen Image",
+ tags=["model", "qwen_image"],
+ category="model",
+ version="1.2.0",
+ classification=Classification.Prototype,
+)
+class QwenImageModelLoaderInvocation(BaseInvocation):
+ """Loads a Qwen Image model, outputting its submodels.
+
+ The transformer is always loaded from the main model (Diffusers or GGUF).
+
+ Components can be mixed and matched:
+ - VAE: standalone Qwen Image VAE checkpoint, the Component Source (Diffusers),
+ or the main model if it's Diffusers.
+ - Qwen VL Encoder: standalone Qwen2.5-VL encoder, the Component Source
+ (Diffusers), or the main model if it's Diffusers.
+
+ Together, the standalone VAE and standalone encoder allow running a GGUF
+ transformer without ever downloading the full ~40 GB Diffusers pipeline.
+ """
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.qwen_image_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.QwenImage,
+ ui_model_type=ModelType.Main,
+ title="Transformer",
+ )
+
+ vae_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Qwen Image VAE model. "
+ "If not provided, VAE will be loaded from the Component Source (or from the main model if it is Diffusers).",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.QwenImage,
+ ui_model_type=ModelType.VAE,
+ title="VAE",
+ )
+
+ qwen_vl_encoder_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Qwen2.5-VL encoder model. "
+ "If not provided, the encoder will be loaded from the Component Source "
+ "(or from the main model if it is Diffusers).",
+ input=Input.Direct,
+ ui_model_type=ModelType.QwenVLEncoder,
+ title="Qwen VL Encoder",
+ )
+
+ component_source: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Diffusers Qwen Image model to extract VAE and/or Qwen VL encoder from. "
+ "Use this if you don't have separate VAE/encoder models. "
+ "Ignored for any submodel that is provided separately.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.QwenImage,
+ ui_model_type=ModelType.Main,
+ ui_model_format=ModelFormat.Diffusers,
+ title="Component Source (Diffusers)",
+ )
+
+ def invoke(self, context: InvocationContext) -> QwenImageModelLoaderOutput:
+ main_config = context.models.get_config(self.model)
+ main_is_diffusers = main_config.format == ModelFormat.Diffusers
+
+ # Transformer always comes from the main model
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+
+ # Resolve VAE: standalone override > main (if Diffusers) > component source
+ if self.vae_model is not None:
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif main_is_diffusers:
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif self.component_source is not None:
+ self._validate_component_source_format(context, self.component_source)
+ vae = self.component_source.model_copy(update={"submodel_type": SubModelType.VAE})
+ else:
+ raise ValueError(
+ "No source for VAE. Either set 'VAE' to a standalone Qwen Image VAE, "
+ "or set 'Component Source' to a Diffusers Qwen Image model."
+ )
+
+ # Resolve Qwen VL encoder: standalone override > main (if Diffusers) > component source
+ if self.qwen_vl_encoder_model is not None:
+ tokenizer = self.qwen_vl_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.qwen_vl_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif main_is_diffusers:
+ tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif self.component_source is not None:
+ self._validate_component_source_format(context, self.component_source)
+ tokenizer = self.component_source.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.component_source.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ else:
+ raise ValueError(
+ "No source for Qwen VL encoder. "
+ "Either set 'Qwen VL Encoder' to a standalone Qwen2.5-VL encoder, "
+ "or set 'Component Source' to a Diffusers Qwen Image model."
+ )
+
+ return QwenImageModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ qwen_vl_encoder=QwenVLEncoderField(tokenizer=tokenizer, text_encoder=text_encoder),
+ vae=VAEField(vae=vae),
+ )
+
+ @staticmethod
+ def _validate_component_source_format(context: InvocationContext, model: ModelIdentifierField) -> None:
+ source_config = context.models.get_config(model)
+ if source_config.format != ModelFormat.Diffusers:
+ raise ValueError(
+ f"The Component Source model must be in Diffusers format. "
+ f"The selected model '{source_config.name}' is in {source_config.format.value} format."
+ )
diff --git a/invokeai/app/invocations/qwen_image_text_encoder.py b/invokeai/app/invocations/qwen_image_text_encoder.py
new file mode 100644
index 00000000000..d2aecd9f226
--- /dev/null
+++ b/invokeai/app/invocations/qwen_image_text_encoder.py
@@ -0,0 +1,322 @@
+from typing import Literal
+
+import torch
+from PIL import Image as PILImage
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ UIComponent,
+)
+from invokeai.app.invocations.model import QwenVLEncoderField
+from invokeai.app.invocations.primitives import QwenImageConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ ConditioningFieldData,
+ QwenImageConditioningInfo,
+)
+
+# Prompt templates and drop indices for the two Qwen Image model modes.
+# These are taken directly from the diffusers pipelines.
+
+# Image editing mode (QwenImagePipeline)
+_EDIT_SYSTEM_PROMPT = (
+ "Describe the key features of the input image (color, shape, size, texture, objects, background), "
+ "then explain how the user's text instruction should alter or modify the image. "
+ "Generate a new image that meets the user's requirements while maintaining consistency "
+ "with the original input where appropriate."
+)
+_EDIT_DROP_IDX = 64
+
+# Text-to-image mode (QwenImagePipeline)
+_GENERATE_SYSTEM_PROMPT = (
+ "Describe the image by detailing the color, shape, size, texture, quantity, "
+ "text, spatial relationships of the objects and background:"
+)
+_GENERATE_DROP_IDX = 34
+
+_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"
+
+
+def _build_prompt(user_prompt: str, num_images: int) -> str:
+ """Build the full prompt with the appropriate template based on whether reference images are provided."""
+ if num_images > 0:
+ # Edit mode: include vision placeholders for reference images
+ image_tokens = _IMAGE_PLACEHOLDER * num_images
+ return (
+ f"<|im_start|>system\n{_EDIT_SYSTEM_PROMPT}<|im_end|>\n"
+ f"<|im_start|>user\n{image_tokens}{user_prompt}<|im_end|>\n"
+ "<|im_start|>assistant\n"
+ )
+ else:
+ # Generate mode: text-only prompt
+ return (
+ f"<|im_start|>system\n{_GENERATE_SYSTEM_PROMPT}<|im_end|>\n"
+ f"<|im_start|>user\n{user_prompt}<|im_end|>\n"
+ "<|im_start|>assistant\n"
+ )
+
+
+@invocation(
+ "qwen_image_text_encoder",
+ title="Prompt - Qwen Image",
+ tags=["prompt", "conditioning", "qwen_image"],
+ category="conditioning",
+ version="1.2.0",
+ classification=Classification.Prototype,
+)
+class QwenImageTextEncoderInvocation(BaseInvocation):
+ """Encodes text and reference images for Qwen Image using Qwen2.5-VL."""
+
+ prompt: str = InputField(description="Text prompt describing the desired edit.", ui_component=UIComponent.Textarea)
+ reference_images: list[ImageField] = InputField(
+ default=[],
+ description="Reference images to guide the edit. The model can use multiple reference images.",
+ )
+ qwen_vl_encoder: QwenVLEncoderField = InputField(
+ title="Qwen VL Encoder",
+ description=FieldDescriptions.qwen_vl_encoder,
+ input=Input.Connection,
+ )
+ quantization: Literal["none", "int8", "nf4"] = InputField(
+ default="none",
+ description="Quantize the Qwen VL encoder to reduce VRAM usage. "
+ "'nf4' (4-bit) saves the most memory, 'int8' (8-bit) is a middle ground.",
+ )
+
+ @staticmethod
+ def _resize_for_vl_encoder(image: PILImage.Image, target_pixels: int = 512 * 512) -> PILImage.Image:
+ """Resize image to fit within target_pixels while preserving aspect ratio.
+
+ Matches the diffusers pipeline's calculate_dimensions logic: the image is resized
+ so its total pixel count is approximately target_pixels, with dimensions rounded to
+ multiples of 32. This prevents large images from producing too many vision tokens
+ which can overwhelm the text prompt.
+ """
+ w, h = image.size
+ aspect = w / h
+ # Compute dimensions that preserve aspect ratio at ~target_pixels total
+ new_w = int((target_pixels * aspect) ** 0.5)
+ new_h = int(target_pixels / new_w)
+ # Round to multiples of 32
+ new_w = max(32, (new_w // 32) * 32)
+ new_h = max(32, (new_h // 32) * 32)
+ if new_w != w or new_h != h:
+ image = image.resize((new_w, new_h), resample=PILImage.LANCZOS)
+ return image
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> QwenImageConditioningOutput:
+ # Load and resize reference images to ~1M pixels (matching diffusers pipeline)
+ pil_images: list[PILImage.Image] = []
+ for img_field in self.reference_images:
+ pil_img = context.images.get_pil(img_field.image_name)
+ pil_img = self._resize_for_vl_encoder(pil_img.convert("RGB"))
+ pil_images.append(pil_img)
+
+ prompt_embeds, prompt_mask = self._encode(context, pil_images)
+ prompt_embeds = prompt_embeds.detach().to("cpu")
+ prompt_mask = prompt_mask.detach().to("cpu") if prompt_mask is not None else None
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[QwenImageConditioningInfo(prompt_embeds=prompt_embeds, prompt_embeds_mask=prompt_mask)]
+ )
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return QwenImageConditioningOutput.build(conditioning_name)
+
+ def _encode(
+ self, context: InvocationContext, images: list[PILImage.Image]
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
+ """Encode text prompt and reference images using Qwen2.5-VL.
+
+ Matches the diffusers QwenImagePipeline._get_qwen_prompt_embeds logic:
+ 1. Format prompt with the edit-specific system template
+ 2. Run through Qwen2.5-VL to get hidden states
+ 3. Extract valid (non-padding) tokens and drop the system prefix
+ 4. Return padded embeddings + attention mask
+ """
+ from transformers import AutoTokenizer, Qwen2_5_VLProcessor
+
+ try:
+ from transformers import Qwen2_5_VLImageProcessor as _ImageProcessorCls
+ except ImportError:
+ from transformers.models.qwen2_vl.image_processing_qwen2_vl import ( # type: ignore[no-redef]
+ Qwen2VLImageProcessor as _ImageProcessorCls,
+ )
+
+ try:
+ from transformers import Qwen2_5_VLVideoProcessor as _VideoProcessorCls
+ except ImportError:
+ from transformers.models.qwen2_vl.video_processing_qwen2_vl import ( # type: ignore[no-redef]
+ Qwen2VLVideoProcessor as _VideoProcessorCls,
+ )
+
+ # Format the prompt with one vision placeholder per reference image
+ text = _build_prompt(self.prompt, len(images))
+
+ # Build the processor
+ tokenizer_config = context.models.get_config(self.qwen_vl_encoder.tokenizer)
+ model_root = context.models.get_absolute_path(tokenizer_config)
+
+ # Single-file checkpoints (e.g. ComfyUI fp8_scaled): model_root is the
+ # safetensors file itself, so there's no tokenizer/processor folder
+ # alongside it. Fall back to the canonical Qwen2.5-VL repo on HF (small
+ # ~10 MB download for tokenizer+processor configs, cached for offline use).
+ if model_root.is_file():
+ HF_REPO = "Qwen/Qwen2.5-VL-7B-Instruct"
+ try:
+ tokenizer = AutoTokenizer.from_pretrained(HF_REPO, local_files_only=True)
+ except OSError:
+ tokenizer = AutoTokenizer.from_pretrained(HF_REPO)
+ try:
+ image_processor = _ImageProcessorCls.from_pretrained(HF_REPO, local_files_only=True)
+ except OSError:
+ try:
+ image_processor = _ImageProcessorCls.from_pretrained(HF_REPO)
+ except Exception:
+ image_processor = _ImageProcessorCls()
+ else:
+ tokenizer_dir = model_root / "tokenizer"
+ tokenizer = AutoTokenizer.from_pretrained(str(tokenizer_dir), local_files_only=True)
+
+ image_processor = None
+ for search_dir in [model_root / "processor", tokenizer_dir, model_root, model_root / "image_processor"]:
+ if (search_dir / "preprocessor_config.json").exists():
+ image_processor = _ImageProcessorCls.from_pretrained(str(search_dir), local_files_only=True)
+ break
+ if image_processor is None:
+ image_processor = _ImageProcessorCls()
+
+ processor = Qwen2_5_VLProcessor(
+ tokenizer=tokenizer,
+ image_processor=image_processor,
+ video_processor=_VideoProcessorCls(),
+ )
+
+ context.util.signal_progress("Running Qwen2.5-VL text/vision encoder")
+
+ if self.quantization != "none":
+ text_encoder, device, cleanup = self._load_quantized_encoder(context)
+ else:
+ text_encoder, device, cleanup = self._load_cached_encoder(context)
+
+ try:
+ model_inputs = processor(
+ text=[text],
+ images=images if images else None,
+ padding=True,
+ return_tensors="pt",
+ ).to(device=device)
+
+ outputs = text_encoder(
+ input_ids=model_inputs.input_ids,
+ attention_mask=model_inputs.attention_mask,
+ pixel_values=getattr(model_inputs, "pixel_values", None),
+ image_grid_thw=getattr(model_inputs, "image_grid_thw", None),
+ output_hidden_states=True,
+ )
+
+ # Use last hidden state (matching diffusers pipeline)
+ hidden_states = outputs.hidden_states[-1]
+
+ # Extract valid (non-padding) tokens using the attention mask,
+ # then drop the system prompt prefix tokens.
+ # The drop index differs between edit mode (64) and generate mode (34).
+ drop_idx = _EDIT_DROP_IDX if images else _GENERATE_DROP_IDX
+
+ attn_mask = model_inputs.attention_mask
+ bool_mask = attn_mask.bool()
+ valid_lengths = bool_mask.sum(dim=1)
+ selected = hidden_states[bool_mask]
+ split_hidden = torch.split(selected, valid_lengths.tolist(), dim=0)
+
+ # Drop system prefix tokens and build padded output
+ trimmed = [h[drop_idx:] for h in split_hidden]
+ attn_mask_list = [torch.ones(h.size(0), dtype=torch.long, device=device) for h in trimmed]
+ max_seq_len = max(h.size(0) for h in trimmed)
+
+ prompt_embeds = torch.stack(
+ [torch.cat([h, h.new_zeros(max_seq_len - h.size(0), h.size(1))]) for h in trimmed]
+ )
+ encoder_attention_mask = torch.stack(
+ [torch.cat([m, m.new_zeros(max_seq_len - m.size(0))]) for m in attn_mask_list]
+ )
+
+ prompt_embeds = prompt_embeds.to(dtype=torch.bfloat16)
+ finally:
+ if cleanup is not None:
+ cleanup()
+
+ # If all tokens are valid (no padding), mask is not needed
+ if encoder_attention_mask.all():
+ encoder_attention_mask = None
+
+ return prompt_embeds, encoder_attention_mask
+
+ def _load_cached_encoder(self, context: InvocationContext):
+ """Load the text encoder through the model cache (no quantization)."""
+ from transformers import Qwen2_5_VLForConditionalGeneration
+
+ text_encoder_info = context.models.load(self.qwen_vl_encoder.text_encoder)
+ ctx = text_encoder_info.model_on_device()
+ _, text_encoder = ctx.__enter__()
+ device = get_effective_device(text_encoder)
+ assert isinstance(text_encoder, Qwen2_5_VLForConditionalGeneration)
+ return text_encoder, device, lambda: ctx.__exit__(None, None, None)
+
+ def _load_quantized_encoder(self, context: InvocationContext):
+ """Load the text encoder with BitsAndBytes quantization, bypassing the model cache.
+
+ BnB-quantized models are pinned to GPU and can't be moved between devices,
+ so they can't go through the standard model cache. The model is loaded fresh
+ each time and freed after use via the cleanup callback.
+ """
+ import gc
+ import warnings
+
+ from transformers import BitsAndBytesConfig, Qwen2_5_VLForConditionalGeneration
+
+ encoder_config = context.models.get_config(self.qwen_vl_encoder.text_encoder)
+ model_root = context.models.get_absolute_path(encoder_config)
+ if model_root.is_file():
+ # Single-file checkpoint (e.g. ComfyUI fp8_scaled): BnB can't load from
+ # a single file, and the checkpoint is already FP8-compressed anyway.
+ # Fall back to the cached path; the user effectively gets fp8 instead of
+ # int8/nf4, which is comparable in size.
+ return self._load_cached_encoder(context)
+ encoder_path = model_root / "text_encoder"
+
+ if self.quantization == "nf4":
+ bnb_config = BitsAndBytesConfig(
+ load_in_4bit=True,
+ bnb_4bit_compute_dtype=torch.bfloat16,
+ bnb_4bit_quant_type="nf4",
+ )
+ else: # int8
+ bnb_config = BitsAndBytesConfig(load_in_8bit=True)
+
+ context.util.signal_progress("Loading Qwen2.5-VL encoder (quantized)")
+ with warnings.catch_warnings():
+ # BnB int8 internally casts bfloat16→float16; the warning is harmless
+ warnings.filterwarnings("ignore", message="MatMul8bitLt.*cast.*float16")
+ text_encoder = Qwen2_5_VLForConditionalGeneration.from_pretrained(
+ str(encoder_path),
+ quantization_config=bnb_config,
+ device_map="auto",
+ torch_dtype=torch.bfloat16,
+ local_files_only=True,
+ )
+
+ device = next(text_encoder.parameters()).device
+
+ def cleanup():
+ nonlocal text_encoder
+ del text_encoder
+ gc.collect()
+ torch.cuda.empty_cache()
+
+ return text_encoder, device, cleanup
diff --git a/invokeai/app/invocations/resize_latents.py b/invokeai/app/invocations/resize_latents.py
new file mode 100644
index 00000000000..90253e52e83
--- /dev/null
+++ b/invokeai/app/invocations/resize_latents.py
@@ -0,0 +1,103 @@
+from typing import Literal
+
+import torch
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+)
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.util.devices import TorchDevice
+
+LATENTS_INTERPOLATION_MODE = Literal["nearest", "linear", "bilinear", "bicubic", "trilinear", "area", "nearest-exact"]
+
+
+@invocation(
+ "lresize",
+ title="Resize Latents",
+ tags=["latents", "resize"],
+ category="latents",
+ version="1.0.2",
+)
+class ResizeLatentsInvocation(BaseInvocation):
+ """Resizes latents to explicit width/height (in pixels). Provided dimensions are floor-divided by 8."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ width: int = InputField(
+ ge=64,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description=FieldDescriptions.width,
+ )
+ height: int = InputField(
+ ge=64,
+ multiple_of=LATENT_SCALE_FACTOR,
+ description=FieldDescriptions.width,
+ )
+ mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode)
+ antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias)
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+ device = TorchDevice.choose_torch_device()
+
+ resized_latents = torch.nn.functional.interpolate(
+ latents.to(device),
+ size=(self.height // LATENT_SCALE_FACTOR, self.width // LATENT_SCALE_FACTOR),
+ mode=self.mode,
+ antialias=self.antialias if self.mode in ["bilinear", "bicubic"] else False,
+ )
+
+ # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
+ resized_latents = resized_latents.to("cpu")
+
+ TorchDevice.empty_cache()
+
+ name = context.tensors.save(tensor=resized_latents)
+ return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed)
+
+
+@invocation(
+ "lscale",
+ title="Scale Latents",
+ tags=["latents", "resize"],
+ category="latents",
+ version="1.0.2",
+)
+class ScaleLatentsInvocation(BaseInvocation):
+ """Scales latents by a given factor."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ scale_factor: float = InputField(gt=0, description=FieldDescriptions.scale_factor)
+ mode: LATENTS_INTERPOLATION_MODE = InputField(default="bilinear", description=FieldDescriptions.interp_mode)
+ antialias: bool = InputField(default=False, description=FieldDescriptions.torch_antialias)
+
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ device = TorchDevice.choose_torch_device()
+
+ # resizing
+ resized_latents = torch.nn.functional.interpolate(
+ latents.to(device),
+ scale_factor=self.scale_factor,
+ mode=self.mode,
+ antialias=self.antialias if self.mode in ["bilinear", "bicubic"] else False,
+ )
+
+ # https://discuss.huggingface.co/t/memory-usage-by-later-pipeline-stages/23699
+ resized_latents = resized_latents.to("cpu")
+ TorchDevice.empty_cache()
+
+ name = context.tensors.save(tensor=resized_latents)
+ return LatentsOutput.build(latents_name=name, latents=resized_latents, seed=self.latents.seed)
diff --git a/invokeai/app/invocations/scheduler.py b/invokeai/app/invocations/scheduler.py
new file mode 100644
index 00000000000..a870a442ef8
--- /dev/null
+++ b/invokeai/app/invocations/scheduler.py
@@ -0,0 +1,34 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ InputField,
+ OutputField,
+ UIType,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
+
+
+@invocation_output("scheduler_output")
+class SchedulerOutput(BaseInvocationOutput):
+ scheduler: SCHEDULER_NAME_VALUES = OutputField(description=FieldDescriptions.scheduler, ui_type=UIType.Scheduler)
+
+
+@invocation(
+ "scheduler",
+ title="Scheduler",
+ tags=["scheduler"],
+ category="latents",
+ version="1.0.0",
+)
+class SchedulerInvocation(BaseInvocation):
+ """Selects a scheduler."""
+
+ scheduler: SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description=FieldDescriptions.scheduler,
+ ui_type=UIType.Scheduler,
+ )
+
+ def invoke(self, context: InvocationContext) -> SchedulerOutput:
+ return SchedulerOutput(scheduler=self.scheduler)
diff --git a/invokeai/app/invocations/sd3_denoise.py b/invokeai/app/invocations/sd3_denoise.py
new file mode 100644
index 00000000000..f6c90b9690c
--- /dev/null
+++ b/invokeai/app/invocations/sd3_denoise.py
@@ -0,0 +1,351 @@
+from typing import Callable, Optional, Tuple
+
+import torch
+import torchvision.transforms as tv_transforms
+from diffusers.models.transformers.transformer_sd3 import SD3Transformer2DModel
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ SD3ConditioningField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import TransformerField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.invocations.sd3_text_encoder import SD3_T5_MAX_SEQ_LEN
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.sampling_utils import clip_timestep_schedule_fractional
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import SD3ConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "sd3_denoise",
+ title="Denoise - SD3",
+ tags=["image", "sd3"],
+ category="latents",
+ version="1.2.0",
+)
+class SD3DenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Run denoising process with a SD3 model."""
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.latents, input=Input.Connection
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.noise, input=Input.Connection
+ )
+ # denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None, description=FieldDescriptions.denoise_mask, input=Input.Connection
+ )
+ 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)
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.sd3_model, input=Input.Connection, title="Transformer"
+ )
+ positive_conditioning: SD3ConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: SD3ConditioningField = InputField(
+ description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ cfg_scale: float | list[float] = InputField(default=3.5, description=FieldDescriptions.cfg_scale, title="CFG Scale")
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ """Prepare the inpaint mask.
+ - Loads the mask
+ - Resizes if necessary
+ - Casts to same device/dtype as latents
+
+ Args:
+ context (InvocationContext): The invocation context, for loading the inpaint mask.
+ latents (torch.Tensor): A latent image tensor. Used to determine the target shape, device, and dtype for the
+ inpaint mask.
+
+ Returns:
+ torch.Tensor | None: Inpaint mask. Values of 0.0 represent the regions to be fully denoised, and 1.0
+ represent the regions to be preserved.
+ """
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+
+ # The input denoise_mask contains values in [0, 1], where 0.0 represents the regions to be fully denoised, and
+ # 1.0 represents the regions to be preserved.
+ # We invert the mask so that the regions to be preserved are 0.0 and the regions to be denoised are 1.0.
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask
+
+ def _load_text_conditioning(
+ self,
+ context: InvocationContext,
+ conditioning_name: str,
+ joint_attention_dim: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ # Load the conditioning data.
+ cond_data = context.conditioning.load(conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ sd3_conditioning = cond_data.conditionings[0]
+ assert isinstance(sd3_conditioning, SD3ConditioningInfo)
+ sd3_conditioning = sd3_conditioning.to(dtype=dtype, device=device)
+
+ t5_embeds = sd3_conditioning.t5_embeds
+ if t5_embeds is None:
+ t5_embeds = torch.zeros(
+ (1, SD3_T5_MAX_SEQ_LEN, joint_attention_dim),
+ device=device,
+ dtype=dtype,
+ )
+
+ clip_prompt_embeds = torch.cat([sd3_conditioning.clip_l_embeds, sd3_conditioning.clip_g_embeds], dim=-1)
+ clip_prompt_embeds = torch.nn.functional.pad(
+ clip_prompt_embeds, (0, t5_embeds.shape[-1] - clip_prompt_embeds.shape[-1])
+ )
+
+ prompt_embeds = torch.cat([clip_prompt_embeds, t5_embeds], dim=-2)
+ pooled_prompt_embeds = torch.cat(
+ [sd3_conditioning.clip_l_pooled_embeds, sd3_conditioning.clip_g_pooled_embeds], dim=-1
+ )
+
+ return prompt_embeds, pooled_prompt_embeds
+
+ def _get_noise(
+ self,
+ num_samples: int,
+ num_channels_latents: int,
+ height: int,
+ width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ seed: int,
+ ) -> torch.Tensor:
+ # We always generate noise on the same device and dtype then cast to ensure consistency across devices/dtypes.
+ rand_device = "cpu"
+ rand_dtype = torch.float16
+
+ return torch.randn(
+ num_samples,
+ num_channels_latents,
+ int(height) // LATENT_SCALE_FACTOR,
+ int(width) // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+ def _prepare_cfg_scale(self, num_timesteps: int) -> list[float]:
+ """Prepare the CFG scale list.
+
+ Args:
+ num_timesteps (int): The number of timesteps in the scheduler. Could be different from num_steps depending
+ on the scheduler used (e.g. higher order schedulers).
+
+ Returns:
+ list[float]: _description_
+ """
+ if isinstance(self.cfg_scale, float):
+ cfg_scale = [self.cfg_scale] * num_timesteps
+ elif isinstance(self.cfg_scale, list):
+ assert len(self.cfg_scale) == num_timesteps
+ cfg_scale = self.cfg_scale
+ else:
+ raise ValueError(f"Invalid CFG scale type: {type(self.cfg_scale)}")
+
+ return cfg_scale
+
+ def _run_diffusion(
+ self,
+ context: InvocationContext,
+ ):
+ inference_dtype = TorchDevice.choose_torch_dtype()
+ device = TorchDevice.choose_torch_device()
+
+ transformer_info = context.models.load(self.transformer.transformer)
+
+ # Load/process the conditioning data.
+ # TODO(ryand): Make CFG optional.
+ do_classifier_free_guidance = True
+ pos_prompt_embeds, pos_pooled_prompt_embeds = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.positive_conditioning.conditioning_name,
+ joint_attention_dim=transformer_info.model.config.joint_attention_dim,
+ dtype=inference_dtype,
+ device=device,
+ )
+ neg_prompt_embeds, neg_pooled_prompt_embeds = self._load_text_conditioning(
+ context=context,
+ conditioning_name=self.negative_conditioning.conditioning_name,
+ joint_attention_dim=transformer_info.model.config.joint_attention_dim,
+ dtype=inference_dtype,
+ device=device,
+ )
+ # TODO(ryand): Support both sequential and batched CFG inference.
+ prompt_embeds = torch.cat([neg_prompt_embeds, pos_prompt_embeds], dim=0)
+ pooled_prompt_embeds = torch.cat([neg_pooled_prompt_embeds, pos_pooled_prompt_embeds], dim=0)
+
+ # Prepare the timestep schedule.
+ # We add an extra step to the end to account for the final timestep of 0.0.
+ timesteps: list[float] = torch.linspace(1, 0, self.steps + 1).tolist()
+ # Clip the timesteps schedule based on denoising_start and denoising_end.
+ timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
+ total_steps = len(timesteps) - 1
+
+ # Prepare the CFG scale list.
+ cfg_scale = self._prepare_cfg_scale(total_steps)
+
+ # Load the input latents, if provided.
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+
+ # Generate initial latent noise.
+ num_channels_latents = transformer_info.model.config.in_channels
+ assert isinstance(num_channels_latents, int)
+ noise = self._prepare_noise_tensor(context, num_channels_latents, inference_dtype, device)
+
+ # Prepare input latent image.
+ if init_latents is not None:
+ # Noise the init_latents by the appropriate amount for the first timestep.
+ t_0 = timesteps[0]
+ latents = t_0 * noise + (1.0 - t_0) * init_latents
+ else:
+ # init_latents are not provided, so we are not doing image-to-image (i.e. we are starting from pure noise).
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ latents = noise
+
+ # If len(timesteps) == 1, then short-circuit. We are just noising the input latents, but not taking any
+ # denoising steps.
+ if len(timesteps) <= 1:
+ return latents
+
+ # Prepare inpaint extension.
+ inpaint_mask = self._prep_inpaint_mask(context, latents)
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ assert init_latents is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents,
+ inpaint_mask=inpaint_mask,
+ noise=noise,
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=0,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(timesteps[0]),
+ latents=latents,
+ ),
+ )
+
+ with transformer_info.model_on_device() as (cached_weights, transformer):
+ assert isinstance(transformer, SD3Transformer2DModel)
+
+ # 6. Denoising loop
+ for step_idx, (t_curr, t_prev) in tqdm(list(enumerate(zip(timesteps[:-1], timesteps[1:], strict=True)))):
+ # Expand the latents if we are doing CFG.
+ latent_model_input = torch.cat([latents] * 2) if do_classifier_free_guidance else latents
+ # Expand the timestep to match the latent model input.
+ # Multiply by 1000 to match the default FlowMatchEulerDiscreteScheduler num_train_timesteps.
+ timestep = torch.tensor([t_curr * 1000], device=device).expand(latent_model_input.shape[0])
+
+ noise_pred = transformer(
+ hidden_states=latent_model_input,
+ timestep=timestep,
+ encoder_hidden_states=prompt_embeds,
+ pooled_projections=pooled_prompt_embeds,
+ joint_attention_kwargs=None,
+ return_dict=False,
+ )[0]
+
+ # Apply CFG.
+ if do_classifier_free_guidance:
+ noise_pred_uncond, noise_pred_cond = noise_pred.chunk(2)
+ noise_pred = noise_pred_uncond + cfg_scale[step_idx] * (noise_pred_cond - noise_pred_uncond)
+
+ # Compute the previous noisy sample x_t -> x_t-1.
+ latents_dtype = latents.dtype
+ latents = latents.to(dtype=torch.float32)
+ latents = latents + (t_prev - t_curr) * noise_pred
+ latents = latents.to(dtype=latents_dtype)
+
+ if inpaint_extension is not None:
+ latents = inpaint_extension.merge_intermediate_latents_with_init_latents(latents, t_prev)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(t_curr),
+ latents=latents,
+ ),
+ )
+
+ return latents
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, num_channels_latents: int, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "SD3", self.width, self.height)
+ return noise
+
+ return self._get_noise(
+ num_samples=1,
+ num_channels_latents=num_channels_latents,
+ height=self.height,
+ width=self.width,
+ dtype=inference_dtype,
+ device=device,
+ seed=self.seed,
+ )
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.StableDiffusion3)
+
+ return step_callback
diff --git a/invokeai/app/invocations/sd3_image_to_latents.py b/invokeai/app/invocations/sd3_image_to_latents.py
new file mode 100644
index 00000000000..9af641d8bcf
--- /dev/null
+++ b/invokeai/app/invocations/sd3_image_to_latents.py
@@ -0,0 +1,72 @@
+import einops
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd3
+
+
+@invocation(
+ "sd3_i2l",
+ title="Image to Latents - SD3",
+ tags=["image", "latents", "vae", "i2l", "sd3"],
+ category="latents",
+ version="1.0.1",
+)
+class SD3ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates latents from an image."""
+
+ image: ImageField = InputField(description="The image to encode")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ assert isinstance(vae_info.model, AutoencoderKL)
+ estimated_working_memory = estimate_vae_working_memory_sd3(
+ operation="encode", image_tensor=image_tensor, vae=vae_info.model
+ )
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ assert isinstance(vae, AutoencoderKL)
+
+ vae.disable_tiling()
+
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae.dtype)
+ with torch.inference_mode():
+ image_tensor_dist = vae.encode(image_tensor).latent_dist
+ # TODO: Use seed to make sampling reproducible.
+ latents: torch.Tensor = image_tensor_dist.sample().to(dtype=vae.dtype)
+
+ latents = vae.config.scaling_factor * latents
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, AutoencoderKL)
+
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/sd3_latents_to_image.py b/invokeai/app/invocations/sd3_latents_to_image.py
new file mode 100644
index 00000000000..e6a20d38a9c
--- /dev/null
+++ b/invokeai/app/invocations/sd3_latents_to_image.py
@@ -0,0 +1,81 @@
+from contextlib import nullcontext
+
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd3
+
+
+@invocation(
+ "sd3_l2i",
+ title="Latents to Image - SD3",
+ tags=["latents", "image", "vae", "l2i", "sd3"],
+ category="latents",
+ version="1.3.2",
+)
+class SD3LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents."""
+
+ latents: LatentsField = InputField(
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ vae: VAEField = InputField(
+ description=FieldDescriptions.vae,
+ input=Input.Connection,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ assert isinstance(vae_info.model, (AutoencoderKL))
+ estimated_working_memory = estimate_vae_working_memory_sd3(
+ operation="decode", image_tensor=latents, vae=vae_info.model
+ )
+ with (
+ SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
+ vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),
+ ):
+ context.util.signal_progress("Running VAE")
+ assert isinstance(vae, (AutoencoderKL))
+ latents = latents.to(TorchDevice.choose_torch_device())
+
+ vae.disable_tiling()
+
+ tiling_context = nullcontext()
+
+ # clear memory as vae decode can request a lot
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode(), tiling_context:
+ # copied from diffusers pipeline
+ latents = latents / vae.config.scaling_factor
+ img = vae.decode(latents, return_dict=False)[0]
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c") # noqa: F821
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/sd3_model_loader.py b/invokeai/app/invocations/sd3_model_loader.py
new file mode 100644
index 00000000000..7d095d96c6b
--- /dev/null
+++ b/invokeai/app/invocations/sd3_model_loader.py
@@ -0,0 +1,109 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.t5_model_identifier import (
+ preprocess_t5_encoder_model_identifier,
+ preprocess_t5_tokenizer_model_identifier,
+)
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ClipVariantType, ModelType, SubModelType
+
+
+@invocation_output("sd3_model_loader_output")
+class Sd3ModelLoaderOutput(BaseInvocationOutput):
+ """SD3 base model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ clip_l: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP L")
+ clip_g: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP G")
+ t5_encoder: T5EncoderField = OutputField(description=FieldDescriptions.t5_encoder, title="T5 Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "sd3_model_loader",
+ title="Main Model - SD3",
+ tags=["model", "sd3"],
+ category="model",
+ version="1.0.1",
+)
+class Sd3ModelLoaderInvocation(BaseInvocation):
+ """Loads a SD3 base model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.sd3_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.StableDiffusion3,
+ ui_model_type=ModelType.Main,
+ )
+
+ t5_encoder_model: Optional[ModelIdentifierField] = InputField(
+ description=FieldDescriptions.t5_encoder,
+ input=Input.Direct,
+ title="T5 Encoder",
+ default=None,
+ ui_model_type=ModelType.T5Encoder,
+ )
+
+ clip_l_model: Optional[ModelIdentifierField] = InputField(
+ description=FieldDescriptions.clip_embed_model,
+ input=Input.Direct,
+ title="CLIP L Encoder",
+ default=None,
+ ui_model_type=ModelType.CLIPEmbed,
+ ui_model_variant=ClipVariantType.L,
+ )
+
+ clip_g_model: Optional[ModelIdentifierField] = InputField(
+ description=FieldDescriptions.clip_g_model,
+ input=Input.Direct,
+ title="CLIP G Encoder",
+ default=None,
+ ui_model_type=ModelType.CLIPEmbed,
+ ui_model_variant=ClipVariantType.G,
+ )
+
+ vae_model: Optional[ModelIdentifierField] = InputField(
+ description=FieldDescriptions.vae_model,
+ title="VAE",
+ default=None,
+ ui_model_base=BaseModelType.StableDiffusion3,
+ ui_model_type=ModelType.VAE,
+ )
+
+ def invoke(self, context: InvocationContext) -> Sd3ModelLoaderOutput:
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+ vae = (
+ self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ if self.vae_model
+ else self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+ )
+ tokenizer_l = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ clip_encoder_l = (
+ self.clip_l_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ if self.clip_l_model
+ else self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ )
+ tokenizer_g = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
+ clip_encoder_g = (
+ self.clip_g_model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
+ if self.clip_g_model
+ else self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
+ )
+ tokenizer_t5 = preprocess_t5_tokenizer_model_identifier(self.t5_encoder_model or self.model)
+ t5_encoder = preprocess_t5_encoder_model_identifier(self.t5_encoder_model or self.model)
+
+ return Sd3ModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ clip_l=CLIPField(tokenizer=tokenizer_l, text_encoder=clip_encoder_l, loras=[], skipped_layers=0),
+ clip_g=CLIPField(tokenizer=tokenizer_g, text_encoder=clip_encoder_g, loras=[], skipped_layers=0),
+ t5_encoder=T5EncoderField(tokenizer=tokenizer_t5, text_encoder=t5_encoder, loras=[]),
+ vae=VAEField(vae=vae),
+ )
diff --git a/invokeai/app/invocations/sd3_text_encoder.py b/invokeai/app/invocations/sd3_text_encoder.py
new file mode 100644
index 00000000000..7af138fe45e
--- /dev/null
+++ b/invokeai/app/invocations/sd3_text_encoder.py
@@ -0,0 +1,206 @@
+from contextlib import ExitStack
+from typing import Iterator, Tuple
+
+import torch
+from transformers import (
+ CLIPTextModel,
+ CLIPTextModelWithProjection,
+ CLIPTokenizer,
+ T5EncoderModel,
+ T5Tokenizer,
+ T5TokenizerFast,
+)
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField
+from invokeai.app.invocations.model import CLIPField, T5EncoderField
+from invokeai.app.invocations.primitives import SD3ConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.model_manager.taxonomy import ModelFormat
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, SD3ConditioningInfo
+
+# The SD3 T5 Max Sequence Length set based on the default in diffusers.
+SD3_T5_MAX_SEQ_LEN = 256
+
+
+@invocation(
+ "sd3_text_encoder",
+ title="Prompt - SD3",
+ tags=["prompt", "conditioning", "sd3"],
+ category="prompt",
+ version="1.0.1",
+)
+class Sd3TextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for a SD3 image."""
+
+ clip_l: CLIPField = InputField(
+ title="CLIP L",
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ )
+ clip_g: CLIPField = InputField(
+ title="CLIP G",
+ description=FieldDescriptions.clip,
+ input=Input.Connection,
+ )
+
+ # The SD3 models were trained with text encoder dropout, so the T5 encoder can be omitted to save time/memory.
+ t5_encoder: T5EncoderField | None = InputField(
+ title="T5Encoder",
+ default=None,
+ description=FieldDescriptions.t5_encoder,
+ input=Input.Connection,
+ )
+ prompt: str = InputField(description="Text prompt to encode.")
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> SD3ConditioningOutput:
+ # Note: The text encoding model are run in separate functions to ensure that all model references are locally
+ # scoped. This ensures that earlier models can be freed and gc'd before loading later models (if necessary).
+
+ clip_l_embeddings, clip_l_pooled_embeddings = self._clip_encode(context, self.clip_l)
+ clip_g_embeddings, clip_g_pooled_embeddings = self._clip_encode(context, self.clip_g)
+
+ t5_embeddings: torch.Tensor | None = None
+ if self.t5_encoder is not None:
+ t5_embeddings = self._t5_encode(context, SD3_T5_MAX_SEQ_LEN)
+
+ # Move all embeddings to CPU for storage to save VRAM
+ # They will be moved to the appropriate device when used by the denoiser
+ clip_l_embeddings = clip_l_embeddings.detach().to("cpu")
+ clip_l_pooled_embeddings = clip_l_pooled_embeddings.detach().to("cpu")
+ clip_g_embeddings = clip_g_embeddings.detach().to("cpu")
+ clip_g_pooled_embeddings = clip_g_pooled_embeddings.detach().to("cpu")
+ if t5_embeddings is not None:
+ t5_embeddings = t5_embeddings.detach().to("cpu")
+
+ conditioning_data = ConditioningFieldData(
+ conditionings=[
+ SD3ConditioningInfo(
+ clip_l_embeds=clip_l_embeddings,
+ clip_l_pooled_embeds=clip_l_pooled_embeddings,
+ clip_g_embeds=clip_g_embeddings,
+ clip_g_pooled_embeds=clip_g_pooled_embeddings,
+ t5_embeds=t5_embeddings,
+ )
+ ]
+ )
+
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return SD3ConditioningOutput.build(conditioning_name)
+
+ def _t5_encode(self, context: InvocationContext, max_seq_len: int) -> torch.Tensor:
+ assert self.t5_encoder is not None
+ prompt = [self.prompt]
+
+ with (
+ context.models.load(self.t5_encoder.text_encoder) as t5_text_encoder,
+ context.models.load(self.t5_encoder.tokenizer) as t5_tokenizer,
+ ):
+ context.util.signal_progress("Running T5 encoder")
+ assert isinstance(t5_text_encoder, T5EncoderModel)
+ assert isinstance(t5_tokenizer, (T5Tokenizer, T5TokenizerFast))
+ t5_device = get_effective_device(t5_text_encoder)
+
+ text_inputs = t5_tokenizer(
+ prompt,
+ padding="max_length",
+ max_length=max_seq_len,
+ truncation=True,
+ add_special_tokens=True,
+ return_tensors="pt",
+ )
+ text_input_ids = text_inputs.input_ids
+ untruncated_ids = t5_tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
+ assert isinstance(text_input_ids, torch.Tensor)
+ assert isinstance(untruncated_ids, torch.Tensor)
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
+ text_input_ids, untruncated_ids
+ ):
+ removed_text = t5_tokenizer.batch_decode(untruncated_ids[:, max_seq_len - 1 : -1])
+ context.logger.warning(
+ "The following part of your input was truncated because `max_sequence_length` is set to "
+ f" {max_seq_len} tokens: {removed_text}"
+ )
+
+ prompt_embeds = t5_text_encoder(text_input_ids.to(t5_device))[0]
+
+ assert isinstance(prompt_embeds, torch.Tensor)
+ return prompt_embeds
+
+ def _clip_encode(
+ self, context: InvocationContext, clip_model: CLIPField, tokenizer_max_length: int = 77
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
+ prompt = [self.prompt]
+
+ clip_text_encoder_info = context.models.load(clip_model.text_encoder)
+ with (
+ clip_text_encoder_info.model_on_device() as (cached_weights, clip_text_encoder),
+ context.models.load(clip_model.tokenizer) as clip_tokenizer,
+ ExitStack() as exit_stack,
+ ):
+ context.util.signal_progress("Running CLIP encoder")
+ assert isinstance(clip_text_encoder, (CLIPTextModel, CLIPTextModelWithProjection))
+ assert isinstance(clip_tokenizer, CLIPTokenizer)
+ clip_device = get_effective_device(clip_text_encoder)
+
+ clip_text_encoder_config = clip_text_encoder_info.config
+ assert clip_text_encoder_config is not None
+
+ # Apply LoRA models to the CLIP encoder.
+ # Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
+ if clip_text_encoder_config.format in [ModelFormat.Diffusers]:
+ # The model is non-quantized, so we can apply the LoRA weights directly into the model.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=clip_text_encoder,
+ patches=self._clip_lora_iterator(context, clip_model),
+ prefix=FLUX_LORA_CLIP_PREFIX,
+ dtype=clip_text_encoder.dtype,
+ cached_weights=cached_weights,
+ )
+ )
+ else:
+ # There are currently no supported CLIP quantized models. Add support here if needed.
+ raise ValueError(f"Unsupported model format: {clip_text_encoder_config.format}")
+
+ clip_text_encoder = clip_text_encoder.eval().requires_grad_(False)
+
+ text_inputs = clip_tokenizer(
+ prompt,
+ padding="max_length",
+ max_length=tokenizer_max_length,
+ truncation=True,
+ return_tensors="pt",
+ )
+
+ text_input_ids = text_inputs.input_ids
+ untruncated_ids = clip_tokenizer(prompt, padding="longest", return_tensors="pt").input_ids
+ assert isinstance(text_input_ids, torch.Tensor)
+ assert isinstance(untruncated_ids, torch.Tensor)
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
+ text_input_ids, untruncated_ids
+ ):
+ removed_text = clip_tokenizer.batch_decode(untruncated_ids[:, tokenizer_max_length - 1 : -1])
+ context.logger.warning(
+ "The following part of your input was truncated because CLIP can only handle sequences up to"
+ f" {tokenizer_max_length} tokens: {removed_text}"
+ )
+ prompt_embeds = clip_text_encoder(input_ids=text_input_ids.to(clip_device), output_hidden_states=True)
+ pooled_prompt_embeds = prompt_embeds[0]
+ prompt_embeds = prompt_embeds.hidden_states[-2]
+
+ return prompt_embeds, pooled_prompt_embeds
+
+ def _clip_lora_iterator(
+ self, context: InvocationContext, clip_model: CLIPField
+ ) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in clip_model.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/sdxl.py b/invokeai/app/invocations/sdxl.py
new file mode 100644
index 00000000000..0f509828c13
--- /dev/null
+++ b/invokeai/app/invocations/sdxl.py
@@ -0,0 +1,95 @@
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
+from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, UNetField, VAEField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
+
+
+@invocation_output("sdxl_model_loader_output")
+class SDXLModelLoaderOutput(BaseInvocationOutput):
+ """SDXL base model loader output"""
+
+ unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
+ clip: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP 1")
+ clip2: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP 2")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation_output("sdxl_refiner_model_loader_output")
+class SDXLRefinerModelLoaderOutput(BaseInvocationOutput):
+ """SDXL refiner model loader output"""
+
+ unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
+ clip2: CLIPField = OutputField(description=FieldDescriptions.clip, title="CLIP 2")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation("sdxl_model_loader", title="Main Model - SDXL", tags=["model", "sdxl"], category="model", version="1.0.4")
+class SDXLModelLoaderInvocation(BaseInvocation):
+ """Loads an sdxl base model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.sdxl_main_model,
+ ui_model_base=BaseModelType.StableDiffusionXL,
+ ui_model_type=ModelType.Main,
+ )
+ # TODO: precision?
+
+ def invoke(self, context: InvocationContext) -> SDXLModelLoaderOutput:
+ model_key = self.model.key
+
+ # TODO: not found exceptions
+ if not context.models.exists(model_key):
+ raise Exception(f"Unknown model: {model_key}")
+
+ unet = self.model.model_copy(update={"submodel_type": SubModelType.UNet})
+ scheduler = self.model.model_copy(update={"submodel_type": SubModelType.Scheduler})
+ tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ text_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ tokenizer2 = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
+ text_encoder2 = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+
+ return SDXLModelLoaderOutput(
+ unet=UNetField(unet=unet, scheduler=scheduler, loras=[]),
+ clip=CLIPField(tokenizer=tokenizer, text_encoder=text_encoder, loras=[], skipped_layers=0),
+ clip2=CLIPField(tokenizer=tokenizer2, text_encoder=text_encoder2, loras=[], skipped_layers=0),
+ vae=VAEField(vae=vae),
+ )
+
+
+@invocation(
+ "sdxl_refiner_model_loader",
+ title="Refiner Model - SDXL",
+ tags=["model", "sdxl", "refiner"],
+ category="model",
+ version="1.0.4",
+)
+class SDXLRefinerModelLoaderInvocation(BaseInvocation):
+ """Loads an sdxl refiner model, outputting its submodels."""
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.sdxl_refiner_model,
+ ui_model_base=BaseModelType.StableDiffusionXLRefiner,
+ ui_model_type=ModelType.Main,
+ )
+ # TODO: precision?
+
+ def invoke(self, context: InvocationContext) -> SDXLRefinerModelLoaderOutput:
+ model_key = self.model.key
+
+ # TODO: not found exceptions
+ if not context.models.exists(model_key):
+ raise Exception(f"Unknown model: {model_key}")
+
+ unet = self.model.model_copy(update={"submodel_type": SubModelType.UNet})
+ scheduler = self.model.model_copy(update={"submodel_type": SubModelType.Scheduler})
+ tokenizer2 = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
+ text_encoder2 = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
+ vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
+
+ return SDXLRefinerModelLoaderOutput(
+ unet=UNetField(unet=unet, scheduler=scheduler, loras=[]),
+ clip2=CLIPField(tokenizer=tokenizer2, text_encoder=text_encoder2, loras=[], skipped_layers=0),
+ vae=VAEField(vae=vae),
+ )
diff --git a/invokeai/app/invocations/segment_anything.py b/invokeai/app/invocations/segment_anything.py
new file mode 100644
index 00000000000..35d20e47336
--- /dev/null
+++ b/invokeai/app/invocations/segment_anything.py
@@ -0,0 +1,217 @@
+from itertools import zip_longest
+from pathlib import Path
+from typing import Literal
+
+import numpy as np
+import torch
+from PIL import Image
+from pydantic import BaseModel, Field, model_validator
+from transformers.models.sam import SamModel
+from transformers.models.sam.processing_sam import SamProcessor
+from transformers.models.sam2 import Sam2Model
+from transformers.models.sam2.processing_sam2 import Sam2Processor
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import BoundingBoxField, ImageField, InputField, TensorField
+from invokeai.app.invocations.primitives import MaskOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.segment_anything.mask_refinement import mask_to_polygon, polygon_to_mask
+from invokeai.backend.image_util.segment_anything.segment_anything_2_pipeline import SegmentAnything2Pipeline
+from invokeai.backend.image_util.segment_anything.segment_anything_pipeline import SegmentAnythingPipeline
+from invokeai.backend.image_util.segment_anything.shared import SAMInput, SAMPoint
+
+SegmentAnythingModelKey = Literal[
+ "segment-anything-base",
+ "segment-anything-large",
+ "segment-anything-huge",
+ "segment-anything-2-tiny",
+ "segment-anything-2-small",
+ "segment-anything-2-base",
+ "segment-anything-2-large",
+]
+SEGMENT_ANYTHING_MODEL_IDS: dict[SegmentAnythingModelKey, str] = {
+ "segment-anything-base": "facebook/sam-vit-base",
+ "segment-anything-large": "facebook/sam-vit-large",
+ "segment-anything-huge": "facebook/sam-vit-huge",
+ "segment-anything-2-tiny": "facebook/sam2.1-hiera-tiny",
+ "segment-anything-2-small": "facebook/sam2.1-hiera-small",
+ "segment-anything-2-base": "facebook/sam2.1-hiera-base-plus",
+ "segment-anything-2-large": "facebook/sam2.1-hiera-large",
+}
+
+
+class SAMPointsField(BaseModel):
+ points: list[SAMPoint] = Field(..., description="The points of the object", min_length=1)
+
+ def to_list(self) -> list[list[float]]:
+ return [[point.x, point.y, point.label.value] for point in self.points]
+
+
+@invocation(
+ "segment_anything",
+ title="Segment Anything",
+ tags=["prompt", "segmentation", "sam", "sam2"],
+ category="segmentation",
+ version="1.3.0",
+)
+class SegmentAnythingInvocation(BaseInvocation):
+ """Runs a Segment Anything Model (SAM or SAM2)."""
+
+ # Reference:
+ # - https://arxiv.org/pdf/2304.02643
+ # - https://huggingface.co/docs/transformers/v4.43.3/en/model_doc/grounding-dino#grounded-sam
+ # - https://github.com/NielsRogge/Transformers-Tutorials/blob/a39f33ac1557b02ebfb191ea7753e332b5ca933f/Grounding%20DINO/GroundingDINO_with_Segment_Anything.ipynb
+
+ model: SegmentAnythingModelKey = InputField(description="The Segment Anything model to use (SAM or SAM2).")
+ image: ImageField = InputField(description="The image to segment.")
+ bounding_boxes: list[BoundingBoxField] | None = InputField(
+ default=None, description="The bounding boxes to prompt the model with."
+ )
+ point_lists: list[SAMPointsField] | None = InputField(
+ default=None,
+ description="The list of point lists to prompt the model with. Each list of points represents a single object.",
+ )
+ apply_polygon_refinement: bool = InputField(
+ description="Whether to apply polygon refinement to the masks. This will smooth the edges of the masks slightly and ensure that each mask consists of a single closed polygon (before merging).",
+ default=True,
+ )
+ mask_filter: Literal["all", "largest", "highest_box_score"] = InputField(
+ description="The filtering to apply to the detected masks before merging them into a final output.",
+ default="all",
+ )
+
+ @model_validator(mode="after")
+ def validate_points_and_boxes_len(self):
+ if self.point_lists is not None and self.bounding_boxes is not None:
+ if len(self.point_lists) != len(self.bounding_boxes):
+ raise ValueError("If both point_lists and bounding_boxes are provided, they must have the same length.")
+ return self
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> MaskOutput:
+ # The models expect a 3-channel RGB image.
+ image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
+
+ if (not self.bounding_boxes or len(self.bounding_boxes) == 0) and (
+ not self.point_lists or len(self.point_lists) == 0
+ ):
+ combined_mask = torch.zeros(image_pil.size[::-1], dtype=torch.bool)
+ else:
+ masks = self._segment(context=context, image=image_pil)
+ masks = self._filter_masks(masks=masks, bounding_boxes=self.bounding_boxes)
+
+ # masks contains bool values, so we merge them via max-reduce.
+ combined_mask, _ = torch.stack(masks).max(dim=0)
+
+ # Unsqueeze the channel dimension.
+ combined_mask = combined_mask.unsqueeze(0)
+ mask_tensor_name = context.tensors.save(combined_mask)
+ _, height, width = combined_mask.shape
+ return MaskOutput(mask=TensorField(tensor_name=mask_tensor_name), width=width, height=height)
+
+ @staticmethod
+ def _load_sam_model(model_path: Path):
+ sam_model = SamModel.from_pretrained(
+ model_path,
+ local_files_only=True,
+ # TODO(ryand): Setting the torch_dtype here doesn't work. Investigate whether fp16 is supported by the
+ # model, and figure out how to make it work in the pipeline.
+ # torch_dtype=TorchDevice.choose_torch_dtype(),
+ )
+ sam_processor = SamProcessor.from_pretrained(model_path, local_files_only=True)
+ return SegmentAnythingPipeline(sam_model=sam_model, sam_processor=sam_processor)
+
+ @staticmethod
+ def _load_sam_2_model(model_path: Path):
+ sam2_model = Sam2Model.from_pretrained(model_path, local_files_only=True)
+ sam2_processor = Sam2Processor.from_pretrained(model_path, local_files_only=True)
+ return SegmentAnything2Pipeline(sam2_model=sam2_model, sam2_processor=sam2_processor)
+
+ def _segment(self, context: InvocationContext, image: Image.Image) -> list[torch.Tensor]:
+ """Use Segment Anything (SAM or SAM2) to generate masks given an image + a set of bounding boxes."""
+
+ source = SEGMENT_ANYTHING_MODEL_IDS[self.model]
+ inputs: list[SAMInput] = []
+ for bbox_field, point_field in zip_longest(self.bounding_boxes or [], self.point_lists or [], fillvalue=None):
+ inputs.append(
+ SAMInput(
+ bounding_box=bbox_field,
+ points=point_field.points if point_field else None,
+ )
+ )
+
+ if "sam2" in source:
+ loader = SegmentAnythingInvocation._load_sam_2_model
+ with context.models.load_remote_model(source=source, loader=loader) as pipeline:
+ assert isinstance(pipeline, SegmentAnything2Pipeline)
+ masks = pipeline.segment(image=image, inputs=inputs)
+ else:
+ loader = SegmentAnythingInvocation._load_sam_model
+ with context.models.load_remote_model(source=source, loader=loader) as pipeline:
+ assert isinstance(pipeline, SegmentAnythingPipeline)
+ masks = pipeline.segment(image=image, inputs=inputs)
+
+ masks = self._process_masks(masks)
+ if self.apply_polygon_refinement:
+ masks = self._apply_polygon_refinement(masks)
+
+ return masks
+
+ def _process_masks(self, masks: torch.Tensor) -> list[torch.Tensor]:
+ """Convert the tensor output from the Segment Anything model from a tensor of shape
+ [num_masks, channels, height, width] to a list of tensors of shape [height, width].
+ """
+ assert masks.dtype == torch.bool
+ # [num_masks, channels, height, width] -> [num_masks, height, width]
+ masks, _ = masks.max(dim=1)
+ # Split the first dimension into a list of masks.
+ return list(masks.cpu().unbind(dim=0))
+
+ def _apply_polygon_refinement(self, masks: list[torch.Tensor]) -> list[torch.Tensor]:
+ """Apply polygon refinement to the masks.
+
+ Convert each mask to a polygon, then back to a mask. This has the following effect:
+ - Smooth the edges of the mask slightly.
+ - Ensure that each mask consists of a single closed polygon
+ - Removes small mask pieces.
+ - Removes holes from the mask.
+ """
+ # Convert tensor masks to np masks.
+ np_masks = [mask.cpu().numpy().astype(np.uint8) for mask in masks]
+
+ # Apply polygon refinement.
+ for idx, mask in enumerate(np_masks):
+ shape = mask.shape
+ assert len(shape) == 2 # Assert length to satisfy type checker.
+ polygon = mask_to_polygon(mask)
+ mask = polygon_to_mask(polygon, shape)
+ np_masks[idx] = mask
+
+ # Convert np masks back to tensor masks.
+ masks = [torch.tensor(mask, dtype=torch.bool) for mask in np_masks]
+
+ return masks
+
+ def _filter_masks(
+ self, masks: list[torch.Tensor], bounding_boxes: list[BoundingBoxField] | None
+ ) -> list[torch.Tensor]:
+ """Filter the detected masks based on the specified mask filter."""
+
+ if self.mask_filter == "all":
+ return masks
+ elif self.mask_filter == "largest":
+ # Find the largest mask.
+ return [max(masks, key=lambda x: float(x.sum()))]
+ elif self.mask_filter == "highest_box_score":
+ assert bounding_boxes is not None, (
+ "Bounding boxes must be provided to use the 'highest_box_score' mask filter."
+ )
+ assert len(masks) == len(bounding_boxes)
+ # Find the index of the bounding box with the highest score.
+ # Note that we fallback to -1.0 if the score is None. This is mainly to satisfy the type checker. In most
+ # cases the scores should all be non-None when using this filtering mode. That being said, -1.0 is a
+ # reasonable fallback since the expected score range is [0.0, 1.0].
+ max_score_idx = max(range(len(bounding_boxes)), key=lambda i: bounding_boxes[i].score or -1.0)
+ return [masks[max_score_idx]]
+ else:
+ raise ValueError(f"Invalid mask filter: {self.mask_filter}")
diff --git a/invokeai/app/invocations/spandrel_image_to_image.py b/invokeai/app/invocations/spandrel_image_to_image.py
new file mode 100644
index 00000000000..fb870c429c4
--- /dev/null
+++ b/invokeai/app/invocations/spandrel_image_to_image.py
@@ -0,0 +1,291 @@
+import functools
+from typing import Callable
+
+import numpy as np
+import torch
+from PIL import Image
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import ModelType
+from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel
+from invokeai.backend.tiles.tiles import calc_tiles_min_overlap
+from invokeai.backend.tiles.utils import TBLR, Tile
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation("spandrel_image_to_image", title="Image-to-Image", tags=["upscale"], category="upscale", version="1.3.0")
+class SpandrelImageToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel)."""
+
+ image: ImageField = InputField(description="The input image")
+ image_to_image_model: ModelIdentifierField = InputField(
+ title="Image-to-Image Model",
+ description=FieldDescriptions.spandrel_image_to_image_model,
+ ui_model_type=ModelType.SpandrelImageToImage,
+ )
+ tile_size: int = InputField(
+ default=512, description="The tile size for tiled image-to-image. Set to 0 to disable tiling."
+ )
+
+ @classmethod
+ def scale_tile(cls, tile: Tile, scale: int) -> Tile:
+ return Tile(
+ coords=TBLR(
+ top=tile.coords.top * scale,
+ bottom=tile.coords.bottom * scale,
+ left=tile.coords.left * scale,
+ right=tile.coords.right * scale,
+ ),
+ overlap=TBLR(
+ top=tile.overlap.top * scale,
+ bottom=tile.overlap.bottom * scale,
+ left=tile.overlap.left * scale,
+ right=tile.overlap.right * scale,
+ ),
+ )
+
+ @classmethod
+ def upscale_image(
+ cls,
+ image: Image.Image,
+ tile_size: int,
+ spandrel_model: SpandrelImageToImageModel,
+ is_canceled: Callable[[], bool],
+ step_callback: Callable[[int, int], None],
+ ) -> Image.Image:
+ # Compute the image tiles.
+ if tile_size > 0:
+ min_overlap = 20
+ tiles = calc_tiles_min_overlap(
+ image_height=image.height,
+ image_width=image.width,
+ tile_height=tile_size,
+ tile_width=tile_size,
+ min_overlap=min_overlap,
+ )
+ else:
+ # No tiling. Generate a single tile that covers the entire image.
+ min_overlap = 0
+ tiles = [
+ Tile(
+ coords=TBLR(top=0, bottom=image.height, left=0, right=image.width),
+ overlap=TBLR(top=0, bottom=0, left=0, right=0),
+ )
+ ]
+
+ # Sort tiles first by left x coordinate, then by top y coordinate. During tile processing, we want to iterate
+ # over tiles left-to-right, top-to-bottom.
+ tiles = sorted(tiles, key=lambda x: x.coords.left)
+ tiles = sorted(tiles, key=lambda x: x.coords.top)
+
+ # Prepare input image for inference.
+ image_tensor = SpandrelImageToImageModel.pil_to_tensor(image)
+
+ # Scale the tiles for re-assembling the final image.
+ scale = spandrel_model.scale
+ scaled_tiles = [cls.scale_tile(tile, scale=scale) for tile in tiles]
+
+ # Prepare the output tensor.
+ _, channels, height, width = image_tensor.shape
+ output_tensor = torch.zeros(
+ (height * scale, width * scale, channels), dtype=torch.uint8, device=torch.device("cpu")
+ )
+
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=spandrel_model.dtype)
+
+ # Run the model on each tile.
+ pbar = tqdm(list(zip(tiles, scaled_tiles, strict=True)), desc="Upscaling Tiles")
+
+ # Update progress, starting with 0.
+ step_callback(0, pbar.total)
+
+ for tile, scaled_tile in pbar:
+ # Exit early if the invocation has been canceled.
+ if is_canceled():
+ raise CanceledException
+
+ # Extract the current tile from the input tensor.
+ input_tile = image_tensor[:, :, tile.coords.top : tile.coords.bottom, tile.coords.left : tile.coords.right]
+
+ # Run the model on the tile.
+ output_tile = spandrel_model.run(input_tile)
+
+ # Convert the output tile into the output tensor's format.
+ # (N, C, H, W) -> (C, H, W)
+ output_tile = output_tile.squeeze(0)
+ # (C, H, W) -> (H, W, C)
+ output_tile = output_tile.permute(1, 2, 0)
+ output_tile = output_tile.clamp(0, 1)
+ output_tile = (output_tile * 255).to(dtype=torch.uint8, device=torch.device("cpu"))
+
+ # Merge the output tile into the output tensor.
+ # We only keep half of the overlap on the top and left side of the tile. We do this in case there are
+ # edge artifacts. We don't bother with any 'blending' in the current implementation - for most upscalers
+ # it seems unnecessary, but we may find a need in the future.
+ top_overlap = scaled_tile.overlap.top // 2
+ left_overlap = scaled_tile.overlap.left // 2
+ output_tensor[
+ scaled_tile.coords.top + top_overlap : scaled_tile.coords.bottom,
+ scaled_tile.coords.left + left_overlap : scaled_tile.coords.right,
+ :,
+ ] = output_tile[top_overlap:, left_overlap:, :]
+
+ step_callback(pbar.n + 1, pbar.total)
+
+ # Convert the output tensor to a PIL image.
+ np_image = output_tensor.detach().numpy().astype(np.uint8)
+ pil_image = Image.fromarray(np_image)
+
+ return pil_image
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to
+ # revisit this.
+ image = context.images.get_pil(self.image.image_name, mode="RGB")
+
+ def step_callback(step: int, total_steps: int) -> None:
+ context.util.signal_progress(
+ message=f"Processing tile {step}/{total_steps}",
+ percentage=step / total_steps,
+ )
+
+ # Do the upscaling.
+ with context.models.load(self.image_to_image_model) as spandrel_model:
+ assert isinstance(spandrel_model, SpandrelImageToImageModel)
+
+ # Upscale the image
+ pil_image = self.upscale_image(
+ image, self.tile_size, spandrel_model, context.util.is_canceled, step_callback
+ )
+
+ image_dto = context.images.save(image=pil_image)
+ return ImageOutput.build(image_dto)
+
+
+@invocation(
+ "spandrel_image_to_image_autoscale",
+ title="Image-to-Image (Autoscale)",
+ tags=["upscale"],
+ category="upscale",
+ version="1.0.0",
+)
+class SpandrelImageToImageAutoscaleInvocation(SpandrelImageToImageInvocation):
+ """Run any spandrel image-to-image model (https://github.com/chaiNNer-org/spandrel) until the target scale is reached."""
+
+ scale: float = InputField(
+ default=4.0,
+ gt=0.0,
+ le=16.0,
+ description="The final scale of the output image. If the model does not upscale the image, this will be ignored.",
+ )
+ fit_to_multiple_of_8: bool = InputField(
+ default=False,
+ description="If true, the output image will be resized to the nearest multiple of 8 in both dimensions.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ # Images are converted to RGB, because most models don't support an alpha channel. In the future, we may want to
+ # revisit this.
+ image = context.images.get_pil(self.image.image_name, mode="RGB")
+
+ # The target size of the image, determined by the provided scale. We'll run the upscaler until we hit this size.
+ # Later, we may mutate this value if the model doesn't upscale the image or if the user requested a multiple of 8.
+ target_width = int(image.width * self.scale)
+ target_height = int(image.height * self.scale)
+
+ def step_callback(iteration: int, step: int, total_steps: int) -> None:
+ context.util.signal_progress(
+ message=self._get_progress_message(iteration, step, total_steps),
+ percentage=step / total_steps,
+ )
+
+ # Do the upscaling.
+ with context.models.load(self.image_to_image_model) as spandrel_model:
+ assert isinstance(spandrel_model, SpandrelImageToImageModel)
+
+ iteration = 1
+ context.util.signal_progress(self._get_progress_message(iteration))
+
+ # First pass of upscaling. Note: `pil_image` will be mutated.
+ pil_image = self.upscale_image(
+ image,
+ self.tile_size,
+ spandrel_model,
+ context.util.is_canceled,
+ functools.partial(step_callback, iteration),
+ )
+
+ # Some models don't upscale the image, but we have no way to know this in advance. We'll check if the model
+ # upscaled the image and run the loop below if it did. We'll require the model to upscale both dimensions
+ # to be considered an upscale model.
+ is_upscale_model = pil_image.width > image.width and pil_image.height > image.height
+
+ if is_upscale_model:
+ # This is an upscale model, so we should keep upscaling until we reach the target size.
+ while pil_image.width < target_width or pil_image.height < target_height:
+ iteration += 1
+ context.util.signal_progress(self._get_progress_message(iteration))
+ pil_image = self.upscale_image(
+ pil_image,
+ self.tile_size,
+ spandrel_model,
+ context.util.is_canceled,
+ functools.partial(step_callback, iteration),
+ )
+
+ # Sanity check to prevent excessive or infinite loops. All known upscaling models are at least 2x.
+ # Our max scale is 16x, so with a 2x model, we should never exceed 16x == 2^4 -> 4 iterations.
+ # We'll allow one extra iteration "just in case" and bail at 5 upscaling iterations. In practice,
+ # we should never reach this limit.
+ if iteration >= 5:
+ context.logger.warning(
+ "Upscale loop reached maximum iteration count of 5, stopping upscaling early."
+ )
+ break
+ else:
+ # This model doesn't upscale the image. We should ignore the scale parameter, modifying the output size
+ # to be the same as the processed image size.
+
+ # The output size is now the size of the processed image.
+ target_width = pil_image.width
+ target_height = pil_image.height
+
+ # Warn the user if they requested a scale greater than 1.
+ if self.scale > 1:
+ context.logger.warning(
+ "Model does not increase the size of the image, but a greater scale than 1 was requested. Image will not be scaled."
+ )
+
+ # We may need to resize the image to a multiple of 8. Use floor division to ensure we don't scale the image up
+ # in the final resize
+ if self.fit_to_multiple_of_8:
+ target_width = int(target_width // 8 * 8)
+ target_height = int(target_height // 8 * 8)
+
+ # Final resize. Per PIL documentation, Lanczos provides the best quality for both upscale and downscale.
+ # See: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters-comparison-table
+ pil_image = pil_image.resize((target_width, target_height), resample=Image.Resampling.LANCZOS)
+
+ image_dto = context.images.save(image=pil_image)
+ return ImageOutput.build(image_dto)
+
+ @classmethod
+ def _get_progress_message(cls, iteration: int, step: int | None = None, total_steps: int | None = None) -> str:
+ if step is not None and total_steps is not None:
+ return f"Processing iteration {iteration}, tile {step}/{total_steps}"
+
+ return f"Processing iteration {iteration}"
diff --git a/invokeai/app/invocations/strings.py b/invokeai/app/invocations/strings.py
new file mode 100644
index 00000000000..6d64e8771ad
--- /dev/null
+++ b/invokeai/app/invocations/strings.py
@@ -0,0 +1,134 @@
+# 2023 skunkworxdark (https://github.com/skunkworxdark)
+
+import re
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
+from invokeai.app.invocations.fields import InputField, OutputField, UIComponent
+from invokeai.app.invocations.primitives import StringOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+
+
+@invocation_output("string_pos_neg_output")
+class StringPosNegOutput(BaseInvocationOutput):
+ """Base class for invocations that output a positive and negative string"""
+
+ positive_string: str = OutputField(description="Positive string")
+ negative_string: str = OutputField(description="Negative string")
+
+
+@invocation(
+ "string_split_neg",
+ title="String Split Negative",
+ tags=["string", "split", "negative"],
+ category="strings",
+ version="1.0.1",
+)
+class StringSplitNegInvocation(BaseInvocation):
+ """Splits string into two strings, inside [] goes into negative string everthing else goes into positive string. Each [ and ] character is replaced with a space"""
+
+ string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea)
+
+ def invoke(self, context: InvocationContext) -> StringPosNegOutput:
+ p_string = ""
+ n_string = ""
+ brackets_depth = 0
+ escaped = False
+
+ for char in self.string or "":
+ if char == "[" and not escaped:
+ n_string += " "
+ brackets_depth += 1
+ elif char == "]" and not escaped:
+ brackets_depth -= 1
+ char = " "
+ elif brackets_depth > 0:
+ n_string += char
+ else:
+ p_string += char
+
+ # keep track of the escape char but only if it isn't escaped already
+ if char == "\\" and not escaped:
+ escaped = True
+ else:
+ escaped = False
+
+ return StringPosNegOutput(positive_string=p_string, negative_string=n_string)
+
+
+@invocation_output("string_2_output")
+class String2Output(BaseInvocationOutput):
+ """Base class for invocations that output two strings"""
+
+ string_1: str = OutputField(description="string 1")
+ string_2: str = OutputField(description="string 2")
+
+
+@invocation("string_split", title="String Split", tags=["string", "split"], category="strings", version="1.0.1")
+class StringSplitInvocation(BaseInvocation):
+ """Splits string into two strings, based on the first occurance of the delimiter. The delimiter will be removed from the string"""
+
+ string: str = InputField(default="", description="String to split", ui_component=UIComponent.Textarea)
+ delimiter: str = InputField(
+ default="", description="Delimiter to spilt with. blank will split on the first whitespace"
+ )
+
+ def invoke(self, context: InvocationContext) -> String2Output:
+ result = self.string.split(self.delimiter, 1)
+ if len(result) == 2:
+ part1, part2 = result
+ else:
+ part1 = result[0]
+ part2 = ""
+
+ return String2Output(string_1=part1, string_2=part2)
+
+
+@invocation("string_join", title="String Join", tags=["string", "join"], category="strings", version="1.0.1")
+class StringJoinInvocation(BaseInvocation):
+ """Joins string left to string right"""
+
+ string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea)
+ string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea)
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ return StringOutput(value=((self.string_left or "") + (self.string_right or "")))
+
+
+@invocation(
+ "string_join_three", title="String Join Three", tags=["string", "join"], category="strings", version="1.0.1"
+)
+class StringJoinThreeInvocation(BaseInvocation):
+ """Joins string left to string middle to string right"""
+
+ string_left: str = InputField(default="", description="String Left", ui_component=UIComponent.Textarea)
+ string_middle: str = InputField(default="", description="String Middle", ui_component=UIComponent.Textarea)
+ string_right: str = InputField(default="", description="String Right", ui_component=UIComponent.Textarea)
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ return StringOutput(value=((self.string_left or "") + (self.string_middle or "") + (self.string_right or "")))
+
+
+@invocation(
+ "string_replace", title="String Replace", tags=["string", "replace", "regex"], category="strings", version="1.0.1"
+)
+class StringReplaceInvocation(BaseInvocation):
+ """Replaces the search string with the replace string"""
+
+ string: str = InputField(default="", description="String to work on", ui_component=UIComponent.Textarea)
+ search_string: str = InputField(default="", description="String to search for", ui_component=UIComponent.Textarea)
+ replace_string: str = InputField(
+ default="", description="String to replace the search", ui_component=UIComponent.Textarea
+ )
+ use_regex: bool = InputField(
+ default=False, description="Use search string as a regex expression (non regex is case insensitive)"
+ )
+
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ pattern = self.search_string or ""
+ new_string = self.string or ""
+ if len(pattern) > 0:
+ if not self.use_regex:
+ # None regex so make case insensitve
+ pattern = "(?i)" + re.escape(pattern)
+ new_string = re.sub(pattern, (self.replace_string or ""), new_string)
+ return StringOutput(value=new_string)
diff --git a/invokeai/app/invocations/t2i_adapter.py b/invokeai/app/invocations/t2i_adapter.py
new file mode 100644
index 00000000000..cf4b7cda474
--- /dev/null
+++ b/invokeai/app/invocations/t2i_adapter.py
@@ -0,0 +1,102 @@
+from typing import Union
+
+from pydantic import BaseModel, Field, field_validator, model_validator
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+class T2IAdapterField(BaseModel):
+ image: ImageField = Field(description="The T2I-Adapter image prompt.")
+ t2i_adapter_model: ModelIdentifierField = Field(description="The T2I-Adapter model to use.")
+ weight: Union[float, list[float]] = Field(default=1, description="The weight given to the T2I-Adapter")
+ begin_step_percent: float = Field(
+ default=0, ge=0, le=1, description="When the T2I-Adapter is first applied (% of total steps)"
+ )
+ end_step_percent: float = Field(
+ default=1, ge=0, le=1, description="When the T2I-Adapter is last applied (% of total steps)"
+ )
+ resize_mode: CONTROLNET_RESIZE_VALUES = Field(default="just_resize", description="The resize mode to use")
+
+ @field_validator("weight")
+ @classmethod
+ def validate_ip_adapter_weight(cls, v):
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self):
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+
+@invocation_output("t2i_adapter_output")
+class T2IAdapterOutput(BaseInvocationOutput):
+ t2i_adapter: T2IAdapterField = OutputField(description=FieldDescriptions.t2i_adapter, title="T2I Adapter")
+
+
+@invocation(
+ "t2i_adapter",
+ title="T2I-Adapter - SD1.5, SDXL",
+ tags=["t2i_adapter", "control"],
+ category="conditioning",
+ version="1.0.4",
+)
+class T2IAdapterInvocation(BaseInvocation):
+ """Collects T2I-Adapter info to pass to other nodes."""
+
+ # Inputs
+ image: ImageField = InputField(description="The IP-Adapter image prompt.")
+ t2i_adapter_model: ModelIdentifierField = InputField(
+ description="The T2I-Adapter model.",
+ title="T2I-Adapter Model",
+ ui_order=-1,
+ ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL],
+ ui_model_type=ModelType.T2IAdapter,
+ )
+ weight: Union[float, list[float]] = InputField(
+ default=1, ge=0, description="The weight given to the T2I-Adapter", title="Weight"
+ )
+ begin_step_percent: float = InputField(
+ default=0, ge=0, le=1, description="When the T2I-Adapter is first applied (% of total steps)"
+ )
+ end_step_percent: float = InputField(
+ default=1, ge=0, le=1, description="When the T2I-Adapter is last applied (% of total steps)"
+ )
+ resize_mode: CONTROLNET_RESIZE_VALUES = InputField(
+ default="just_resize",
+ description="The resize mode applied to the T2I-Adapter input image so that it matches the target output size.",
+ )
+
+ @field_validator("weight")
+ @classmethod
+ def validate_ip_adapter_weight(cls, v):
+ validate_weights(v)
+ return v
+
+ @model_validator(mode="after")
+ def validate_begin_end_step_percent(self):
+ validate_begin_end_step(self.begin_step_percent, self.end_step_percent)
+ return self
+
+ def invoke(self, context: InvocationContext) -> T2IAdapterOutput:
+ return T2IAdapterOutput(
+ t2i_adapter=T2IAdapterField(
+ image=self.image,
+ t2i_adapter_model=self.t2i_adapter_model,
+ weight=self.weight,
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ resize_mode=self.resize_mode,
+ )
+ )
diff --git a/invokeai/app/invocations/text_llm.py b/invokeai/app/invocations/text_llm.py
new file mode 100644
index 00000000000..789e65be018
--- /dev/null
+++ b/invokeai/app/invocations/text_llm.py
@@ -0,0 +1,65 @@
+import torch
+from transformers import AutoTokenizer
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import FieldDescriptions, InputField, UIComponent
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.invocations.primitives import StringOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import ModelType
+from invokeai.backend.text_llm_pipeline import DEFAULT_SYSTEM_PROMPT, TextLLMPipeline
+from invokeai.backend.util.devices import TorchDevice
+
+
+@invocation(
+ "text_llm",
+ title="Text LLM",
+ tags=["llm", "text", "prompt"],
+ category="llm",
+ version="1.0.0",
+ classification=Classification.Beta,
+)
+class TextLLMInvocation(BaseInvocation):
+ """Run a text language model to generate or expand text (e.g. for prompt expansion)."""
+
+ prompt: str = InputField(
+ default="",
+ description="Input text prompt.",
+ ui_component=UIComponent.Textarea,
+ )
+ system_prompt: str = InputField(
+ default=DEFAULT_SYSTEM_PROMPT,
+ description="System prompt that guides the model's behavior.",
+ ui_component=UIComponent.Textarea,
+ )
+ text_llm_model: ModelIdentifierField = InputField(
+ title="Text LLM Model",
+ description=FieldDescriptions.text_llm_model,
+ ui_model_type=ModelType.TextLLM,
+ )
+ max_tokens: int = InputField(
+ default=300,
+ ge=1,
+ le=2048,
+ description="Maximum number of tokens to generate.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> StringOutput:
+ model_config = context.models.get_config(self.text_llm_model)
+
+ with context.models.load(self.text_llm_model).model_on_device() as (_, model):
+ model_abs_path = context.models.get_absolute_path(model_config)
+ tokenizer = AutoTokenizer.from_pretrained(model_abs_path, local_files_only=True)
+
+ pipeline = TextLLMPipeline(model, tokenizer)
+ model_device = next(model.parameters()).device
+ output = pipeline.run(
+ prompt=self.prompt,
+ system_prompt=self.system_prompt,
+ max_new_tokens=self.max_tokens,
+ device=model_device,
+ dtype=TorchDevice.choose_torch_dtype(),
+ )
+
+ return StringOutput(value=output)
diff --git a/invokeai/app/invocations/tiled_multi_diffusion_denoise_latents.py b/invokeai/app/invocations/tiled_multi_diffusion_denoise_latents.py
new file mode 100644
index 00000000000..80067b8c931
--- /dev/null
+++ b/invokeai/app/invocations/tiled_multi_diffusion_denoise_latents.py
@@ -0,0 +1,292 @@
+import copy
+from contextlib import ExitStack
+from typing import Iterator, Tuple
+
+import torch
+from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel
+from diffusers.schedulers.scheduling_utils import SchedulerMixin
+from pydantic import field_validator
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.controlnet import ControlField
+from invokeai.app.invocations.denoise_latents import DenoiseLatentsInvocation, get_scheduler
+from invokeai.app.invocations.fields import (
+ ConditioningField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ UIType,
+)
+from invokeai.app.invocations.model import UNetField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusers_pipeline import ControlNetData, PipelineIntermediateState
+from invokeai.backend.stable_diffusion.multi_diffusion_pipeline import (
+ MultiDiffusionPipeline,
+ MultiDiffusionRegionConditioning,
+)
+from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
+from invokeai.backend.tiles.tiles import (
+ calc_tiles_min_overlap,
+)
+from invokeai.backend.tiles.utils import TBLR
+from invokeai.backend.util.devices import TorchDevice
+
+
+def crop_controlnet_data(control_data: ControlNetData, latent_region: TBLR) -> ControlNetData:
+ """Crop a ControlNetData object to a region."""
+ # Create a shallow copy of the control_data object.
+ control_data_copy = copy.copy(control_data)
+ # The ControlNet reference image is the only attribute that needs to be cropped.
+ control_data_copy.image_tensor = control_data.image_tensor[
+ :,
+ :,
+ latent_region.top * LATENT_SCALE_FACTOR : latent_region.bottom * LATENT_SCALE_FACTOR,
+ latent_region.left * LATENT_SCALE_FACTOR : latent_region.right * LATENT_SCALE_FACTOR,
+ ]
+ return control_data_copy
+
+
+@invocation(
+ "tiled_multi_diffusion_denoise_latents",
+ title="Tiled Multi-Diffusion Denoise - SD1.5, SDXL",
+ tags=["upscale", "denoise"],
+ category="latents",
+ version="1.0.1",
+)
+class TiledMultiDiffusionDenoiseLatents(BaseInvocation):
+ """Tiled Multi-Diffusion denoising.
+
+ This node handles automatically tiling the input image, and is primarily intended for global refinement of images
+ in tiled upscaling workflows. Future Multi-Diffusion nodes should allow the user to specify custom regions with
+ different parameters for each region to harness the full power of Multi-Diffusion.
+
+ This node has a similar interface to the `DenoiseLatents` node, but it has a reduced feature set (no IP-Adapter,
+ T2I-Adapter, masking, etc.).
+ """
+
+ positive_conditioning: ConditioningField = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: ConditioningField = InputField(
+ description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ noise: LatentsField | None = InputField(
+ default=None,
+ description=FieldDescriptions.noise,
+ input=Input.Connection,
+ )
+ latents: LatentsField | None = InputField(
+ default=None,
+ description=FieldDescriptions.latents,
+ input=Input.Connection,
+ )
+ tile_height: int = InputField(
+ default=1024, gt=0, multiple_of=LATENT_SCALE_FACTOR, description="Height of the tiles in image space."
+ )
+ tile_width: int = InputField(
+ default=1024, gt=0, multiple_of=LATENT_SCALE_FACTOR, description="Width of the tiles in image space."
+ )
+ tile_overlap: int = InputField(
+ default=32,
+ multiple_of=LATENT_SCALE_FACTOR,
+ gt=0,
+ description="The overlap between adjacent tiles in pixel space. (Of course, tile merging is applied in latent "
+ "space.) Tiles will be cropped during merging (if necessary) to ensure that they overlap by exactly this "
+ "amount.",
+ )
+ steps: int = InputField(default=18, gt=0, description=FieldDescriptions.steps)
+ cfg_scale: float | list[float] = InputField(default=6.0, description=FieldDescriptions.cfg_scale, title="CFG Scale")
+ 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)
+ scheduler: SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description=FieldDescriptions.scheduler,
+ ui_type=UIType.Scheduler,
+ )
+ unet: UNetField = InputField(
+ description=FieldDescriptions.unet,
+ input=Input.Connection,
+ title="UNet",
+ )
+ cfg_rescale_multiplier: float = InputField(
+ title="CFG Rescale Multiplier", default=0, ge=0, lt=1, description=FieldDescriptions.cfg_rescale_multiplier
+ )
+ control: ControlField | list[ControlField] | None = InputField(
+ default=None,
+ input=Input.Connection,
+ )
+
+ @field_validator("cfg_scale")
+ def ge_one(cls, v: list[float] | float) -> list[float] | float:
+ """Validate that all cfg_scale values are >= 1"""
+ if isinstance(v, list):
+ for i in v:
+ if i < 1:
+ raise ValueError("cfg_scale must be greater than 1")
+ else:
+ if v < 1:
+ raise ValueError("cfg_scale must be greater than 1")
+ return v
+
+ @staticmethod
+ def create_pipeline(
+ unet: UNet2DConditionModel,
+ scheduler: SchedulerMixin,
+ ) -> MultiDiffusionPipeline:
+ # TODO(ryand): Get rid of this FakeVae hack.
+ class FakeVae:
+ class FakeVaeConfig:
+ def __init__(self) -> None:
+ self.block_out_channels = [0]
+
+ def __init__(self) -> None:
+ self.config = FakeVae.FakeVaeConfig()
+
+ return MultiDiffusionPipeline(
+ vae=FakeVae(),
+ text_encoder=None,
+ tokenizer=None,
+ unet=unet,
+ scheduler=scheduler,
+ safety_checker=None,
+ feature_extractor=None,
+ requires_safety_checker=False,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ # Convert tile image-space dimensions to latent-space dimensions.
+ latent_tile_height = self.tile_height // LATENT_SCALE_FACTOR
+ latent_tile_width = self.tile_width // LATENT_SCALE_FACTOR
+ latent_tile_overlap = self.tile_overlap // LATENT_SCALE_FACTOR
+
+ seed, noise, latents = DenoiseLatentsInvocation.prepare_noise_and_latents(context, self.noise, self.latents)
+ _, _, latent_height, latent_width = latents.shape
+
+ # Calculate the tile locations to cover the latent-space image.
+ # TODO(ryand): In the future, we may want to revisit the tile overlap strategy. Things to consider:
+ # - How much overlap 'context' to provide for each denoising step.
+ # - How much overlap to use during merging/blending.
+ # - Should we 'jitter' the tile locations in each step so that the seams are in different places?
+ tiles = calc_tiles_min_overlap(
+ image_height=latent_height,
+ image_width=latent_width,
+ tile_height=latent_tile_height,
+ tile_width=latent_tile_width,
+ min_overlap=latent_tile_overlap,
+ )
+
+ # Get the unet's config so that we can pass the base to sd_step_callback().
+ unet_config = context.models.get_config(self.unet.unet.key)
+
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, unet_config.base)
+
+ # Prepare an iterator that yields the UNet's LoRA models and their weights.
+ def _lora_loader() -> Iterator[Tuple[ModelPatchRaw, float]]:
+ for lora in self.unet.loras:
+ lora_info = context.models.load(lora.lora)
+ assert isinstance(lora_info.model, ModelPatchRaw)
+ yield (lora_info.model, lora.weight)
+ del lora_info
+
+ device = TorchDevice.choose_torch_device()
+ with (
+ ExitStack() as exit_stack,
+ context.models.load(self.unet.unet) as unet,
+ LayerPatcher.apply_smart_model_patches(
+ model=unet, patches=_lora_loader(), prefix="lora_unet_", dtype=unet.dtype
+ ),
+ ):
+ assert isinstance(unet, UNet2DConditionModel)
+ latents = latents.to(device=device, dtype=unet.dtype)
+ if noise is not None:
+ noise = noise.to(device=device, dtype=unet.dtype)
+ scheduler = get_scheduler(
+ context=context,
+ scheduler_info=self.unet.scheduler,
+ scheduler_name=self.scheduler,
+ seed=seed,
+ unet_config=unet_config,
+ )
+ pipeline = self.create_pipeline(unet=unet, scheduler=scheduler)
+
+ # Prepare the prompt conditioning data. The same prompt conditioning is applied to all tiles.
+ conditioning_data = DenoiseLatentsInvocation.get_conditioning_data(
+ context=context,
+ positive_conditioning_field=self.positive_conditioning,
+ negative_conditioning_field=self.negative_conditioning,
+ device=device,
+ dtype=unet.dtype,
+ latent_height=latent_tile_height,
+ latent_width=latent_tile_width,
+ cfg_scale=self.cfg_scale,
+ steps=self.steps,
+ cfg_rescale_multiplier=self.cfg_rescale_multiplier,
+ )
+
+ controlnet_data = DenoiseLatentsInvocation.prep_control_data(
+ context=context,
+ control_input=self.control,
+ latents_shape=list(latents.shape),
+ device=device,
+ # do_classifier_free_guidance=(self.cfg_scale >= 1.0))
+ do_classifier_free_guidance=True,
+ exit_stack=exit_stack,
+ )
+
+ # Split the controlnet_data into tiles.
+ # controlnet_data_tiles[t][c] is the c'th control data for the t'th tile.
+ controlnet_data_tiles: list[list[ControlNetData]] = []
+ for tile in tiles:
+ tile_controlnet_data = [crop_controlnet_data(cn, tile.coords) for cn in controlnet_data or []]
+ controlnet_data_tiles.append(tile_controlnet_data)
+
+ # Prepare the MultiDiffusionRegionConditioning list.
+ multi_diffusion_conditioning: list[MultiDiffusionRegionConditioning] = []
+ for tile, tile_controlnet_data in zip(tiles, controlnet_data_tiles, strict=True):
+ multi_diffusion_conditioning.append(
+ MultiDiffusionRegionConditioning(
+ region=tile,
+ text_conditioning_data=conditioning_data,
+ control_data=tile_controlnet_data,
+ )
+ )
+
+ timesteps, init_timestep, scheduler_step_kwargs = DenoiseLatentsInvocation.init_scheduler(
+ scheduler,
+ device=device,
+ steps=self.steps,
+ denoising_start=self.denoising_start,
+ denoising_end=self.denoising_end,
+ seed=seed,
+ )
+
+ # Run Multi-Diffusion denoising.
+ result_latents = pipeline.multi_diffusion_denoise(
+ multi_diffusion_conditioning=multi_diffusion_conditioning,
+ target_overlap=latent_tile_overlap,
+ latents=latents,
+ scheduler_step_kwargs=scheduler_step_kwargs,
+ noise=noise,
+ timesteps=timesteps,
+ init_timestep=init_timestep,
+ callback=step_callback,
+ )
+
+ result_latents = result_latents.to("cpu")
+ # TODO(ryand): I copied this from DenoiseLatentsInvocation. I'm not sure if it's actually important.
+ TorchDevice.empty_cache()
+
+ name = context.tensors.save(tensor=result_latents)
+ return LatentsOutput.build(latents_name=name, latents=result_latents, seed=None)
diff --git a/invokeai/app/invocations/tiles.py b/invokeai/app/invocations/tiles.py
new file mode 100644
index 00000000000..a631f3ba4ec
--- /dev/null
+++ b/invokeai/app/invocations/tiles.py
@@ -0,0 +1,284 @@
+from typing import Literal
+
+import numpy as np
+from PIL import Image
+from pydantic import BaseModel
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import ImageField, Input, InputField, OutputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.tiles.tiles import (
+ calc_tiles_even_split,
+ calc_tiles_min_overlap,
+ calc_tiles_with_overlap,
+ merge_tiles_with_linear_blending,
+ merge_tiles_with_seam_blending,
+)
+from invokeai.backend.tiles.utils import Tile
+
+
+class TileWithImage(BaseModel):
+ tile: Tile
+ image: ImageField
+
+
+@invocation_output("calculate_image_tiles_output")
+class CalculateImageTilesOutput(BaseInvocationOutput):
+ tiles: list[Tile] = OutputField(description="The tiles coordinates that cover a particular image shape.")
+
+
+@invocation(
+ "calculate_image_tiles",
+ title="Calculate Image Tiles",
+ tags=["tiles"],
+ category="tiles",
+ version="1.0.1",
+)
+class CalculateImageTilesInvocation(BaseInvocation):
+ """Calculate the coordinates and overlaps of tiles that cover a target image shape."""
+
+ image_width: int = InputField(ge=1, default=1024, description="The image width, in pixels, to calculate tiles for.")
+ image_height: int = InputField(
+ ge=1, default=1024, description="The image height, in pixels, to calculate tiles for."
+ )
+ tile_width: int = InputField(ge=1, default=576, description="The tile width, in pixels.")
+ tile_height: int = InputField(ge=1, default=576, description="The tile height, in pixels.")
+ overlap: int = InputField(
+ ge=0,
+ default=128,
+ description="The target overlap, in pixels, between adjacent tiles. Adjacent tiles will overlap by at least this amount",
+ )
+
+ def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput:
+ tiles = calc_tiles_with_overlap(
+ image_height=self.image_height,
+ image_width=self.image_width,
+ tile_height=self.tile_height,
+ tile_width=self.tile_width,
+ overlap=self.overlap,
+ )
+ return CalculateImageTilesOutput(tiles=tiles)
+
+
+@invocation(
+ "calculate_image_tiles_even_split",
+ title="Calculate Image Tiles Even Split",
+ tags=["tiles"],
+ category="tiles",
+ version="1.1.1",
+)
+class CalculateImageTilesEvenSplitInvocation(BaseInvocation):
+ """Calculate the coordinates and overlaps of tiles that cover a target image shape."""
+
+ image_width: int = InputField(ge=1, default=1024, description="The image width, in pixels, to calculate tiles for.")
+ image_height: int = InputField(
+ ge=1, default=1024, description="The image height, in pixels, to calculate tiles for."
+ )
+ num_tiles_x: int = InputField(
+ default=2,
+ ge=1,
+ description="Number of tiles to divide image into on the x axis",
+ )
+ num_tiles_y: int = InputField(
+ default=2,
+ ge=1,
+ description="Number of tiles to divide image into on the y axis",
+ )
+ overlap: int = InputField(
+ default=128,
+ ge=0,
+ multiple_of=8,
+ description="The overlap, in pixels, between adjacent tiles.",
+ )
+
+ def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput:
+ tiles = calc_tiles_even_split(
+ image_height=self.image_height,
+ image_width=self.image_width,
+ num_tiles_x=self.num_tiles_x,
+ num_tiles_y=self.num_tiles_y,
+ overlap=self.overlap,
+ )
+ return CalculateImageTilesOutput(tiles=tiles)
+
+
+@invocation(
+ "calculate_image_tiles_min_overlap",
+ title="Calculate Image Tiles Minimum Overlap",
+ tags=["tiles"],
+ category="tiles",
+ version="1.0.1",
+)
+class CalculateImageTilesMinimumOverlapInvocation(BaseInvocation):
+ """Calculate the coordinates and overlaps of tiles that cover a target image shape."""
+
+ image_width: int = InputField(ge=1, default=1024, description="The image width, in pixels, to calculate tiles for.")
+ image_height: int = InputField(
+ ge=1, default=1024, description="The image height, in pixels, to calculate tiles for."
+ )
+ tile_width: int = InputField(ge=1, default=576, description="The tile width, in pixels.")
+ tile_height: int = InputField(ge=1, default=576, description="The tile height, in pixels.")
+ min_overlap: int = InputField(default=128, ge=0, description="Minimum overlap between adjacent tiles, in pixels.")
+
+ def invoke(self, context: InvocationContext) -> CalculateImageTilesOutput:
+ tiles = calc_tiles_min_overlap(
+ image_height=self.image_height,
+ image_width=self.image_width,
+ tile_height=self.tile_height,
+ tile_width=self.tile_width,
+ min_overlap=self.min_overlap,
+ )
+ return CalculateImageTilesOutput(tiles=tiles)
+
+
+@invocation_output("tile_to_properties_output")
+class TileToPropertiesOutput(BaseInvocationOutput):
+ coords_left: int = OutputField(description="Left coordinate of the tile relative to its parent image.")
+ coords_right: int = OutputField(description="Right coordinate of the tile relative to its parent image.")
+ coords_top: int = OutputField(description="Top coordinate of the tile relative to its parent image.")
+ coords_bottom: int = OutputField(description="Bottom coordinate of the tile relative to its parent image.")
+
+ # HACK: The width and height fields are 'meta' fields that can easily be calculated from the other fields on this
+ # object. Including redundant fields that can cheaply/easily be re-calculated goes against conventional API design
+ # principles. These fields are included, because 1) they are often useful in tiled workflows, and 2) they are
+ # difficult to calculate in a workflow (even though it's just a couple of subtraction nodes the graph gets
+ # surprisingly complicated).
+ width: int = OutputField(description="The width of the tile. Equal to coords_right - coords_left.")
+ height: int = OutputField(description="The height of the tile. Equal to coords_bottom - coords_top.")
+
+ overlap_top: int = OutputField(description="Overlap between this tile and its top neighbor.")
+ overlap_bottom: int = OutputField(description="Overlap between this tile and its bottom neighbor.")
+ overlap_left: int = OutputField(description="Overlap between this tile and its left neighbor.")
+ overlap_right: int = OutputField(description="Overlap between this tile and its right neighbor.")
+
+
+@invocation(
+ "tile_to_properties",
+ title="Tile to Properties",
+ tags=["tiles"],
+ category="tiles",
+ version="1.0.1",
+)
+class TileToPropertiesInvocation(BaseInvocation):
+ """Split a Tile into its individual properties."""
+
+ tile: Tile = InputField(description="The tile to split into properties.")
+
+ def invoke(self, context: InvocationContext) -> TileToPropertiesOutput:
+ return TileToPropertiesOutput(
+ coords_left=self.tile.coords.left,
+ coords_right=self.tile.coords.right,
+ coords_top=self.tile.coords.top,
+ coords_bottom=self.tile.coords.bottom,
+ width=self.tile.coords.right - self.tile.coords.left,
+ height=self.tile.coords.bottom - self.tile.coords.top,
+ overlap_top=self.tile.overlap.top,
+ overlap_bottom=self.tile.overlap.bottom,
+ overlap_left=self.tile.overlap.left,
+ overlap_right=self.tile.overlap.right,
+ )
+
+
+@invocation_output("pair_tile_image_output")
+class PairTileImageOutput(BaseInvocationOutput):
+ tile_with_image: TileWithImage = OutputField(description="A tile description with its corresponding image.")
+
+
+@invocation(
+ "pair_tile_image",
+ title="Pair Tile with Image",
+ tags=["tiles"],
+ category="tiles",
+ version="1.0.1",
+)
+class PairTileImageInvocation(BaseInvocation):
+ """Pair an image with its tile properties."""
+
+ # TODO(ryand): The only reason that PairTileImage is needed is because the iterate/collect nodes don't preserve
+ # order. Can this be fixed?
+
+ image: ImageField = InputField(description="The tile image.")
+ tile: Tile = InputField(description="The tile properties.")
+
+ def invoke(self, context: InvocationContext) -> PairTileImageOutput:
+ return PairTileImageOutput(
+ tile_with_image=TileWithImage(
+ tile=self.tile,
+ image=self.image,
+ )
+ )
+
+
+BLEND_MODES = Literal["Linear", "Seam"]
+
+
+@invocation(
+ "merge_tiles_to_image",
+ title="Merge Tiles to Image",
+ tags=["tiles"],
+ category="tiles",
+ version="1.1.1",
+)
+class MergeTilesToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Merge multiple tile images into a single image."""
+
+ # Inputs
+ tiles_with_images: list[TileWithImage] = InputField(description="A list of tile images with tile properties.")
+ blend_mode: BLEND_MODES = InputField(
+ default="Seam",
+ description="blending type Linear or Seam",
+ input=Input.Direct,
+ )
+ blend_amount: int = InputField(
+ default=32,
+ ge=0,
+ description="The amount to blend adjacent tiles in pixels. Must be <= the amount of overlap between adjacent tiles.",
+ )
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ images = [twi.image for twi in self.tiles_with_images]
+ tiles = [twi.tile for twi in self.tiles_with_images]
+
+ # Infer the output image dimensions from the max/min tile limits.
+ height = 0
+ width = 0
+ for tile in tiles:
+ height = max(height, tile.coords.bottom)
+ width = max(width, tile.coords.right)
+
+ # Get all tile images for processing.
+ # TODO(ryand): It pains me that we spend time PNG decoding each tile from disk when they almost certainly
+ # existed in memory at an earlier point in the graph.
+ tile_np_images: list[np.ndarray] = []
+ for image in images:
+ pil_image = context.images.get_pil(image.image_name)
+ pil_image = pil_image.convert("RGB")
+ tile_np_images.append(np.array(pil_image))
+
+ # Prepare the output image buffer.
+ # Check the first tile to determine how many image channels are expected in the output.
+ channels = tile_np_images[0].shape[-1]
+ dtype = tile_np_images[0].dtype
+ np_image = np.zeros(shape=(height, width, channels), dtype=dtype)
+ if self.blend_mode == "Linear":
+ merge_tiles_with_linear_blending(
+ dst_image=np_image, tiles=tiles, tile_images=tile_np_images, blend_amount=self.blend_amount
+ )
+ elif self.blend_mode == "Seam":
+ merge_tiles_with_seam_blending(
+ dst_image=np_image, tiles=tiles, tile_images=tile_np_images, blend_amount=self.blend_amount
+ )
+ else:
+ raise ValueError(f"Unsupported blend mode: '{self.blend_mode}'.")
+
+ # Convert into a PIL image and save
+ pil_image = Image.fromarray(np_image)
+
+ image_dto = context.images.save(image=pil_image)
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py
new file mode 100644
index 00000000000..64e372a0f6b
--- /dev/null
+++ b/invokeai/app/invocations/upscale.py
@@ -0,0 +1,114 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) & the InvokeAI Team
+from typing import Literal
+
+import cv2
+import numpy as np
+from PIL import Image
+from pydantic import ConfigDict
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
+from invokeai.app.invocations.fields import ImageField, InputField, WithBoard, WithMetadata
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet
+from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN
+
+# TODO: Populate this from disk?
+# TODO: Use model manager to load?
+ESRGAN_MODELS = Literal[
+ "RealESRGAN_x4plus.pth",
+ "RealESRGAN_x4plus_anime_6B.pth",
+ "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth",
+ "RealESRGAN_x2plus.pth",
+]
+
+ESRGAN_MODEL_URLS: dict[str, str] = {
+ "RealESRGAN_x4plus.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth",
+ "RealESRGAN_x4plus_anime_6B.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth",
+ "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.1/ESRGAN_SRx4_DF2KOST_official-ff704c30.pth",
+ "RealESRGAN_x2plus.pth": "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth",
+}
+
+
+@invocation("esrgan", title="Upscale (RealESRGAN)", tags=["esrgan", "upscale"], category="upscale", version="1.3.2")
+class ESRGANInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Upscales an image using RealESRGAN."""
+
+ image: ImageField = InputField(description="The input image")
+ model_name: ESRGAN_MODELS = InputField(default="RealESRGAN_x4plus.pth", description="The Real-ESRGAN model to use")
+ tile_size: int = InputField(
+ default=400, ge=0, description="Tile size for tiled ESRGAN upscaling (0=tiling disabled)"
+ )
+
+ model_config = ConfigDict(protected_namespaces=())
+
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ rrdbnet_model = None
+ netscale = None
+
+ if self.model_name in [
+ "RealESRGAN_x4plus.pth",
+ "ESRGAN_SRx4_DF2KOST_official-ff704c30.pth",
+ ]:
+ # x4 RRDBNet model
+ rrdbnet_model = RRDBNet(
+ num_in_ch=3,
+ num_out_ch=3,
+ num_feat=64,
+ num_block=23,
+ num_grow_ch=32,
+ scale=4,
+ )
+ netscale = 4
+ elif self.model_name in ["RealESRGAN_x4plus_anime_6B.pth"]:
+ # x4 RRDBNet model, 6 blocks
+ rrdbnet_model = RRDBNet(
+ num_in_ch=3,
+ num_out_ch=3,
+ num_feat=64,
+ num_block=6, # 6 blocks
+ num_grow_ch=32,
+ scale=4,
+ )
+ netscale = 4
+ elif self.model_name in ["RealESRGAN_x2plus.pth"]:
+ # x2 RRDBNet model
+ rrdbnet_model = RRDBNet(
+ num_in_ch=3,
+ num_out_ch=3,
+ num_feat=64,
+ num_block=23,
+ num_grow_ch=32,
+ scale=2,
+ )
+ netscale = 2
+ else:
+ msg = f"Invalid RealESRGAN model: {self.model_name}"
+ context.logger.error(msg)
+ raise ValueError(msg)
+
+ loadnet = context.models.load_remote_model(
+ source=ESRGAN_MODEL_URLS[self.model_name],
+ )
+
+ with loadnet as loadnet_model:
+ upscaler = RealESRGAN(
+ scale=netscale,
+ loadnet=loadnet_model,
+ model=rrdbnet_model,
+ half=False,
+ tile=self.tile_size,
+ )
+
+ # prepare image - Real-ESRGAN uses cv2 internally, and cv2 uses BGR vs RGB for PIL
+ # TODO: This strips the alpha... is that okay?
+ cv2_image = cv2.cvtColor(np.array(image.convert("RGB")), cv2.COLOR_RGB2BGR)
+ upscaled_image = upscaler.upscale(cv2_image)
+
+ pil_image = Image.fromarray(cv2.cvtColor(upscaled_image, cv2.COLOR_BGR2RGB)).convert("RGBA")
+
+ image_dto = context.images.save(image=pil_image)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/util.py b/invokeai/app/invocations/util.py
new file mode 100644
index 00000000000..3ae3e17ae67
--- /dev/null
+++ b/invokeai/app/invocations/util.py
@@ -0,0 +1,14 @@
+from typing import Union
+
+
+def validate_weights(weights: Union[float, list[float]]) -> None:
+ """Validate that all control weights in the valid range"""
+ to_validate = weights if isinstance(weights, list) else [weights]
+ if any(i < -1 or i > 2 for i in to_validate):
+ raise ValueError("Control weights must be within -1 to 2 range")
+
+
+def validate_begin_end_step(begin_step_percent: float, end_step_percent: float) -> None:
+ """Validate that begin_step_percent is less than or equal to end_step_percent"""
+ if begin_step_percent > end_step_percent:
+ raise ValueError("Begin step percent must be less than or equal to end step percent")
diff --git a/invokeai/app/invocations/z_image_control.py b/invokeai/app/invocations/z_image_control.py
new file mode 100644
index 00000000000..f51c2fcd168
--- /dev/null
+++ b/invokeai/app/invocations/z_image_control.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2024, Lincoln D. Stein and the InvokeAI Development Team
+"""Z-Image Control invocation for spatial conditioning."""
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ InputField,
+ OutputField,
+)
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+class ZImageControlField(BaseModel):
+ """A Z-Image control conditioning field for spatial control (Canny, HED, Depth, Pose, MLSD)."""
+
+ image_name: str = Field(description="The name of the preprocessed control image")
+ control_model: ModelIdentifierField = Field(description="The Z-Image ControlNet adapter model")
+ control_context_scale: float = Field(
+ default=0.75,
+ ge=0.0,
+ le=2.0,
+ description="The strength of the control signal. Recommended range: 0.65-0.80.",
+ )
+ begin_step_percent: float = Field(
+ default=0.0,
+ ge=0.0,
+ le=1.0,
+ description="When the control is first applied (% of total steps)",
+ )
+ end_step_percent: float = Field(
+ default=1.0,
+ ge=0.0,
+ le=1.0,
+ description="When the control is last applied (% of total steps)",
+ )
+
+
+@invocation_output("z_image_control_output")
+class ZImageControlOutput(BaseInvocationOutput):
+ """Z-Image Control output containing control configuration."""
+
+ control: ZImageControlField = OutputField(description="Z-Image control conditioning")
+
+
+@invocation(
+ "z_image_control",
+ title="Z-Image ControlNet",
+ tags=["image", "z-image", "control", "controlnet"],
+ category="conditioning",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class ZImageControlInvocation(BaseInvocation):
+ """Configure Z-Image ControlNet for spatial conditioning.
+
+ Takes a preprocessed control image (e.g., Canny edges, depth map, pose)
+ and a Z-Image ControlNet adapter model to enable spatial control.
+
+ Supports 5 control modes: Canny, HED, Depth, Pose, MLSD.
+ Recommended control_context_scale: 0.65-0.80.
+ """
+
+ image: ImageField = InputField(
+ description="The preprocessed control image (Canny, HED, Depth, Pose, or MLSD)",
+ )
+ control_model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.controlnet_model,
+ title="Control Model",
+ ui_model_base=BaseModelType.ZImage,
+ ui_model_type=ModelType.ControlNet,
+ )
+ control_context_scale: float = InputField(
+ default=0.75,
+ ge=0.0,
+ le=2.0,
+ description="Strength of the control signal. Recommended range: 0.65-0.80.",
+ title="Control Scale",
+ )
+ begin_step_percent: float = InputField(
+ default=0.0,
+ ge=0.0,
+ le=1.0,
+ description="When the control is first applied (% of total steps)",
+ )
+ end_step_percent: float = InputField(
+ default=1.0,
+ ge=0.0,
+ le=1.0,
+ description="When the control is last applied (% of total steps)",
+ )
+
+ def invoke(self, context: InvocationContext) -> ZImageControlOutput:
+ return ZImageControlOutput(
+ control=ZImageControlField(
+ image_name=self.image.image_name,
+ control_model=self.control_model,
+ control_context_scale=self.control_context_scale,
+ begin_step_percent=self.begin_step_percent,
+ end_step_percent=self.end_step_percent,
+ )
+ )
diff --git a/invokeai/app/invocations/z_image_denoise.py b/invokeai/app/invocations/z_image_denoise.py
new file mode 100644
index 00000000000..c1e864ea179
--- /dev/null
+++ b/invokeai/app/invocations/z_image_denoise.py
@@ -0,0 +1,812 @@
+import inspect
+import math
+from contextlib import ExitStack
+from typing import Callable, Iterator, Optional, Tuple
+
+import einops
+import torch
+import torchvision.transforms as tv_transforms
+from diffusers.schedulers.scheduling_utils import SchedulerMixin
+from PIL import Image
+from torchvision.transforms.functional import resize as tv_resize
+from tqdm import tqdm
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
+from invokeai.app.invocations.fields import (
+ DenoiseMaskField,
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ ZImageConditioningField,
+)
+from invokeai.app.invocations.latent_noise import validate_noise_tensor_shape
+from invokeai.app.invocations.model import TransformerField, VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.invocations.z_image_control import ZImageControlField
+from invokeai.app.invocations.z_image_image_to_latents import ZImageImageToLatentsInvocation
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.schedulers import ZIMAGE_SCHEDULER_LABELS, ZIMAGE_SCHEDULER_MAP, ZIMAGE_SCHEDULER_NAME_VALUES
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.z_image_lora_constants import Z_IMAGE_LORA_TRANSFORMER_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ZImageConditioningInfo
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.z_image.extensions.regional_prompting_extension import ZImageRegionalPromptingExtension
+from invokeai.backend.z_image.text_conditioning import ZImageTextConditioning
+from invokeai.backend.z_image.z_image_control_adapter import ZImageControlAdapter
+from invokeai.backend.z_image.z_image_controlnet_extension import (
+ ZImageControlNetExtension,
+ z_image_forward_with_control,
+)
+from invokeai.backend.z_image.z_image_transformer_patch import patch_transformer_for_regional_prompting
+
+
+@invocation(
+ "z_image_denoise",
+ title="Denoise - Z-Image",
+ tags=["image", "z-image"],
+ category="latents",
+ version="1.6.0",
+ classification=Classification.Prototype,
+)
+class ZImageDenoiseInvocation(BaseInvocation):
+ """Run the denoising process with a Z-Image model.
+
+ Supports regional prompting by connecting multiple conditioning inputs with masks.
+ """
+
+ # If latents is provided, this means we are doing image-to-image.
+ latents: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.latents, input=Input.Connection
+ )
+ noise: Optional[LatentsField] = InputField(
+ default=None, description=FieldDescriptions.noise, input=Input.Connection
+ )
+ # denoise_mask is used for image-to-image inpainting. Only the masked region is modified.
+ denoise_mask: Optional[DenoiseMaskField] = InputField(
+ default=None, description=FieldDescriptions.denoise_mask, input=Input.Connection
+ )
+ 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)
+ add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
+ transformer: TransformerField = InputField(
+ description=FieldDescriptions.z_image_model, input=Input.Connection, title="Transformer"
+ )
+ positive_conditioning: ZImageConditioningField | list[ZImageConditioningField] = InputField(
+ description=FieldDescriptions.positive_cond, input=Input.Connection
+ )
+ negative_conditioning: ZImageConditioningField | list[ZImageConditioningField] | None = InputField(
+ default=None, description=FieldDescriptions.negative_cond, input=Input.Connection
+ )
+ # Z-Image-Turbo works best without CFG (guidance_scale=1.0)
+ guidance_scale: float = InputField(
+ default=1.0,
+ ge=1.0,
+ description="Guidance scale for classifier-free guidance. 1.0 = no CFG (recommended for Z-Image-Turbo). "
+ "Values > 1.0 amplify guidance.",
+ title="Guidance Scale",
+ )
+ width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
+ height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
+ # Z-Image-Turbo uses 8 steps by default
+ steps: int = InputField(default=8, gt=0, description="Number of denoising steps. 8 recommended for Z-Image-Turbo.")
+ seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
+ # Z-Image Control support
+ control: Optional[ZImageControlField] = InputField(
+ default=None,
+ description="Z-Image control conditioning for spatial control (Canny, HED, Depth, Pose, MLSD).",
+ input=Input.Connection,
+ )
+ # VAE for encoding control images (required when using control)
+ vae: Optional[VAEField] = InputField(
+ default=None,
+ description=FieldDescriptions.vae + " Required for control conditioning.",
+ input=Input.Connection,
+ )
+ # Shift override for the sigma schedule. If None, shift is auto-calculated from image dimensions.
+ shift: Optional[float] = InputField(
+ default=None,
+ ge=0.0,
+ description="Override the timestep shift (mu) for the sigma schedule. "
+ "Leave blank to auto-calculate based on image dimensions (recommended). "
+ "Lower values (~0.5) produce less noise shifting, higher values (~1.15) produce more.",
+ title="Shift",
+ )
+ # Scheduler selection for the denoising process
+ scheduler: ZIMAGE_SCHEDULER_NAME_VALUES = InputField(
+ default="euler",
+ description="Scheduler (sampler) for the denoising process. Euler is the default and recommended. "
+ "Heun is 2nd-order (better quality, 2x slower). LCM works with Turbo only (not Base).",
+ ui_choice_labels=ZIMAGE_SCHEDULER_LABELS,
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ latents = self._run_diffusion(context)
+ latents = latents.detach().to("cpu")
+
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
+
+ def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> torch.Tensor | None:
+ """Prepare the inpaint mask."""
+ if self.denoise_mask is None:
+ return None
+ mask = context.tensors.load(self.denoise_mask.mask_name)
+
+ # Invert mask: 0.0 = regions to denoise, 1.0 = regions to preserve
+ mask = 1.0 - mask
+
+ _, _, latent_height, latent_width = latents.shape
+ mask = tv_resize(
+ img=mask,
+ size=[latent_height, latent_width],
+ interpolation=tv_transforms.InterpolationMode.BILINEAR,
+ antialias=False,
+ )
+
+ mask = mask.to(device=latents.device, dtype=latents.dtype)
+ return mask
+
+ def _load_text_conditioning(
+ self,
+ context: InvocationContext,
+ cond_field: ZImageConditioningField | list[ZImageConditioningField],
+ img_height: int,
+ img_width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> list[ZImageTextConditioning]:
+ """Load Z-Image text conditioning with optional regional masks.
+
+ Args:
+ context: The invocation context.
+ cond_field: Single conditioning field or list of fields.
+ img_height: Height of the image token grid (H // patch_size).
+ img_width: Width of the image token grid (W // patch_size).
+ dtype: Target dtype.
+ device: Target device.
+
+ Returns:
+ List of ZImageTextConditioning objects with embeddings and masks.
+ """
+ # Normalize to a list
+ cond_list = [cond_field] if isinstance(cond_field, ZImageConditioningField) else cond_field
+
+ text_conditionings: list[ZImageTextConditioning] = []
+ for cond in cond_list:
+ # Load the text embeddings
+ cond_data = context.conditioning.load(cond.conditioning_name)
+ assert len(cond_data.conditionings) == 1
+ z_image_conditioning = cond_data.conditionings[0]
+ assert isinstance(z_image_conditioning, ZImageConditioningInfo)
+ z_image_conditioning = z_image_conditioning.to(dtype=dtype, device=device)
+ prompt_embeds = z_image_conditioning.prompt_embeds
+
+ # Load the mask, if provided
+ mask: torch.Tensor | None = None
+ if cond.mask is not None:
+ mask = context.tensors.load(cond.mask.tensor_name)
+ mask = mask.to(device=device)
+ mask = ZImageRegionalPromptingExtension.preprocess_regional_prompt_mask(
+ mask, img_height, img_width, dtype, device
+ )
+
+ text_conditionings.append(ZImageTextConditioning(prompt_embeds=prompt_embeds, mask=mask))
+
+ return text_conditionings
+
+ def _get_noise(
+ self,
+ batch_size: int,
+ num_channels_latents: int,
+ height: int,
+ width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ seed: int,
+ ) -> torch.Tensor:
+ """Generate initial noise tensor."""
+ # Generate noise as float32 on CPU for maximum compatibility,
+ # then cast to target dtype/device
+ rand_device = "cpu"
+ rand_dtype = torch.float32
+
+ return torch.randn(
+ batch_size,
+ num_channels_latents,
+ int(height) // LATENT_SCALE_FACTOR,
+ int(width) // LATENT_SCALE_FACTOR,
+ device=rand_device,
+ dtype=rand_dtype,
+ generator=torch.Generator(device=rand_device).manual_seed(seed),
+ ).to(device=device, dtype=dtype)
+
+ def _calculate_shift(
+ self,
+ image_seq_len: int,
+ base_image_seq_len: int = 256,
+ max_image_seq_len: int = 4096,
+ base_shift: float = 0.5,
+ max_shift: float = 1.15,
+ ) -> float:
+ """Calculate timestep shift based on image sequence length.
+
+ Based on diffusers ZImagePipeline.calculate_shift method.
+ Returns a linear shift value (exp(mu) from the original formula).
+ """
+ import math
+
+ m = (max_shift - base_shift) / (max_image_seq_len - base_image_seq_len)
+ b = base_shift - m * base_image_seq_len
+ mu = image_seq_len * m + b
+ # Convert from exponential mu to linear shift value
+ return math.exp(mu)
+
+ def _get_sigmas(self, shift: float, num_steps: int) -> list[float]:
+ """Generate sigma schedule with linear time shift.
+
+ Uses linear time shift: shift / (shift + (1/t - 1)).
+ The shift value is used directly as a multiplier.
+ Generates num_steps + 1 sigma values (including terminal 0.0).
+ """
+
+ def time_shift(shift: float, t: float) -> float:
+ """Apply linear time shift to a single timestep value."""
+ if t <= 0:
+ return 0.0
+ if t >= 1:
+ return 1.0
+ return shift / (shift + (1 / t - 1))
+
+ sigmas = []
+ for i in range(num_steps + 1):
+ t = 1.0 - i / num_steps # Goes from 1.0 to 0.0
+ sigma = time_shift(shift, t)
+ sigmas.append(sigma)
+
+ return sigmas
+
+ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
+ device = TorchDevice.choose_torch_device()
+ inference_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+
+ transformer_info = context.models.load(self.transformer.transformer)
+
+ # Calculate image token grid dimensions
+ patch_size = 2 # Z-Image uses patch_size=2
+ latent_height = self.height // LATENT_SCALE_FACTOR
+ latent_width = self.width // LATENT_SCALE_FACTOR
+ img_token_height = latent_height // patch_size
+ img_token_width = latent_width // patch_size
+ img_seq_len = img_token_height * img_token_width
+
+ # Load positive conditioning with regional masks
+ pos_text_conditionings = self._load_text_conditioning(
+ context=context,
+ cond_field=self.positive_conditioning,
+ img_height=img_token_height,
+ img_width=img_token_width,
+ dtype=inference_dtype,
+ device=device,
+ )
+
+ # Create regional prompting extension
+ regional_extension = ZImageRegionalPromptingExtension.from_text_conditionings(
+ text_conditionings=pos_text_conditionings,
+ img_seq_len=img_seq_len,
+ )
+
+ # Get the concatenated prompt embeddings for the transformer
+ pos_prompt_embeds = regional_extension.regional_text_conditioning.prompt_embeds
+
+ # Load negative conditioning if provided and guidance_scale != 1.0
+ # CFG formula: pred = pred_uncond + cfg_scale * (pred_cond - pred_uncond)
+ # At cfg_scale=1.0: pred = pred_cond (no effect, skip uncond computation)
+ # This matches FLUX's convention where 1.0 means "no CFG"
+ neg_prompt_embeds: torch.Tensor | None = None
+ do_classifier_free_guidance = (
+ not math.isclose(self.guidance_scale, 1.0) and self.negative_conditioning is not None
+ )
+ if do_classifier_free_guidance:
+ assert self.negative_conditioning is not None
+ # Load all negative conditionings and concatenate embeddings
+ # Note: We ignore masks for negative conditioning as regional negative prompting is not fully supported
+ neg_text_conditionings = self._load_text_conditioning(
+ context=context,
+ cond_field=self.negative_conditioning,
+ img_height=img_token_height,
+ img_width=img_token_width,
+ dtype=inference_dtype,
+ device=device,
+ )
+ # Concatenate all negative embeddings
+ neg_prompt_embeds = torch.cat([tc.prompt_embeds for tc in neg_text_conditionings], dim=0)
+
+ # Calculate shift based on image sequence length, or use override
+ if self.shift is not None:
+ shift = self.shift
+ else:
+ shift = self._calculate_shift(img_seq_len)
+
+ # Generate sigma schedule with time shift
+ sigmas = self._get_sigmas(shift, self.steps)
+
+ # Apply denoising_start and denoising_end clipping
+ if self.denoising_start > 0 or self.denoising_end < 1:
+ # Calculate start and end indices based on denoising range
+ total_sigmas = len(sigmas)
+ start_idx = int(self.denoising_start * (total_sigmas - 1))
+ end_idx = int(self.denoising_end * (total_sigmas - 1)) + 1
+ sigmas = sigmas[start_idx:end_idx]
+
+ total_steps = len(sigmas) - 1
+
+ # Load input latents if provided (image-to-image)
+ init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
+ if init_latents is not None:
+ init_latents = init_latents.to(device=device, dtype=inference_dtype)
+
+ # Generate initial noise.
+ # If noise will never be consumed, avoid validating/loading it.
+ should_ignore_noise = init_latents is not None and not self.add_noise and self.denoise_mask is None
+ noise: torch.Tensor | None
+ if should_ignore_noise:
+ noise = None
+ else:
+ noise = self._prepare_noise_tensor(context, inference_dtype, device)
+
+ # Prepare input latent image
+ if init_latents is not None:
+ if self.add_noise:
+ assert noise is not None
+ # Noise the init latents using the first sigma from the clipped
+ # InvokeAI schedule.
+ #
+ # Known limitation: if the selected scheduler later starts from a
+ # different first effective sigma/timestep than sigmas[0], the
+ # img2img preblend below may not match that scheduler exactly.
+ # This is an existing pipeline limitation and affects both
+ # internally generated noise and externally supplied noise.
+ s_0 = sigmas[0]
+ latents = s_0 * noise + (1.0 - s_0) * init_latents
+ else:
+ latents = init_latents
+ else:
+ if self.denoising_start > 1e-5:
+ raise ValueError("denoising_start should be 0 when initial latents are not provided.")
+ assert noise is not None
+ latents = noise
+
+ # Short-circuit if no denoising steps
+ if total_steps <= 0:
+ return latents
+
+ # Prepare inpaint extension
+ inpaint_mask = self._prep_inpaint_mask(context, latents)
+ inpaint_extension: RectifiedFlowInpaintExtension | None = None
+ if inpaint_mask is not None:
+ if init_latents is None:
+ raise ValueError("Initial latents are required when using an inpaint mask (image-to-image inpainting)")
+ assert noise is not None
+ inpaint_extension = RectifiedFlowInpaintExtension(
+ init_latents=init_latents,
+ inpaint_mask=inpaint_mask,
+ noise=noise,
+ )
+
+ step_callback = self._build_step_callback(context)
+
+ # Initialize the diffusers scheduler if not using built-in Euler
+ scheduler: SchedulerMixin | None = None
+ use_scheduler = self.scheduler != "euler"
+
+ if use_scheduler:
+ scheduler_class = ZIMAGE_SCHEDULER_MAP[self.scheduler]
+ scheduler = scheduler_class(
+ num_train_timesteps=1000,
+ shift=1.0,
+ )
+ # Set timesteps - LCM uses its own sigma schedule (num_inference_steps),
+ # while other schedulers can use custom sigmas if supported
+ is_lcm = self.scheduler == "lcm"
+ set_timesteps_sig = inspect.signature(scheduler.set_timesteps)
+ if not is_lcm and "sigmas" in set_timesteps_sig.parameters:
+ scheduler.set_timesteps(sigmas=sigmas, device=device)
+ else:
+ # LCM or a scheduler without custom-sigma support computes its own
+ # schedule from num_inference_steps. That can diverge from sigmas[0]
+ # used in the img2img preblend above.
+ scheduler.set_timesteps(num_inference_steps=total_steps, device=device)
+
+ # For Heun scheduler, the number of actual steps may differ
+ num_scheduler_steps = len(scheduler.timesteps)
+ else:
+ num_scheduler_steps = total_steps
+
+ with ExitStack() as exit_stack:
+ # Get transformer config to determine if it's quantized
+ transformer_config = context.models.get_config(self.transformer.transformer)
+
+ # Determine if the model is quantized.
+ # If the model is quantized, then we need to apply the LoRA weights as sidecar layers. This results in
+ # slower inference than direct patching, but is agnostic to the quantization format.
+ if transformer_config.format in [ModelFormat.Diffusers, ModelFormat.Checkpoint]:
+ model_is_quantized = False
+ elif transformer_config.format in [ModelFormat.GGUFQuantized]:
+ model_is_quantized = True
+ else:
+ raise ValueError(f"Unsupported Z-Image model format: {transformer_config.format}")
+
+ # Load transformer - always use base transformer, control is handled via extension
+ (cached_weights, transformer) = exit_stack.enter_context(transformer_info.model_on_device())
+
+ # Prepare control extension if control is provided
+ control_extension: ZImageControlNetExtension | None = None
+
+ if self.control is not None:
+ # Load control adapter using context manager (proper GPU memory management)
+ control_model_info = context.models.load(self.control.control_model)
+ (_, control_adapter) = exit_stack.enter_context(control_model_info.model_on_device())
+ assert isinstance(control_adapter, ZImageControlAdapter)
+
+ # Get control_in_dim from adapter config (16 for V1, 33 for V2.0)
+ adapter_config = control_adapter.config
+ control_in_dim = adapter_config.get("control_in_dim", 16)
+ num_control_blocks = adapter_config.get("num_control_blocks", 6)
+
+ # Log control configuration for debugging
+ version = "V2.0" if control_in_dim > 16 else "V1"
+ context.util.signal_progress(
+ f"Using Z-Image ControlNet {version} (Extension): control_in_dim={control_in_dim}, "
+ f"num_blocks={num_control_blocks}, scale={self.control.control_context_scale}"
+ )
+
+ # Load and prepare control image - must be VAE-encoded!
+ if self.vae is None:
+ raise ValueError("VAE is required when using Z-Image Control. Connect a VAE to the 'vae' input.")
+
+ control_image = context.images.get_pil(self.control.image_name)
+
+ # Resize control image to match output dimensions
+ control_image = control_image.convert("RGB")
+ control_image = control_image.resize((self.width, self.height), Image.Resampling.LANCZOS)
+
+ # Convert to tensor format for VAE encoding
+ from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+
+ control_image_tensor = image_resized_to_grid_as_tensor(control_image)
+ if control_image_tensor.dim() == 3:
+ control_image_tensor = einops.rearrange(control_image_tensor, "c h w -> 1 c h w")
+
+ # Encode control image through VAE to get latents
+ vae_info = context.models.load(self.vae.vae)
+ control_latents = ZImageImageToLatentsInvocation.vae_encode(
+ vae_info=vae_info,
+ image_tensor=control_image_tensor,
+ )
+
+ # Move to inference device/dtype
+ control_latents = control_latents.to(device=device, dtype=inference_dtype)
+
+ # Add frame dimension: [B, C, H, W] -> [C, 1, H, W] (single image)
+ control_latents = control_latents.squeeze(0).unsqueeze(1)
+
+ # Prepare control_cond based on control_in_dim
+ # V1: 16 channels (just control latents)
+ # V2.0: 33 channels = 16 control + 16 reference + 1 mask
+ # - Channels 0-15: control image latents (from VAE encoding)
+ # - Channels 16-31: reference/inpaint image latents (zeros for pure control)
+ # - Channel 32: inpaint mask (1.0 = don't inpaint, 0.0 = inpaint region)
+ # For pure control (no inpainting), we set mask=1 to tell model "use control, don't inpaint"
+ c, f, h, w = control_latents.shape
+ if c < control_in_dim:
+ padding_channels = control_in_dim - c
+ if padding_channels == 17:
+ # V2.0: 16 reference channels (zeros) + 1 mask channel (ones)
+ ref_padding = torch.zeros(
+ (16, f, h, w),
+ device=device,
+ dtype=inference_dtype,
+ )
+ # Mask channel = 1.0 means "don't inpaint this region, use control signal"
+ mask_channel = torch.ones(
+ (1, f, h, w),
+ device=device,
+ dtype=inference_dtype,
+ )
+ control_latents = torch.cat([control_latents, ref_padding, mask_channel], dim=0)
+ else:
+ # Generic padding with zeros for other cases
+ zero_padding = torch.zeros(
+ (padding_channels, f, h, w),
+ device=device,
+ dtype=inference_dtype,
+ )
+ control_latents = torch.cat([control_latents, zero_padding], dim=0)
+
+ # Create control extension (adapter is already on device from model_on_device)
+ control_extension = ZImageControlNetExtension(
+ control_adapter=control_adapter,
+ control_cond=control_latents,
+ weight=self.control.control_context_scale,
+ begin_step_percent=self.control.begin_step_percent,
+ end_step_percent=self.control.end_step_percent,
+ )
+
+ # Apply LoRA models to the transformer.
+ # Note: We apply the LoRA after the transformer has been moved to its target device for faster patching.
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=transformer,
+ patches=self._lora_iterator(context),
+ prefix=Z_IMAGE_LORA_TRANSFORMER_PREFIX,
+ dtype=inference_dtype,
+ cached_weights=cached_weights,
+ force_sidecar_patching=model_is_quantized,
+ )
+ )
+
+ # Apply regional prompting patch if we have regional masks
+ exit_stack.enter_context(
+ patch_transformer_for_regional_prompting(
+ transformer=transformer,
+ regional_attn_mask=regional_extension.regional_attn_mask,
+ img_seq_len=img_seq_len,
+ )
+ )
+
+ # Denoising loop - supports both built-in Euler and diffusers schedulers
+ # Track user-facing step for progress (accounts for Heun's double steps)
+ user_step = 0
+
+ if use_scheduler and scheduler is not None:
+ # Use diffusers scheduler for stepping
+ # Use tqdm with total_steps (user-facing steps) not num_scheduler_steps (internal steps)
+ # This ensures progress bar shows 1/8, 2/8, etc. even when scheduler uses more internal steps
+ pbar = tqdm(total=total_steps, desc="Denoising")
+ for step_index in range(num_scheduler_steps):
+ sched_timestep = scheduler.timesteps[step_index]
+ # Convert scheduler timestep (0-1000) to normalized sigma (0-1)
+ sigma_curr = sched_timestep.item() / scheduler.config.num_train_timesteps
+
+ # For Heun scheduler, track if we're in first or second order step
+ is_heun = hasattr(scheduler, "state_in_first_order")
+ in_first_order = scheduler.state_in_first_order if is_heun else True
+
+ # Timestep tensor for Z-Image model
+ # The model expects t=0 at start (noise) and t=1 at end (clean)
+ model_t = 1.0 - sigma_curr
+ timestep = torch.tensor([model_t], device=device, dtype=inference_dtype).expand(latents.shape[0])
+
+ # Run transformer for positive prediction
+ latent_model_input = latents.to(transformer.dtype)
+ latent_model_input = latent_model_input.unsqueeze(2) # Add frame dimension
+ latent_model_input_list = list(latent_model_input.unbind(dim=0))
+
+ # Determine if control should be applied at this step
+ apply_control = control_extension is not None and control_extension.should_apply(
+ user_step, total_steps
+ )
+
+ # Run forward pass
+ if apply_control:
+ model_out_list, _ = z_image_forward_with_control(
+ transformer=transformer,
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[pos_prompt_embeds],
+ control_extension=control_extension,
+ )
+ else:
+ model_output = transformer(
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[pos_prompt_embeds],
+ )
+ model_out_list = model_output[0]
+
+ noise_pred_cond = torch.stack([t.float() for t in model_out_list], dim=0)
+ noise_pred_cond = noise_pred_cond.squeeze(2)
+ noise_pred_cond = -noise_pred_cond # Z-Image uses v-prediction with negation
+
+ # Apply CFG if enabled
+ if do_classifier_free_guidance and neg_prompt_embeds is not None:
+ if apply_control:
+ model_out_list_uncond, _ = z_image_forward_with_control(
+ transformer=transformer,
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[neg_prompt_embeds],
+ control_extension=control_extension,
+ )
+ else:
+ model_output_uncond = transformer(
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[neg_prompt_embeds],
+ )
+ model_out_list_uncond = model_output_uncond[0]
+
+ noise_pred_uncond = torch.stack([t.float() for t in model_out_list_uncond], dim=0)
+ noise_pred_uncond = noise_pred_uncond.squeeze(2)
+ noise_pred_uncond = -noise_pred_uncond
+ noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ # Use scheduler.step() for the update
+ step_output = scheduler.step(model_output=noise_pred, timestep=sched_timestep, sample=latents)
+ latents = step_output.prev_sample
+
+ # Get sigma_prev for inpainting (next sigma value)
+ if step_index + 1 < len(scheduler.sigmas):
+ sigma_prev = scheduler.sigmas[step_index + 1].item()
+ else:
+ sigma_prev = 0.0
+
+ if inpaint_extension is not None:
+ latents = inpaint_extension.merge_intermediate_latents_with_init_latents(latents, sigma_prev)
+
+ # For Heun, only increment user step after second-order step completes
+ if is_heun:
+ if not in_first_order:
+ user_step += 1
+ # Only call step_callback if we haven't exceeded total_steps
+ if user_step <= total_steps:
+ pbar.update(1)
+ step_callback(
+ PipelineIntermediateState(
+ step=user_step,
+ order=2,
+ total_steps=total_steps,
+ timestep=int(sigma_curr * 1000),
+ latents=latents,
+ ),
+ )
+ else:
+ # For first-order schedulers (Euler, LCM)
+ user_step += 1
+ if user_step <= total_steps:
+ pbar.update(1)
+ step_callback(
+ PipelineIntermediateState(
+ step=user_step,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(sigma_curr * 1000),
+ latents=latents,
+ ),
+ )
+ pbar.close()
+ else:
+ # Original Euler implementation (default, optimized for Z-Image)
+ for step_idx in tqdm(range(total_steps)):
+ sigma_curr = sigmas[step_idx]
+ sigma_prev = sigmas[step_idx + 1]
+
+ # Timestep tensor for Z-Image model
+ # The model expects t=0 at start (noise) and t=1 at end (clean)
+ # Sigma goes from 1 (noise) to 0 (clean), so model_t = 1 - sigma
+ model_t = 1.0 - sigma_curr
+ timestep = torch.tensor([model_t], device=device, dtype=inference_dtype).expand(latents.shape[0])
+
+ # Run transformer for positive prediction
+ # Z-Image transformer expects: x as list of [C, 1, H, W] tensors, t, cap_feats as list
+ # Prepare latent input: [B, C, H, W] -> [B, C, 1, H, W] -> list of [C, 1, H, W]
+ latent_model_input = latents.to(transformer.dtype)
+ latent_model_input = latent_model_input.unsqueeze(2) # Add frame dimension
+ latent_model_input_list = list(latent_model_input.unbind(dim=0))
+
+ # Determine if control should be applied at this step
+ apply_control = control_extension is not None and control_extension.should_apply(
+ step_idx, total_steps
+ )
+
+ # Run forward pass - use custom forward with control if extension is active
+ if apply_control:
+ model_out_list, _ = z_image_forward_with_control(
+ transformer=transformer,
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[pos_prompt_embeds],
+ control_extension=control_extension,
+ )
+ else:
+ model_output = transformer(
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[pos_prompt_embeds],
+ )
+ model_out_list = model_output[0] # Extract list of tensors from tuple
+
+ noise_pred_cond = torch.stack([t.float() for t in model_out_list], dim=0)
+ noise_pred_cond = noise_pred_cond.squeeze(2) # Remove frame dimension
+ noise_pred_cond = -noise_pred_cond # Z-Image uses v-prediction with negation
+
+ # Apply CFG if enabled
+ if do_classifier_free_guidance and neg_prompt_embeds is not None:
+ if apply_control:
+ model_out_list_uncond, _ = z_image_forward_with_control(
+ transformer=transformer,
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[neg_prompt_embeds],
+ control_extension=control_extension,
+ )
+ else:
+ model_output_uncond = transformer(
+ x=latent_model_input_list,
+ t=timestep,
+ cap_feats=[neg_prompt_embeds],
+ )
+ model_out_list_uncond = model_output_uncond[0] # Extract list of tensors from tuple
+
+ noise_pred_uncond = torch.stack([t.float() for t in model_out_list_uncond], dim=0)
+ noise_pred_uncond = noise_pred_uncond.squeeze(2)
+ noise_pred_uncond = -noise_pred_uncond
+ noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_cond - noise_pred_uncond)
+ else:
+ noise_pred = noise_pred_cond
+
+ # Euler step
+ latents_dtype = latents.dtype
+ latents = latents.to(dtype=torch.float32)
+ latents = latents + (sigma_prev - sigma_curr) * noise_pred
+ latents = latents.to(dtype=latents_dtype)
+
+ if inpaint_extension is not None:
+ latents = inpaint_extension.merge_intermediate_latents_with_init_latents(latents, sigma_prev)
+
+ step_callback(
+ PipelineIntermediateState(
+ step=step_idx + 1,
+ order=1,
+ total_steps=total_steps,
+ timestep=int(sigma_curr * 1000),
+ latents=latents,
+ ),
+ )
+
+ return latents
+
+ def _prepare_noise_tensor(
+ self, context: InvocationContext, inference_dtype: torch.dtype, device: torch.device
+ ) -> torch.Tensor:
+ if self.noise is not None:
+ noise = context.tensors.load(self.noise.latents_name).to(device=device, dtype=inference_dtype)
+ validate_noise_tensor_shape(noise, "Z-Image", self.width, self.height)
+ return noise
+
+ return self._get_noise(
+ batch_size=1,
+ num_channels_latents=16,
+ height=self.height,
+ width=self.width,
+ dtype=inference_dtype,
+ device=device,
+ seed=self.seed,
+ )
+
+ def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
+ def step_callback(state: PipelineIntermediateState) -> None:
+ context.util.sd_step_callback(state, BaseModelType.ZImage)
+
+ return step_callback
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the transformer."""
+ for lora in self.transformer.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
+ "The LoRA model may be corrupted or incompatible."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/invocations/z_image_image_to_latents.py b/invokeai/app/invocations/z_image_image_to_latents.py
new file mode 100644
index 00000000000..263346e2962
--- /dev/null
+++ b/invokeai/app/invocations/z_image_image_to_latents.py
@@ -0,0 +1,110 @@
+from typing import Union
+
+import einops
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ ImageField,
+ Input,
+ InputField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import LatentsOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder as FluxAutoEncoder
+from invokeai.backend.model_manager.load.load_base import LoadedModel
+from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+# Z-Image can use either the Diffusers AutoencoderKL or the FLUX AutoEncoder
+ZImageVAE = Union[AutoencoderKL, FluxAutoEncoder]
+
+
+@invocation(
+ "z_image_i2l",
+ title="Image to Latents - Z-Image",
+ tags=["image", "latents", "vae", "i2l", "z-image"],
+ category="latents",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class ZImageImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates latents from an image using Z-Image VAE (supports both Diffusers and FLUX VAE)."""
+
+ image: ImageField = InputField(description="The image to encode.")
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @staticmethod
+ def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
+ if not isinstance(vae_info.model, (AutoencoderKL, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKL or FluxAutoEncoder for Z-Image VAE, got {type(vae_info.model).__name__}. "
+ "Ensure you are using a compatible VAE model."
+ )
+
+ # Estimate working memory needed for VAE encode
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="encode",
+ image_tensor=image_tensor,
+ vae=vae_info.model,
+ )
+
+ with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ if not isinstance(vae, (AutoencoderKL, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKL or FluxAutoEncoder, got {type(vae).__name__}. "
+ "VAE model type changed unexpectedly after loading."
+ )
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+
+ with torch.inference_mode():
+ if isinstance(vae, FluxAutoEncoder):
+ # FLUX VAE handles scaling internally
+ generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
+ latents = vae.encode(image_tensor, sample=True, generator=generator)
+ else:
+ # AutoencoderKL - needs manual scaling
+ vae.disable_tiling()
+ image_tensor_dist = vae.encode(image_tensor).latent_dist
+ latents: torch.Tensor = image_tensor_dist.sample().to(dtype=vae.dtype)
+
+ # Apply scaling_factor and shift_factor from VAE config
+ # Z-Image uses: latents = (latents - shift_factor) * scaling_factor
+ scaling_factor = vae.config.scaling_factor
+ shift_factor = getattr(vae.config, "shift_factor", None)
+
+ if shift_factor is not None:
+ latents = latents - shift_factor
+ latents = latents * scaling_factor
+
+ return latents
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> LatentsOutput:
+ image = context.images.get_pil(self.image.image_name)
+
+ image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
+ if image_tensor.dim() == 3:
+ image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, (AutoencoderKL, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKL or FluxAutoEncoder for Z-Image VAE, got {type(vae_info.model).__name__}. "
+ "Ensure you are using a compatible VAE model."
+ )
+
+ context.util.signal_progress("Running VAE")
+ latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
+
+ latents = latents.to("cpu")
+ name = context.tensors.save(tensor=latents)
+ return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
diff --git a/invokeai/app/invocations/z_image_latents_to_image.py b/invokeai/app/invocations/z_image_latents_to_image.py
new file mode 100644
index 00000000000..a2e6fdcc077
--- /dev/null
+++ b/invokeai/app/invocations/z_image_latents_to_image.py
@@ -0,0 +1,111 @@
+from contextlib import nullcontext
+from typing import Union
+
+import torch
+from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
+from einops import rearrange
+from PIL import Image
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ LatentsField,
+ WithBoard,
+ WithMetadata,
+)
+from invokeai.app.invocations.model import VAEField
+from invokeai.app.invocations.primitives import ImageOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.flux.modules.autoencoder import AutoEncoder as FluxAutoEncoder
+from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
+
+# Z-Image can use either the Diffusers AutoencoderKL or the FLUX AutoEncoder
+ZImageVAE = Union[AutoencoderKL, FluxAutoEncoder]
+
+
+@invocation(
+ "z_image_l2i",
+ title="Latents to Image - Z-Image",
+ tags=["latents", "image", "vae", "l2i", "z-image"],
+ category="latents",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class ZImageLatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
+ """Generates an image from latents using Z-Image VAE (supports both Diffusers and FLUX VAE)."""
+
+ latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
+ vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ImageOutput:
+ latents = context.tensors.load(self.latents.latents_name)
+
+ vae_info = context.models.load(self.vae.vae)
+ if not isinstance(vae_info.model, (AutoencoderKL, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKL or FluxAutoEncoder for Z-Image VAE, got {type(vae_info.model).__name__}. "
+ "Ensure you are using a compatible VAE model."
+ )
+
+ is_flux_vae = isinstance(vae_info.model, FluxAutoEncoder)
+
+ # Estimate working memory needed for VAE decode
+ estimated_working_memory = estimate_vae_working_memory_flux(
+ operation="decode",
+ image_tensor=latents,
+ vae=vae_info.model,
+ )
+
+ # FLUX VAE doesn't support seamless, so only apply for AutoencoderKL
+ seamless_context = (
+ nullcontext() if is_flux_vae else SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes)
+ )
+
+ with seamless_context, vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
+ context.util.signal_progress("Running VAE")
+ if not isinstance(vae, (AutoencoderKL, FluxAutoEncoder)):
+ raise TypeError(
+ f"Expected AutoencoderKL or FluxAutoEncoder, got {type(vae).__name__}. "
+ "VAE model type changed unexpectedly after loading."
+ )
+
+ vae_dtype = next(iter(vae.parameters())).dtype
+ latents = latents.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)
+
+ # Disable tiling for AutoencoderKL
+ if isinstance(vae, AutoencoderKL):
+ vae.disable_tiling()
+
+ # Clear memory as VAE decode can request a lot
+ TorchDevice.empty_cache()
+
+ with torch.inference_mode():
+ if isinstance(vae, FluxAutoEncoder):
+ # FLUX VAE handles scaling internally
+ img = vae.decode(latents)
+ else:
+ # AutoencoderKL - Apply scaling_factor and shift_factor from VAE config
+ # Z-Image uses: latents = latents / scaling_factor + shift_factor
+ scaling_factor = vae.config.scaling_factor
+ shift_factor = getattr(vae.config, "shift_factor", None)
+
+ latents = latents / scaling_factor
+ if shift_factor is not None:
+ latents = latents + shift_factor
+
+ img = vae.decode(latents, return_dict=False)[0]
+
+ img = img.clamp(-1, 1)
+ img = rearrange(img[0], "c h w -> h w c")
+ img_pil = Image.fromarray((127.5 * (img + 1.0)).byte().cpu().numpy())
+
+ TorchDevice.empty_cache()
+
+ image_dto = context.images.save(image=img_pil)
+
+ return ImageOutput.build(image_dto)
diff --git a/invokeai/app/invocations/z_image_lora_loader.py b/invokeai/app/invocations/z_image_lora_loader.py
new file mode 100644
index 00000000000..54c353a6ab7
--- /dev/null
+++ b/invokeai/app/invocations/z_image_lora_loader.py
@@ -0,0 +1,177 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, Qwen3EncoderField, TransformerField
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+
+@invocation_output("z_image_lora_loader_output")
+class ZImageLoRALoaderOutput(BaseInvocationOutput):
+ """Z-Image LoRA Loader Output"""
+
+ transformer: Optional[TransformerField] = OutputField(
+ default=None, description=FieldDescriptions.transformer, title="Z-Image Transformer"
+ )
+ qwen3_encoder: Optional[Qwen3EncoderField] = OutputField(
+ default=None, description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder"
+ )
+
+
+@invocation(
+ "z_image_lora_loader",
+ title="Apply LoRA - Z-Image",
+ tags=["lora", "model", "z-image"],
+ category="model",
+ version="1.0.0",
+)
+class ZImageLoRALoaderInvocation(BaseInvocation):
+ """Apply a LoRA model to a Z-Image transformer and/or Qwen3 text encoder."""
+
+ lora: ModelIdentifierField = InputField(
+ description=FieldDescriptions.lora_model,
+ title="LoRA",
+ ui_model_base=BaseModelType.ZImage,
+ ui_model_type=ModelType.LoRA,
+ )
+ weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
+ transformer: TransformerField | None = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Z-Image Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> ZImageLoRALoaderOutput:
+ lora_key = self.lora.key
+
+ if not context.models.exists(lora_key):
+ raise ValueError(f"Unknown lora: {lora_key}!")
+
+ # Check for existing LoRAs with the same key.
+ if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
+ if self.qwen3_encoder and any(lora.lora.key == lora_key for lora in self.qwen3_encoder.loras):
+ raise ValueError(f'LoRA "{lora_key}" already applied to Qwen3 encoder.')
+
+ # Warn on variant mismatch between LoRA and transformer.
+ lora_config = context.models.get_config(lora_key)
+ lora_variant = getattr(lora_config, "variant", None)
+ if lora_variant and self.transformer is not None:
+ transformer_config = context.models.get_config(self.transformer.transformer.key)
+ transformer_variant = getattr(transformer_config, "variant", None)
+ if transformer_variant and lora_variant != transformer_variant:
+ context.logger.warning(
+ f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
+ f"but transformer is {transformer_variant.value}. This may cause unexpected results."
+ )
+
+ output = ZImageLoRALoaderOutput()
+
+ # Attach LoRA layers to the models.
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+ output.transformer.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+ output.qwen3_encoder.loras.append(
+ LoRAField(
+ lora=self.lora,
+ weight=self.weight,
+ )
+ )
+
+ return output
+
+
+@invocation(
+ "z_image_lora_collection_loader",
+ title="Apply LoRA Collection - Z-Image",
+ tags=["lora", "model", "z-image"],
+ category="model",
+ version="1.0.0",
+)
+class ZImageLoRACollectionLoader(BaseInvocation):
+ """Applies a collection of LoRAs to a Z-Image transformer."""
+
+ loras: Optional[LoRAField | list[LoRAField]] = InputField(
+ default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
+ )
+
+ transformer: Optional[TransformerField] = InputField(
+ default=None,
+ description=FieldDescriptions.transformer,
+ input=Input.Connection,
+ title="Transformer",
+ )
+ qwen3_encoder: Qwen3EncoderField | None = InputField(
+ default=None,
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> ZImageLoRALoaderOutput:
+ output = ZImageLoRALoaderOutput()
+ loras = self.loras if isinstance(self.loras, list) else [self.loras]
+ added_loras: list[str] = []
+
+ if self.transformer is not None:
+ output.transformer = self.transformer.model_copy(deep=True)
+
+ if self.qwen3_encoder is not None:
+ output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
+
+ for lora in loras:
+ if lora is None:
+ continue
+ if lora.lora.key in added_loras:
+ continue
+
+ if not context.models.exists(lora.lora.key):
+ raise Exception(f"Unknown lora: {lora.lora.key}!")
+
+ if lora.lora.base is not BaseModelType.ZImage:
+ raise ValueError(
+ f"LoRA '{lora.lora.key}' is for {lora.lora.base.value if lora.lora.base else 'unknown'} models, "
+ "not Z-Image models. Ensure you are using a Z-Image compatible LoRA."
+ )
+
+ # Warn on variant mismatch between LoRA and transformer.
+ lora_config = context.models.get_config(lora.lora.key)
+ lora_variant = getattr(lora_config, "variant", None)
+ if lora_variant and self.transformer is not None:
+ transformer_config = context.models.get_config(self.transformer.transformer.key)
+ transformer_variant = getattr(transformer_config, "variant", None)
+ if transformer_variant and lora_variant != transformer_variant:
+ context.logger.warning(
+ f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
+ f"but transformer is {transformer_variant.value}. This may cause unexpected results."
+ )
+
+ added_loras.append(lora.lora.key)
+
+ if self.transformer is not None and output.transformer is not None:
+ output.transformer.loras.append(lora)
+
+ if self.qwen3_encoder is not None and output.qwen3_encoder is not None:
+ output.qwen3_encoder.loras.append(lora)
+
+ return output
diff --git a/invokeai/app/invocations/z_image_model_loader.py b/invokeai/app/invocations/z_image_model_loader.py
new file mode 100644
index 00000000000..4d746061dcc
--- /dev/null
+++ b/invokeai/app/invocations/z_image_model_loader.py
@@ -0,0 +1,135 @@
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ Classification,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
+from invokeai.app.invocations.model import (
+ ModelIdentifierField,
+ Qwen3EncoderField,
+ TransformerField,
+ VAEField,
+)
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, SubModelType
+
+
+@invocation_output("z_image_model_loader_output")
+class ZImageModelLoaderOutput(BaseInvocationOutput):
+ """Z-Image base model loader output."""
+
+ transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
+ qwen3_encoder: Qwen3EncoderField = OutputField(description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder")
+ vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
+
+
+@invocation(
+ "z_image_model_loader",
+ title="Main Model - Z-Image",
+ tags=["model", "z-image"],
+ category="model",
+ version="3.0.0",
+ classification=Classification.Prototype,
+)
+class ZImageModelLoaderInvocation(BaseInvocation):
+ """Loads a Z-Image model, outputting its submodels.
+
+ Similar to FLUX, you can mix and match components:
+ - Transformer: From Z-Image main model (GGUF quantized or Diffusers format)
+ - VAE: Separate FLUX VAE (shared with FLUX models) or from a Diffusers Z-Image model
+ - Qwen3 Encoder: Separate Qwen3Encoder model or from a Diffusers Z-Image model
+ """
+
+ model: ModelIdentifierField = InputField(
+ description=FieldDescriptions.z_image_model,
+ input=Input.Direct,
+ ui_model_base=BaseModelType.ZImage,
+ ui_model_type=ModelType.Main,
+ title="Transformer",
+ )
+
+ vae_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone VAE model. Z-Image uses the same VAE as FLUX (16-channel). "
+ "If not provided, VAE will be loaded from the Qwen3 Source model.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.Flux,
+ ui_model_type=ModelType.VAE,
+ title="VAE",
+ )
+
+ qwen3_encoder_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Standalone Qwen3 Encoder model. "
+ "If not provided, encoder will be loaded from the Qwen3 Source model.",
+ input=Input.Direct,
+ ui_model_type=ModelType.Qwen3Encoder,
+ title="Qwen3 Encoder",
+ )
+
+ qwen3_source_model: Optional[ModelIdentifierField] = InputField(
+ default=None,
+ description="Diffusers Z-Image model to extract VAE and/or Qwen3 encoder from. "
+ "Use this if you don't have separate VAE/Qwen3 models. "
+ "Ignored if both VAE and Qwen3 Encoder are provided separately.",
+ input=Input.Direct,
+ ui_model_base=BaseModelType.ZImage,
+ ui_model_type=ModelType.Main,
+ ui_model_format=ModelFormat.Diffusers,
+ title="Qwen3 Source (Diffusers)",
+ )
+
+ def invoke(self, context: InvocationContext) -> ZImageModelLoaderOutput:
+ # Transformer always comes from the main model
+ transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
+
+ # Determine VAE source
+ if self.vae_model is not None:
+ # Use standalone FLUX VAE
+ vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ elif self.qwen3_source_model is not None:
+ # Extract from Diffusers Z-Image model
+ self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
+ vae = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.VAE})
+ else:
+ raise ValueError(
+ "No VAE source provided. Either set 'VAE' to a FLUX VAE model, "
+ "or set 'Qwen3 Source' to a Diffusers Z-Image model."
+ )
+
+ # Determine Qwen3 Encoder source
+ if self.qwen3_encoder_model is not None:
+ # Use standalone Qwen3 Encoder
+ qwen3_tokenizer = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ elif self.qwen3_source_model is not None:
+ # Extract from Diffusers Z-Image model
+ self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
+ qwen3_tokenizer = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
+ qwen3_encoder = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
+ else:
+ raise ValueError(
+ "No Qwen3 Encoder source provided. Either set 'Qwen3 Encoder' to a standalone model, "
+ "or set 'Qwen3 Source' to a Diffusers Z-Image model."
+ )
+
+ return ZImageModelLoaderOutput(
+ transformer=TransformerField(transformer=transformer, loras=[]),
+ qwen3_encoder=Qwen3EncoderField(tokenizer=qwen3_tokenizer, text_encoder=qwen3_encoder),
+ vae=VAEField(vae=vae),
+ )
+
+ def _validate_diffusers_format(
+ self, context: InvocationContext, model: ModelIdentifierField, model_name: str
+ ) -> None:
+ """Validate that a model is in Diffusers format."""
+ config = context.models.get_config(model)
+ if config.format != ModelFormat.Diffusers:
+ raise ValueError(
+ f"The {model_name} model must be a Diffusers format Z-Image model. "
+ f"The selected model '{config.name}' is in {config.format.value} format."
+ )
diff --git a/invokeai/app/invocations/z_image_seed_variance_enhancer.py b/invokeai/app/invocations/z_image_seed_variance_enhancer.py
new file mode 100644
index 00000000000..72819a966a2
--- /dev/null
+++ b/invokeai/app/invocations/z_image_seed_variance_enhancer.py
@@ -0,0 +1,110 @@
+import torch
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ ZImageConditioningField,
+)
+from invokeai.app.invocations.primitives import ZImageConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ ConditioningFieldData,
+ ZImageConditioningInfo,
+)
+
+
+@invocation(
+ "z_image_seed_variance_enhancer",
+ title="Seed Variance Enhancer - Z-Image",
+ tags=["conditioning", "z-image", "variance", "seed"],
+ category="prompt",
+ version="1.0.0",
+ classification=Classification.Prototype,
+)
+class ZImageSeedVarianceEnhancerInvocation(BaseInvocation):
+ """Adds seed-based noise to Z-Image conditioning to increase variance between seeds.
+
+ Z-Image-Turbo can produce relatively similar images with different seeds,
+ making it harder to explore variations of a prompt. This node implements
+ reproducible, seed-based noise injection into text embeddings to increase
+ visual variation while maintaining reproducibility.
+
+ The noise strength is auto-calibrated relative to the embedding's standard
+ deviation, ensuring consistent results across different prompts.
+ """
+
+ conditioning: ZImageConditioningField = InputField(
+ description=FieldDescriptions.cond,
+ input=Input.Connection,
+ title="Conditioning",
+ )
+ seed: int = InputField(
+ default=0,
+ ge=0,
+ description="Seed for reproducible noise generation. Different seeds produce different noise patterns.",
+ )
+ strength: float = InputField(
+ default=0.1,
+ ge=0.0,
+ le=2.0,
+ description="Noise strength as multiplier of embedding std. 0=off, 0.1=subtle, 0.5=strong.",
+ )
+ randomize_percent: float = InputField(
+ default=50.0,
+ ge=1.0,
+ le=100.0,
+ description="Percentage of embedding values to add noise to (1-100). Lower values create more selective noise patterns.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ZImageConditioningOutput:
+ # Load conditioning data
+ cond_data = context.conditioning.load(self.conditioning.conditioning_name)
+ assert len(cond_data.conditionings) == 1, "Expected exactly one conditioning tensor"
+ z_image_conditioning = cond_data.conditionings[0]
+ assert isinstance(z_image_conditioning, ZImageConditioningInfo), "Expected ZImageConditioningInfo"
+
+ # Early return if strength is zero (no modification needed)
+ if self.strength == 0:
+ return ZImageConditioningOutput(conditioning=self.conditioning)
+
+ # Clone embeddings to avoid modifying the original
+ prompt_embeds = z_image_conditioning.prompt_embeds.clone()
+
+ # Calculate actual noise strength based on embedding statistics
+ # This auto-calibration ensures consistent results across different prompts
+ embed_std = torch.std(prompt_embeds).item()
+ actual_strength = self.strength * embed_std
+
+ # Generate deterministic noise using the seed
+ generator = torch.Generator(device=prompt_embeds.device)
+ generator.manual_seed(self.seed)
+ noise = torch.rand(
+ prompt_embeds.shape, generator=generator, device=prompt_embeds.device, dtype=prompt_embeds.dtype
+ )
+ noise = noise * 2 - 1 # Scale to [-1, 1)
+ noise = noise * actual_strength
+
+ # Create selective mask for noise application
+ generator.manual_seed(self.seed + 1)
+ noise_mask = torch.bernoulli(
+ torch.ones_like(prompt_embeds) * (self.randomize_percent / 100.0),
+ generator=generator,
+ ).bool()
+
+ # Apply noise only to masked positions
+ prompt_embeds = prompt_embeds + (noise * noise_mask)
+
+ # Save modified conditioning
+ new_conditioning = ZImageConditioningInfo(prompt_embeds=prompt_embeds)
+ conditioning_data = ConditioningFieldData(conditionings=[new_conditioning])
+ conditioning_name = context.conditioning.save(conditioning_data)
+
+ return ZImageConditioningOutput(
+ conditioning=ZImageConditioningField(
+ conditioning_name=conditioning_name,
+ mask=self.conditioning.mask,
+ )
+ )
diff --git a/invokeai/app/invocations/z_image_text_encoder.py b/invokeai/app/invocations/z_image_text_encoder.py
new file mode 100644
index 00000000000..71af6085d0e
--- /dev/null
+++ b/invokeai/app/invocations/z_image_text_encoder.py
@@ -0,0 +1,209 @@
+from contextlib import ExitStack
+from typing import Iterator, Optional, Tuple
+
+import torch
+from transformers import PreTrainedModel, PreTrainedTokenizerBase
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
+from invokeai.app.invocations.fields import (
+ FieldDescriptions,
+ Input,
+ InputField,
+ TensorField,
+ UIComponent,
+ ZImageConditioningField,
+)
+from invokeai.app.invocations.model import Qwen3EncoderField
+from invokeai.app.invocations.primitives import ZImageConditioningOutput
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
+from invokeai.backend.patches.layer_patcher import LayerPatcher
+from invokeai.backend.patches.lora_conversions.z_image_lora_constants import Z_IMAGE_LORA_QWEN3_PREFIX
+from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
+ ConditioningFieldData,
+ ZImageConditioningInfo,
+)
+from invokeai.backend.util.devices import TorchDevice
+
+# Z-Image max sequence length based on diffusers default
+Z_IMAGE_MAX_SEQ_LEN = 512
+
+
+@invocation(
+ "z_image_text_encoder",
+ title="Prompt - Z-Image",
+ tags=["prompt", "conditioning", "z-image"],
+ category="prompt",
+ version="1.1.0",
+ classification=Classification.Prototype,
+)
+class ZImageTextEncoderInvocation(BaseInvocation):
+ """Encodes and preps a prompt for a Z-Image image.
+
+ Supports regional prompting by connecting a mask input.
+ """
+
+ prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
+ qwen3_encoder: Qwen3EncoderField = InputField(
+ title="Qwen3 Encoder",
+ description=FieldDescriptions.qwen3_encoder,
+ input=Input.Connection,
+ )
+ mask: Optional[TensorField] = InputField(
+ default=None,
+ description="A mask defining the region that this conditioning prompt applies to.",
+ )
+
+ @torch.no_grad()
+ def invoke(self, context: InvocationContext) -> ZImageConditioningOutput:
+ prompt_embeds = self._encode_prompt(context, max_seq_len=Z_IMAGE_MAX_SEQ_LEN)
+ # Move embeddings to CPU for storage to save VRAM
+ prompt_embeds = prompt_embeds.detach().to("cpu")
+ conditioning_data = ConditioningFieldData(conditionings=[ZImageConditioningInfo(prompt_embeds=prompt_embeds)])
+ conditioning_name = context.conditioning.save(conditioning_data)
+ return ZImageConditioningOutput(
+ conditioning=ZImageConditioningField(conditioning_name=conditioning_name, mask=self.mask)
+ )
+
+ def _encode_prompt(self, context: InvocationContext, max_seq_len: int) -> torch.Tensor:
+ """Encode prompt using Qwen3 text encoder.
+
+ Based on the ZImagePipeline._encode_prompt method from diffusers.
+ """
+ prompt = self.prompt
+
+ text_encoder_info = context.models.load(self.qwen3_encoder.text_encoder)
+ tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer)
+
+ with ExitStack() as exit_stack:
+ (cached_weights, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device())
+ (_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device())
+
+ # Use the device that the text encoder is effectively executing on, and repair any required tensors left on
+ # the CPU by a previous interrupted run.
+ repaired_tensors = text_encoder_info.repair_required_tensors_on_device()
+ device = get_effective_device(text_encoder)
+ if repaired_tensors > 0:
+ context.logger.warning(
+ f"Recovered {repaired_tensors} required Qwen3 tensor(s) onto {device} after a partial device mismatch."
+ )
+
+ # Apply LoRA models to the text encoder
+ lora_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
+ exit_stack.enter_context(
+ LayerPatcher.apply_smart_model_patches(
+ model=text_encoder,
+ patches=self._lora_iterator(context),
+ prefix=Z_IMAGE_LORA_QWEN3_PREFIX,
+ dtype=lora_dtype,
+ cached_weights=cached_weights,
+ )
+ )
+
+ context.util.signal_progress("Running Qwen3 text encoder")
+ if not isinstance(text_encoder, PreTrainedModel):
+ raise TypeError(
+ f"Expected PreTrainedModel for text encoder, got {type(text_encoder).__name__}. "
+ "The Qwen3 encoder model may be corrupted or incompatible."
+ )
+ if not isinstance(tokenizer, PreTrainedTokenizerBase):
+ raise TypeError(
+ f"Expected PreTrainedTokenizerBase for tokenizer, got {type(tokenizer).__name__}. "
+ "The Qwen3 tokenizer may be corrupted or incompatible."
+ )
+
+ # Apply chat template similar to diffusers ZImagePipeline
+ # The chat template formats the prompt for the Qwen3 model
+ try:
+ prompt_formatted = tokenizer.apply_chat_template(
+ [{"role": "user", "content": prompt}],
+ tokenize=False,
+ add_generation_prompt=True,
+ enable_thinking=True,
+ )
+ except (AttributeError, TypeError) as e:
+ # Fallback if tokenizer doesn't support apply_chat_template or enable_thinking
+ context.logger.warning(f"Chat template failed ({e}), using raw prompt.")
+ prompt_formatted = prompt
+
+ # Tokenize the formatted prompt
+ text_inputs = tokenizer(
+ prompt_formatted,
+ padding="max_length",
+ max_length=max_seq_len,
+ truncation=True,
+ return_attention_mask=True,
+ return_tensors="pt",
+ )
+
+ text_input_ids = text_inputs.input_ids
+ attention_mask = text_inputs.attention_mask
+ if not isinstance(text_input_ids, torch.Tensor):
+ raise TypeError(
+ f"Expected torch.Tensor for input_ids, got {type(text_input_ids).__name__}. "
+ "Tokenizer returned unexpected type."
+ )
+ if not isinstance(attention_mask, torch.Tensor):
+ raise TypeError(
+ f"Expected torch.Tensor for attention_mask, got {type(attention_mask).__name__}. "
+ "Tokenizer returned unexpected type."
+ )
+
+ # Check for truncation
+ untruncated_ids = tokenizer(prompt_formatted, padding="longest", return_tensors="pt").input_ids
+ if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
+ text_input_ids, untruncated_ids
+ ):
+ removed_text = tokenizer.batch_decode(untruncated_ids[:, max_seq_len - 1 : -1])
+ context.logger.warning(
+ f"The following part of your input was truncated because `max_sequence_length` is set to "
+ f"{max_seq_len} tokens: {removed_text}"
+ )
+
+ # Get hidden states from the text encoder
+ # Use the second-to-last hidden state like diffusers does
+ prompt_mask = attention_mask.to(device).bool()
+ outputs = text_encoder(
+ text_input_ids.to(device),
+ attention_mask=prompt_mask,
+ output_hidden_states=True,
+ )
+
+ # Validate hidden_states output
+ if not hasattr(outputs, "hidden_states") or outputs.hidden_states is None:
+ raise RuntimeError(
+ "Text encoder did not return hidden_states. "
+ "Ensure output_hidden_states=True is supported by this model."
+ )
+ if len(outputs.hidden_states) < 2:
+ raise RuntimeError(
+ f"Expected at least 2 hidden states from text encoder, got {len(outputs.hidden_states)}. "
+ "This may indicate an incompatible model or configuration."
+ )
+ prompt_embeds = outputs.hidden_states[-2]
+
+ # Z-Image expects a 2D tensor [seq_len, hidden_dim] with only valid tokens
+ # Based on diffusers ZImagePipeline implementation:
+ # embeddings_list.append(prompt_embeds[i][prompt_masks[i]])
+ # Since batch_size=1, we take the first item and filter by mask
+ prompt_embeds = prompt_embeds[0][prompt_mask[0]]
+
+ if not isinstance(prompt_embeds, torch.Tensor):
+ raise TypeError(
+ f"Expected torch.Tensor for prompt embeddings, got {type(prompt_embeds).__name__}. "
+ "Text encoder returned unexpected type."
+ )
+ return prompt_embeds
+
+ def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
+ """Iterate over LoRA models to apply to the Qwen3 text encoder."""
+ for lora in self.qwen3_encoder.loras:
+ lora_info = context.models.load(lora.lora)
+ if not isinstance(lora_info.model, ModelPatchRaw):
+ raise TypeError(
+ f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
+ "The LoRA model may be corrupted or incompatible."
+ )
+ yield (lora_info.model, lora.weight)
+ del lora_info
diff --git a/invokeai/app/run_app.py b/invokeai/app/run_app.py
new file mode 100644
index 00000000000..febd4f4d4b1
--- /dev/null
+++ b/invokeai/app/run_app.py
@@ -0,0 +1,145 @@
+def get_app():
+ """Import the app and event loop. We wrap this in a function to more explicitly control when it happens, because
+ importing from api_app does a bunch of stuff - it's more like calling a function than importing a module.
+ """
+ from invokeai.app.api_app import app, loop
+
+ return app, loop
+
+
+def run_app() -> None:
+ """The main entrypoint for the app."""
+ import asyncio
+ import sys
+ import threading
+ import traceback
+
+ from invokeai.frontend.cli.arg_parser import InvokeAIArgs
+
+ # Parse the CLI arguments before doing anything else, which ensures CLI args correctly override settings from other
+ # sources like `invokeai.yaml` or env vars.
+ InvokeAIArgs.parse_args()
+
+ import uvicorn
+
+ from invokeai.app.services.config.config_default import get_config
+ from invokeai.app.util.torch_cuda_allocator import configure_torch_cuda_allocator
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ # Load config.
+ app_config = get_config()
+
+ logger = InvokeAILogger.get_logger(config=app_config)
+
+ # Configure the torch CUDA memory allocator.
+ # NOTE: It is important that this happens before torch is imported.
+ if app_config.pytorch_cuda_alloc_conf:
+ configure_torch_cuda_allocator(app_config.pytorch_cuda_alloc_conf, logger)
+
+ # This import must happen after configure_torch_cuda_allocator() is called, because the module imports torch.
+ from invokeai.app.invocations.baseinvocation import InvocationRegistry
+ from invokeai.app.invocations.load_custom_nodes import load_custom_nodes
+ from invokeai.backend.util.devices import TorchDevice
+
+ torch_device_name = TorchDevice.get_torch_device_name()
+ logger.info(f"Using torch device: {torch_device_name}")
+
+ # Import from startup_utils here to avoid importing torch before configure_torch_cuda_allocator() is called.
+ from invokeai.app.util.startup_utils import (
+ apply_monkeypatches,
+ check_cudnn,
+ enable_dev_reload,
+ find_open_port,
+ register_mime_types,
+ )
+
+ # Find an open port, and modify the config accordingly.
+ first_open_port = find_open_port(app_config.port)
+ if app_config.port != first_open_port:
+ orig_config_port = app_config.port
+ app_config.port = first_open_port
+ logger.warning(f"Port {orig_config_port} is already in use. Using port {app_config.port}.")
+
+ # Miscellaneous startup tasks.
+ apply_monkeypatches()
+ register_mime_types()
+ check_cudnn(logger)
+
+ # Initialize the app and event loop.
+ app, loop = get_app()
+
+ # Load custom nodes. This must be done after importing the Graph class, which itself imports all modules from the
+ # invocations module. The ordering here is implicit, but important - we want to load custom nodes after all the
+ # core nodes have been imported so that we can catch when a custom node clobbers a core node.
+ load_custom_nodes(custom_nodes_path=app_config.custom_nodes_path, logger=logger)
+
+ # Check all invocations and ensure their outputs are registered.
+ for invocation in InvocationRegistry.get_invocation_classes():
+ invocation_type = invocation.get_type()
+ output_annotation = invocation.get_output_annotation()
+ if output_annotation not in InvocationRegistry.get_output_classes():
+ logger.warning(
+ f'Invocation "{invocation_type}" has unregistered output class "{output_annotation.__name__}"'
+ )
+
+ if app_config.dev_reload:
+ # load_custom_nodes seems to bypass jurrigged's import sniffer, so be sure to call it *after* they're already
+ # imported.
+ enable_dev_reload(custom_nodes_path=app_config.custom_nodes_path)
+
+ # Start the server.
+ config = uvicorn.Config(
+ app=app,
+ host=app_config.host,
+ port=app_config.port,
+ loop="asyncio",
+ log_level=app_config.log_level_network,
+ ssl_certfile=app_config.ssl_certfile,
+ ssl_keyfile=app_config.ssl_keyfile,
+ )
+ server = uvicorn.Server(config)
+
+ # replace uvicorn's loggers with InvokeAI's for consistent appearance
+ uvicorn_logger = InvokeAILogger.get_logger("uvicorn")
+ uvicorn_logger.handlers.clear()
+ for hdlr in logger.handlers:
+ uvicorn_logger.addHandler(hdlr)
+
+ try:
+ loop.run_until_complete(server.serve())
+ except KeyboardInterrupt:
+ logger.info("InvokeAI shutting down...")
+ # Gracefully shut down services (e.g. model download and install managers) so that any
+ # active work is completed or cleanly cancelled before the process exits.
+ from invokeai.app.api.dependencies import ApiDependencies
+
+ ApiDependencies.shutdown()
+
+ # Cancel any pending asyncio tasks (e.g. socket.io ping tasks) so that loop.close() does
+ # not emit "Task was destroyed but it is pending!" warnings for each one.
+ pending = [t for t in asyncio.all_tasks(loop) if not t.done()]
+ for task in pending:
+ task.cancel()
+ if pending:
+ loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
+
+ # Shut down the asyncio default thread executor. asyncio.to_thread() (used e.g. in the
+ # session queue for SQLite operations during generation) creates non-daemon threads via the
+ # event loop's default ThreadPoolExecutor. Without this call those threads remain alive and
+ # cause threading._shutdown() to hang indefinitely after the process's main code finishes.
+ loop.run_until_complete(loop.shutdown_default_executor())
+ loop.close()
+
+ # After graceful shutdown, log any non-daemon threads that are still alive. These are the
+ # threads that will cause Python's threading._shutdown() to block, preventing the process
+ # from exiting cleanly. This helps identify threads that need to be fixed or joined.
+ frames = sys._current_frames()
+ for thread in threading.enumerate():
+ if thread.daemon or thread is threading.main_thread():
+ continue
+ frame = frames.get(thread.ident)
+ stack = "".join(traceback.format_stack(frame)) if frame else "(no frame available)"
+ logger.warning(
+ f"Non-daemon thread still alive after shutdown: {thread.name!r} "
+ f"(ident={thread.ident})\nStack trace:\n{stack}"
+ )
diff --git a/ldm/modules/distributions/__init__.py b/invokeai/app/services/__init__.py
similarity index 100%
rename from ldm/modules/distributions/__init__.py
rename to invokeai/app/services/__init__.py
diff --git a/invokeai/app/services/app_settings/__init__.py b/invokeai/app/services/app_settings/__init__.py
new file mode 100644
index 00000000000..0345874c11f
--- /dev/null
+++ b/invokeai/app/services/app_settings/__init__.py
@@ -0,0 +1,5 @@
+"""App settings service exports."""
+
+from invokeai.app.services.app_settings.app_settings_service import AppSettingsService
+
+__all__ = ["AppSettingsService"]
diff --git a/invokeai/app/services/app_settings/app_settings_service.py b/invokeai/app/services/app_settings/app_settings_service.py
new file mode 100644
index 00000000000..5580709ef65
--- /dev/null
+++ b/invokeai/app/services/app_settings/app_settings_service.py
@@ -0,0 +1,74 @@
+"""Service for managing application-level settings stored in the database."""
+
+from typing import Optional
+
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class AppSettingsService:
+ """Service for accessing application-level settings from the database.
+
+ This service provides a simple key-value store for application-level configuration
+ that needs to be persisted across restarts, such as JWT secrets.
+ """
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ """Initialize the app settings service.
+
+ Args:
+ db: The SQLite database instance
+ """
+ self._db = db
+
+ def get(self, key: str) -> Optional[str]:
+ """Get a setting value by key.
+
+ Args:
+ key: The setting key
+
+ Returns:
+ The setting value if found, None otherwise
+ """
+ try:
+ with self._db.transaction() as cursor:
+ cursor.execute("SELECT value FROM app_settings WHERE key = ?;", (key,))
+ row = cursor.fetchone()
+ return row[0] if row else None
+ except Exception:
+ return None
+
+ def set(self, key: str, value: str) -> None:
+ """Set a setting value.
+
+ Args:
+ key: The setting key
+ value: The setting value
+ """
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ INSERT INTO app_settings (key, value)
+ VALUES (?, ?)
+ ON CONFLICT(key) DO UPDATE SET
+ value = excluded.value,
+ updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW');
+ """,
+ (key, value),
+ )
+
+ def get_jwt_secret(self) -> str:
+ """Get the JWT secret key from the database.
+
+ Returns:
+ The JWT secret key
+
+ Raises:
+ RuntimeError: If the JWT secret is not found in the database
+ """
+ secret = self.get("jwt_secret")
+ if secret is None:
+ raise RuntimeError(
+ "JWT secret not found in database. This should have been created during database migration. "
+ "Please ensure database migrations have been run successfully."
+ )
+ return secret
diff --git a/invokeai/app/services/auth/__init__.py b/invokeai/app/services/auth/__init__.py
new file mode 100644
index 00000000000..099a5e7da1b
--- /dev/null
+++ b/invokeai/app/services/auth/__init__.py
@@ -0,0 +1 @@
+"""Authentication service module."""
diff --git a/invokeai/app/services/auth/password_utils.py b/invokeai/app/services/auth/password_utils.py
new file mode 100644
index 00000000000..b960af5f1c5
--- /dev/null
+++ b/invokeai/app/services/auth/password_utils.py
@@ -0,0 +1,113 @@
+"""Password hashing and validation utilities."""
+
+from typing import Literal, cast
+
+from passlib.context import CryptContext
+
+# Configure bcrypt context - set truncate_error=False to allow passwords >72 bytes
+# without raising an error. They will be automatically truncated by bcrypt to 72 bytes.
+pwd_context = CryptContext(
+ schemes=["bcrypt"],
+ deprecated="auto",
+ bcrypt__truncate_error=False,
+)
+
+
+def hash_password(password: str) -> str:
+ """Hash a password using bcrypt.
+
+ bcrypt has a maximum password length of 72 bytes. Longer passwords
+ are automatically truncated to comply with this limit.
+
+ Args:
+ password: The plain text password to hash
+
+ Returns:
+ The hashed password
+ """
+ # bcrypt has a 72 byte limit - encode and truncate if necessary
+ password_bytes = password.encode("utf-8")
+ if len(password_bytes) > 72:
+ # Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
+ password = password_bytes[:72].decode("utf-8", errors="ignore")
+ return cast(str, pwd_context.hash(password))
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Verify a password against a hash.
+
+ bcrypt has a maximum password length of 72 bytes. Longer passwords
+ are automatically truncated to match hash_password behavior.
+
+ Args:
+ plain_password: The plain text password to verify
+ hashed_password: The hashed password to verify against
+
+ Returns:
+ True if the password matches the hash, False otherwise
+ """
+ try:
+ # bcrypt has a 72 byte limit - encode and truncate if necessary to match hash_password
+ password_bytes = plain_password.encode("utf-8")
+ if len(password_bytes) > 72:
+ # Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
+ plain_password = password_bytes[:72].decode("utf-8", errors="ignore")
+ return cast(bool, pwd_context.verify(plain_password, hashed_password))
+ except Exception:
+ # Invalid hash format or other error - return False
+ return False
+
+
+def validate_password_strength(password: str) -> tuple[bool, str]:
+ """Validate password meets minimum security requirements.
+
+ Password requirements:
+ - At least 8 characters long
+ - Contains at least one uppercase letter
+ - Contains at least one lowercase letter
+ - Contains at least one digit
+
+ Args:
+ password: The password to validate
+
+ Returns:
+ A tuple of (is_valid, error_message). If valid, error_message is empty.
+ """
+ if len(password) < 8:
+ return False, "Password must be at least 8 characters long"
+
+ has_upper = any(c.isupper() for c in password)
+ has_lower = any(c.islower() for c in password)
+ has_digit = any(c.isdigit() for c in password)
+
+ if not (has_upper and has_lower and has_digit):
+ return False, "Password must contain uppercase, lowercase, and numbers"
+
+ return True, ""
+
+
+def get_password_strength(password: str) -> Literal["weak", "moderate", "strong"]:
+ """Determine the strength of a password.
+
+ Strength levels:
+ - weak: less than 8 characters
+ - moderate: 8+ characters but missing at least one of uppercase, lowercase, or digit
+ - strong: 8+ characters with uppercase, lowercase, and digit
+
+ Args:
+ password: The password to evaluate
+
+ Returns:
+ One of "weak", "moderate", or "strong"
+ """
+ if len(password) < 8:
+ return "weak"
+
+ has_upper = any(c.isupper() for c in password)
+ has_lower = any(c.islower() for c in password)
+ has_digit = any(c.isdigit() for c in password)
+
+ if not (has_upper and has_lower and has_digit):
+ return "moderate"
+
+ return "strong"
diff --git a/invokeai/app/services/auth/token_service.py b/invokeai/app/services/auth/token_service.py
new file mode 100644
index 00000000000..2d766bb90aa
--- /dev/null
+++ b/invokeai/app/services/auth/token_service.py
@@ -0,0 +1,106 @@
+"""JWT token generation and validation."""
+
+from datetime import datetime, timedelta, timezone
+from typing import cast
+
+from jose import JWTError, jwt
+from pydantic import BaseModel
+
+ALGORITHM = "HS256"
+DEFAULT_EXPIRATION_HOURS = 24
+
+# Module-level variable to store the JWT secret. This is set during application initialization
+# by calling set_jwt_secret(). The secret is loaded from the database where it is stored
+# securely after being generated during database migration.
+_jwt_secret: str | None = None
+
+
+class TokenData(BaseModel):
+ """Data stored in JWT token."""
+
+ user_id: str
+ email: str
+ is_admin: bool
+ remember_me: bool = False
+
+
+def set_jwt_secret(secret: str) -> None:
+ """Set the JWT secret key for token signing and verification.
+
+ This should be called once during application initialization with the secret
+ loaded from the database.
+
+ Args:
+ secret: The JWT secret key
+ """
+ global _jwt_secret
+ _jwt_secret = secret
+
+
+def get_jwt_secret() -> str:
+ """Get the JWT secret key.
+
+ Returns:
+ The JWT secret key
+
+ Raises:
+ RuntimeError: If the secret has not been initialized
+ """
+ if _jwt_secret is None:
+ raise RuntimeError("JWT secret has not been initialized. Call set_jwt_secret() during application startup.")
+ return _jwt_secret
+
+
+def create_access_token(data: TokenData, expires_delta: timedelta | None = None) -> str:
+ """Create a JWT access token.
+
+ Args:
+ data: The token data to encode
+ expires_delta: Optional expiration time delta. Defaults to 24 hours.
+
+ Returns:
+ The encoded JWT token
+ """
+ to_encode = data.model_dump()
+ expire = datetime.now(timezone.utc) + (expires_delta or timedelta(hours=DEFAULT_EXPIRATION_HOURS))
+ to_encode.update({"exp": expire})
+ return cast(str, jwt.encode(to_encode, get_jwt_secret(), algorithm=ALGORITHM))
+
+
+def verify_token(token: str) -> TokenData | None:
+ """Verify and decode a JWT token.
+
+ Args:
+ token: The JWT token to verify
+
+ Returns:
+ TokenData if valid, None if invalid or expired
+ """
+ try:
+ # python-jose 3.5.0 has a bug where exp verification doesn't work properly
+ # We need to manually check expiration, but MUST verify signature first
+ # to prevent accepting tokens with valid payloads but invalid signatures
+
+ # First, verify the signature - this will raise JWTError if signature is invalid
+ # Note: python-jose won't reject expired tokens here due to the bug
+ payload = jwt.decode(
+ token,
+ get_jwt_secret(),
+ algorithms=[ALGORITHM],
+ )
+
+ # Now manually check expiration (because python-jose 3.5.0 doesn't do this properly)
+ if "exp" in payload:
+ exp_timestamp = payload["exp"]
+ current_timestamp = datetime.now(timezone.utc).timestamp()
+ if current_timestamp >= exp_timestamp:
+ # Token is expired
+ return None
+
+ return TokenData(**payload)
+ except JWTError:
+ # Token is invalid (bad signature, malformed, etc.)
+ return None
+ except Exception:
+ # Catch any other exceptions (e.g., Pydantic validation errors)
+ return None
diff --git a/ldm/modules/encoders/__init__.py b/invokeai/app/services/board_image_records/__init__.py
similarity index 100%
rename from ldm/modules/encoders/__init__.py
rename to invokeai/app/services/board_image_records/__init__.py
diff --git a/invokeai/app/services/board_image_records/board_image_records_base.py b/invokeai/app/services/board_image_records/board_image_records_base.py
new file mode 100644
index 00000000000..4ccbaa952db
--- /dev/null
+++ b/invokeai/app/services/board_image_records/board_image_records_base.py
@@ -0,0 +1,59 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+
+
+class BoardImageRecordStorageBase(ABC):
+ """Abstract base class for the one-to-many board-image relationship record storage."""
+
+ @abstractmethod
+ def add_image_to_board(
+ self,
+ board_id: str,
+ image_name: str,
+ ) -> None:
+ """Adds an image to a board."""
+ pass
+
+ @abstractmethod
+ def remove_image_from_board(
+ self,
+ image_name: str,
+ ) -> None:
+ """Removes an image from a board."""
+ pass
+
+ @abstractmethod
+ def get_all_board_image_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ ) -> list[str]:
+ """Gets all board images for a board, as a list of the image names."""
+ pass
+
+ @abstractmethod
+ def get_board_for_image(
+ self,
+ image_name: str,
+ ) -> Optional[str]:
+ """Gets an image's board id, if it has one."""
+ pass
+
+ @abstractmethod
+ def get_image_count_for_board(
+ self,
+ board_id: str,
+ ) -> int:
+ """Gets the number of images for a board."""
+ pass
+
+ @abstractmethod
+ def get_asset_count_for_board(
+ self,
+ board_id: str,
+ ) -> int:
+ """Gets the number of assets for a board."""
+ pass
diff --git a/invokeai/app/services/board_image_records/board_image_records_sqlite.py b/invokeai/app/services/board_image_records/board_image_records_sqlite.py
new file mode 100644
index 00000000000..b249bb67334
--- /dev/null
+++ b/invokeai/app/services/board_image_records/board_image_records_sqlite.py
@@ -0,0 +1,190 @@
+import sqlite3
+from typing import Optional, cast
+
+from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
+from invokeai.app.services.image_records.image_records_common import (
+ ASSETS_CATEGORIES,
+ IMAGE_CATEGORIES,
+ ImageCategory,
+ ImageRecord,
+ deserialize_image_record,
+)
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class SqliteBoardImageRecordStorage(BoardImageRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def add_image_to_board(
+ self,
+ board_id: str,
+ image_name: str,
+ ) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ INSERT INTO board_images (board_id, image_name)
+ VALUES (?, ?)
+ ON CONFLICT (image_name) DO UPDATE SET board_id = ?;
+ """,
+ (board_id, image_name, board_id),
+ )
+
+ def remove_image_from_board(
+ self,
+ image_name: str,
+ ) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE FROM board_images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+
+ def get_images_for_board(
+ self,
+ board_id: str,
+ offset: int = 0,
+ limit: int = 10,
+ ) -> OffsetPaginatedResults[ImageRecord]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT images.*
+ FROM board_images
+ INNER JOIN images ON board_images.image_name = images.image_name
+ WHERE board_images.board_id = ?
+ ORDER BY board_images.updated_at DESC;
+ """,
+ (board_id,),
+ )
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ images = [deserialize_image_record(dict(r)) for r in result]
+
+ cursor.execute(
+ """--sql
+ SELECT COUNT(*) FROM images WHERE 1=1;
+ """
+ )
+ count = cast(int, cursor.fetchone()[0])
+
+ return OffsetPaginatedResults(items=images, offset=offset, limit=limit, total=count)
+
+ def get_all_board_image_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ ) -> list[str]:
+ with self._db.transaction() as cursor:
+ params: list[str | bool] = []
+
+ # Base query is a join between images and board_images
+ stmt = """
+ SELECT images.image_name
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE 1=1
+ """
+
+ # Handle board_id filter
+ if board_id == "none":
+ stmt += """--sql
+ AND board_images.board_id IS NULL
+ """
+ else:
+ stmt += """--sql
+ AND board_images.board_id = ?
+ """
+ params.append(board_id)
+
+ # Add the category filter
+ if categories is not None:
+ # Convert the enum values to unique list of strings
+ category_strings = [c.value for c in set(categories)]
+ # Create the correct length of placeholders
+ placeholders = ",".join("?" * len(category_strings))
+ stmt += f"""--sql
+ AND images.image_category IN ( {placeholders} )
+ """
+
+ # Unpack the included categories into the query params
+ for c in category_strings:
+ params.append(c)
+
+ # Add the is_intermediate filter
+ if is_intermediate is not None:
+ stmt += """--sql
+ AND images.is_intermediate = ?
+ """
+ params.append(is_intermediate)
+
+ # Put a ring on it
+ stmt += ";"
+
+ cursor.execute(stmt, params)
+
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ image_names = [r[0] for r in result]
+ return image_names
+
+ def get_board_for_image(
+ self,
+ image_name: str,
+ ) -> Optional[str]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT board_id
+ FROM board_images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+ result = cursor.fetchone()
+ if result is None:
+ return None
+ return cast(str, result[0])
+
+ def get_image_count_for_board(self, board_id: str) -> int:
+ with self._db.transaction() as cursor:
+ # Convert the enum values to unique list of strings
+ category_strings = [c.value for c in set(IMAGE_CATEGORIES)]
+ # Create the correct length of placeholders
+ placeholders = ",".join("?" * len(category_strings))
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM board_images
+ INNER JOIN images ON board_images.image_name = images.image_name
+ WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
+ AND board_images.board_id = ?;
+ """,
+ (*category_strings, board_id),
+ )
+ count = cast(int, cursor.fetchone()[0])
+ return count
+
+ def get_asset_count_for_board(self, board_id: str) -> int:
+ with self._db.transaction() as cursor:
+ # Convert the enum values to unique list of strings
+ category_strings = [c.value for c in set(ASSETS_CATEGORIES)]
+ # Create the correct length of placeholders
+ placeholders = ",".join("?" * len(category_strings))
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM board_images
+ INNER JOIN images ON board_images.image_name = images.image_name
+ WHERE images.is_intermediate = FALSE AND images.image_category IN ( {placeholders} )
+ AND board_images.board_id = ?;
+ """,
+ (*category_strings, board_id),
+ )
+ count = cast(int, cursor.fetchone()[0])
+ return count
diff --git a/invokeai/app/services/board_images/__init__.py b/invokeai/app/services/board_images/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/board_images/board_images_base.py b/invokeai/app/services/board_images/board_images_base.py
new file mode 100644
index 00000000000..c16d971cd28
--- /dev/null
+++ b/invokeai/app/services/board_images/board_images_base.py
@@ -0,0 +1,43 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+
+
+class BoardImagesServiceABC(ABC):
+ """High-level service for board-image relationship management."""
+
+ @abstractmethod
+ def add_image_to_board(
+ self,
+ board_id: str,
+ image_name: str,
+ ) -> None:
+ """Adds an image to a board."""
+ pass
+
+ @abstractmethod
+ def remove_image_from_board(
+ self,
+ image_name: str,
+ ) -> None:
+ """Removes an image from a board."""
+ pass
+
+ @abstractmethod
+ def get_all_board_image_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ ) -> list[str]:
+ """Gets all board images for a board, as a list of the image names."""
+ pass
+
+ @abstractmethod
+ def get_board_for_image(
+ self,
+ image_name: str,
+ ) -> Optional[str]:
+ """Gets an image's board id, if it has one."""
+ pass
diff --git a/invokeai/app/services/board_images/board_images_common.py b/invokeai/app/services/board_images/board_images_common.py
new file mode 100644
index 00000000000..fe585215f30
--- /dev/null
+++ b/invokeai/app/services/board_images/board_images_common.py
@@ -0,0 +1,8 @@
+from pydantic import Field
+
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class BoardImage(BaseModelExcludeNull):
+ board_id: str = Field(description="The id of the board")
+ image_name: str = Field(description="The name of the image")
diff --git a/invokeai/app/services/board_images/board_images_default.py b/invokeai/app/services/board_images/board_images_default.py
new file mode 100644
index 00000000000..437495189f3
--- /dev/null
+++ b/invokeai/app/services/board_images/board_images_default.py
@@ -0,0 +1,44 @@
+from typing import Optional
+
+from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+from invokeai.app.services.invoker import Invoker
+
+
+class BoardImagesService(BoardImagesServiceABC):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def add_image_to_board(
+ self,
+ board_id: str,
+ image_name: str,
+ ) -> None:
+ self.__invoker.services.board_image_records.add_image_to_board(board_id, image_name)
+
+ def remove_image_from_board(
+ self,
+ image_name: str,
+ ) -> None:
+ self.__invoker.services.board_image_records.remove_image_from_board(image_name)
+
+ def get_all_board_image_names_for_board(
+ self,
+ board_id: str,
+ categories: list[ImageCategory] | None,
+ is_intermediate: bool | None,
+ ) -> list[str]:
+ return self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
+ board_id,
+ categories,
+ is_intermediate,
+ )
+
+ def get_board_for_image(
+ self,
+ image_name: str,
+ ) -> Optional[str]:
+ board_id = self.__invoker.services.board_image_records.get_board_for_image(image_name)
+ return board_id
diff --git a/invokeai/app/services/board_records/board_records_base.py b/invokeai/app/services/board_records/board_records_base.py
new file mode 100644
index 00000000000..20981f2c7d7
--- /dev/null
+++ b/invokeai/app/services/board_records/board_records_base.py
@@ -0,0 +1,66 @@
+from abc import ABC, abstractmethod
+
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord, BoardRecordOrderBy
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class BoardRecordStorageBase(ABC):
+ """Low-level service responsible for interfacing with the board record store."""
+
+ @abstractmethod
+ def delete(self, board_id: str) -> None:
+ """Deletes a board record."""
+ pass
+
+ @abstractmethod
+ def save(
+ self,
+ board_name: str,
+ user_id: str,
+ ) -> BoardRecord:
+ """Saves a board record for a specific user."""
+ pass
+
+ @abstractmethod
+ def get(
+ self,
+ board_id: str,
+ ) -> BoardRecord:
+ """Gets a board record."""
+ pass
+
+ @abstractmethod
+ def update(
+ self,
+ board_id: str,
+ changes: BoardChanges,
+ ) -> BoardRecord:
+ """Updates a board record."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ offset: int = 0,
+ limit: int = 10,
+ include_archived: bool = False,
+ ) -> OffsetPaginatedResults[BoardRecord]:
+ """Gets many board records for a specific user, including shared boards. Admin users see all boards."""
+ pass
+
+ @abstractmethod
+ def get_all(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ include_archived: bool = False,
+ ) -> list[BoardRecord]:
+ """Gets all board records for a specific user, including shared boards. Admin users see all boards."""
+ pass
diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py
new file mode 100644
index 00000000000..b263f264cb8
--- /dev/null
+++ b/invokeai/app/services/board_records/board_records_common.py
@@ -0,0 +1,113 @@
+from datetime import datetime
+from enum import Enum
+from typing import Optional, Union
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.util.metaenum import MetaEnum
+from invokeai.app.util.misc import get_iso_timestamp
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class BoardVisibility(str, Enum, metaclass=MetaEnum):
+ """The visibility options for a board."""
+
+ Private = "private"
+ """Only the board owner (and admins) can see and modify this board."""
+ Shared = "shared"
+ """All users can view this board, but only the owner (and admins) can modify it."""
+ Public = "public"
+ """All users can view this board; only the owner (and admins) can modify its structure."""
+
+
+class BoardRecord(BaseModelExcludeNull):
+ """Deserialized board record."""
+
+ board_id: str = Field(description="The unique ID of the board.")
+ """The unique ID of the board."""
+ board_name: str = Field(description="The name of the board.")
+ """The name of the board."""
+ user_id: str = Field(description="The user ID of the board owner.")
+ """The user ID of the board owner."""
+ created_at: Union[datetime, str] = Field(description="The created timestamp of the board.")
+ """The created timestamp of the image."""
+ updated_at: Union[datetime, str] = Field(description="The updated timestamp of the board.")
+ """The updated timestamp of the image."""
+ deleted_at: Optional[Union[datetime, str]] = Field(default=None, description="The deleted timestamp of the board.")
+ """The updated timestamp of the image."""
+ cover_image_name: Optional[str] = Field(default=None, description="The name of the cover image of the board.")
+ """The name of the cover image of the board."""
+ archived: bool = Field(description="Whether or not the board is archived.")
+ """Whether or not the board is archived."""
+ board_visibility: BoardVisibility = Field(
+ default=BoardVisibility.Private, description="The visibility of the board."
+ )
+ """The visibility of the board (private, shared, or public)."""
+
+
+def deserialize_board_record(board_dict: dict) -> BoardRecord:
+ """Deserializes a board record."""
+
+ # Retrieve all the values, setting "reasonable" defaults if they are not present.
+
+ board_id = board_dict.get("board_id", "unknown")
+ board_name = board_dict.get("board_name", "unknown")
+ # Default to 'system' for backwards compatibility with boards created before multiuser support
+ user_id = board_dict.get("user_id", "system")
+ cover_image_name = board_dict.get("cover_image_name", "unknown")
+ created_at = board_dict.get("created_at", get_iso_timestamp())
+ updated_at = board_dict.get("updated_at", get_iso_timestamp())
+ deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
+ archived = board_dict.get("archived", False)
+ board_visibility_raw = board_dict.get("board_visibility", BoardVisibility.Private.value)
+ try:
+ board_visibility = BoardVisibility(board_visibility_raw)
+ except ValueError:
+ board_visibility = BoardVisibility.Private
+
+ return BoardRecord(
+ board_id=board_id,
+ board_name=board_name,
+ user_id=user_id,
+ cover_image_name=cover_image_name,
+ created_at=created_at,
+ updated_at=updated_at,
+ deleted_at=deleted_at,
+ archived=archived,
+ board_visibility=board_visibility,
+ )
+
+
+class BoardChanges(BaseModel, extra="forbid"):
+ board_name: Optional[str] = Field(default=None, description="The board's new name.", max_length=300)
+ cover_image_name: Optional[str] = Field(default=None, description="The name of the board's new cover image.")
+ archived: Optional[bool] = Field(default=None, description="Whether or not the board is archived")
+ board_visibility: Optional[BoardVisibility] = Field(default=None, description="The visibility of the board.")
+
+
+class BoardRecordOrderBy(str, Enum, metaclass=MetaEnum):
+ """The order by options for board records"""
+
+ CreatedAt = "created_at"
+ Name = "board_name"
+
+
+class BoardRecordNotFoundException(Exception):
+ """Raised when an board record is not found."""
+
+ def __init__(self, message="Board record not found"):
+ super().__init__(message)
+
+
+class BoardRecordSaveException(Exception):
+ """Raised when an board record cannot be saved."""
+
+ def __init__(self, message="Board record not saved"):
+ super().__init__(message)
+
+
+class BoardRecordDeleteException(Exception):
+ """Raised when an board record cannot be deleted."""
+
+ def __init__(self, message="Board record not deleted"):
+ super().__init__(message)
diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py
new file mode 100644
index 00000000000..1e3e11c8a36
--- /dev/null
+++ b/invokeai/app/services/board_records/board_records_sqlite.py
@@ -0,0 +1,290 @@
+import sqlite3
+from typing import Union, cast
+
+from invokeai.app.services.board_records.board_records_base import BoardRecordStorageBase
+from invokeai.app.services.board_records.board_records_common import (
+ BoardChanges,
+ BoardRecord,
+ BoardRecordDeleteException,
+ BoardRecordNotFoundException,
+ BoardRecordOrderBy,
+ BoardRecordSaveException,
+ deserialize_board_record,
+)
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.util.misc import uuid_string
+
+
+class SqliteBoardRecordStorage(BoardRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def delete(self, board_id: str) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ DELETE FROM boards
+ WHERE board_id = ?;
+ """,
+ (board_id,),
+ )
+ except Exception as e:
+ raise BoardRecordDeleteException from e
+
+ def save(
+ self,
+ board_name: str,
+ user_id: str,
+ ) -> BoardRecord:
+ with self._db.transaction() as cursor:
+ try:
+ board_id = uuid_string()
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO boards (board_id, board_name, user_id)
+ VALUES (?, ?, ?);
+ """,
+ (board_id, board_name, user_id),
+ )
+ except sqlite3.Error as e:
+ raise BoardRecordSaveException from e
+ return self.get(board_id)
+
+ def get(
+ self,
+ board_id: str,
+ ) -> BoardRecord:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ SELECT *
+ FROM boards
+ WHERE board_id = ?;
+ """,
+ (board_id,),
+ )
+
+ result = cast(Union[sqlite3.Row, None], cursor.fetchone())
+ except sqlite3.Error as e:
+ raise BoardRecordNotFoundException from e
+ if result is None:
+ raise BoardRecordNotFoundException
+ return BoardRecord(**dict(result))
+
+ def update(
+ self,
+ board_id: str,
+ changes: BoardChanges,
+ ) -> BoardRecord:
+ with self._db.transaction() as cursor:
+ try:
+ # Change the name of a board
+ if changes.board_name is not None:
+ cursor.execute(
+ """--sql
+ UPDATE boards
+ SET board_name = ?
+ WHERE board_id = ?;
+ """,
+ (changes.board_name, board_id),
+ )
+
+ # Change the cover image of a board
+ if changes.cover_image_name is not None:
+ cursor.execute(
+ """--sql
+ UPDATE boards
+ SET cover_image_name = ?
+ WHERE board_id = ?;
+ """,
+ (changes.cover_image_name, board_id),
+ )
+
+ # Change the archived status of a board
+ if changes.archived is not None:
+ cursor.execute(
+ """--sql
+ UPDATE boards
+ SET archived = ?
+ WHERE board_id = ?;
+ """,
+ (changes.archived, board_id),
+ )
+
+ # Change the visibility of a board
+ if changes.board_visibility is not None:
+ cursor.execute(
+ """--sql
+ UPDATE boards
+ SET board_visibility = ?
+ WHERE board_id = ?;
+ """,
+ (changes.board_visibility.value, board_id),
+ )
+
+ except sqlite3.Error as e:
+ raise BoardRecordSaveException from e
+ return self.get(board_id)
+
+ def get_many(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ offset: int = 0,
+ limit: int = 10,
+ include_archived: bool = False,
+ ) -> OffsetPaginatedResults[BoardRecord]:
+ with self._db.transaction() as cursor:
+ # Build base query - admins see all boards, regular users see owned, shared, or public boards
+ if is_admin:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ {archived_filter}
+ ORDER BY {order_by} {direction}
+ LIMIT ? OFFSET ?;
+ """
+
+ # Determine archived filter condition
+ archived_filter = "WHERE 1=1" if include_archived else "WHERE boards.archived = 0"
+
+ final_query = base_query.format(
+ archived_filter=archived_filter, order_by=order_by.value, direction=direction.value
+ )
+
+ # Execute query to fetch boards
+ cursor.execute(final_query, (limit, offset))
+ else:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ LEFT JOIN shared_boards ON boards.board_id = shared_boards.board_id
+ WHERE (boards.user_id = ? OR shared_boards.user_id = ? OR boards.board_visibility IN ('shared', 'public'))
+ {archived_filter}
+ ORDER BY {order_by} {direction}
+ LIMIT ? OFFSET ?;
+ """
+
+ # Determine archived filter condition
+ archived_filter = "" if include_archived else "AND boards.archived = 0"
+
+ final_query = base_query.format(
+ archived_filter=archived_filter, order_by=order_by.value, direction=direction.value
+ )
+
+ # Execute query to fetch boards
+ cursor.execute(final_query, (user_id, user_id, limit, offset))
+
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ boards = [deserialize_board_record(dict(r)) for r in result]
+
+ # Determine count query - admins count all boards, regular users count accessible boards
+ if is_admin:
+ if include_archived:
+ count_query = """
+ SELECT COUNT(DISTINCT boards.board_id)
+ FROM boards;
+ """
+ else:
+ count_query = """
+ SELECT COUNT(DISTINCT boards.board_id)
+ FROM boards
+ WHERE boards.archived = 0;
+ """
+ cursor.execute(count_query)
+ else:
+ if include_archived:
+ count_query = """
+ SELECT COUNT(DISTINCT boards.board_id)
+ FROM boards
+ LEFT JOIN shared_boards ON boards.board_id = shared_boards.board_id
+ WHERE (boards.user_id = ? OR shared_boards.user_id = ? OR boards.board_visibility IN ('shared', 'public'));
+ """
+ else:
+ count_query = """
+ SELECT COUNT(DISTINCT boards.board_id)
+ FROM boards
+ LEFT JOIN shared_boards ON boards.board_id = shared_boards.board_id
+ WHERE (boards.user_id = ? OR shared_boards.user_id = ? OR boards.board_visibility IN ('shared', 'public'))
+ AND boards.archived = 0;
+ """
+
+ # Execute count query
+ cursor.execute(count_query, (user_id, user_id))
+
+ count = cast(int, cursor.fetchone()[0])
+
+ return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count)
+
+ def get_all(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ include_archived: bool = False,
+ ) -> list[BoardRecord]:
+ with self._db.transaction() as cursor:
+ # Build query - admins see all boards, regular users see owned, shared, or public boards
+ if is_admin:
+ if order_by == BoardRecordOrderBy.Name:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ {archived_filter}
+ ORDER BY LOWER(boards.board_name) {direction}
+ """
+ else:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ {archived_filter}
+ ORDER BY {order_by} {direction}
+ """
+
+ archived_filter = "WHERE 1=1" if include_archived else "WHERE boards.archived = 0"
+
+ final_query = base_query.format(
+ archived_filter=archived_filter, order_by=order_by.value, direction=direction.value
+ )
+
+ cursor.execute(final_query)
+ else:
+ if order_by == BoardRecordOrderBy.Name:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ LEFT JOIN shared_boards ON boards.board_id = shared_boards.board_id
+ WHERE (boards.user_id = ? OR shared_boards.user_id = ? OR boards.board_visibility IN ('shared', 'public'))
+ {archived_filter}
+ ORDER BY LOWER(boards.board_name) {direction}
+ """
+ else:
+ base_query = """
+ SELECT DISTINCT boards.*
+ FROM boards
+ LEFT JOIN shared_boards ON boards.board_id = shared_boards.board_id
+ WHERE (boards.user_id = ? OR shared_boards.user_id = ? OR boards.board_visibility IN ('shared', 'public'))
+ {archived_filter}
+ ORDER BY {order_by} {direction}
+ """
+
+ archived_filter = "" if include_archived else "AND boards.archived = 0"
+
+ final_query = base_query.format(
+ archived_filter=archived_filter, order_by=order_by.value, direction=direction.value
+ )
+
+ cursor.execute(final_query, (user_id, user_id))
+
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ boards = [deserialize_board_record(dict(r)) for r in result]
+
+ return boards
diff --git a/invokeai/app/services/boards/__init__.py b/invokeai/app/services/boards/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/boards/boards_base.py b/invokeai/app/services/boards/boards_base.py
new file mode 100644
index 00000000000..914dfa3d0d7
--- /dev/null
+++ b/invokeai/app/services/boards/boards_base.py
@@ -0,0 +1,70 @@
+from abc import ABC, abstractmethod
+
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
+from invokeai.app.services.boards.boards_common import BoardDTO
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class BoardServiceABC(ABC):
+ """High-level service for board management."""
+
+ @abstractmethod
+ def create(
+ self,
+ board_name: str,
+ user_id: str,
+ ) -> BoardDTO:
+ """Creates a board for a specific user."""
+ pass
+
+ @abstractmethod
+ def get_dto(
+ self,
+ board_id: str,
+ ) -> BoardDTO:
+ """Gets a board."""
+ pass
+
+ @abstractmethod
+ def update(
+ self,
+ board_id: str,
+ changes: BoardChanges,
+ ) -> BoardDTO:
+ """Updates a board."""
+ pass
+
+ @abstractmethod
+ def delete(
+ self,
+ board_id: str,
+ ) -> None:
+ """Deletes a board."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ offset: int = 0,
+ limit: int = 10,
+ include_archived: bool = False,
+ ) -> OffsetPaginatedResults[BoardDTO]:
+ """Gets many boards for a specific user, including shared boards. Admin users see all boards."""
+ pass
+
+ @abstractmethod
+ def get_all(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ include_archived: bool = False,
+ ) -> list[BoardDTO]:
+ """Gets all boards for a specific user, including shared boards. Admin users see all boards."""
+ pass
diff --git a/invokeai/app/services/boards/boards_common.py b/invokeai/app/services/boards/boards_common.py
new file mode 100644
index 00000000000..99952fec134
--- /dev/null
+++ b/invokeai/app/services/boards/boards_common.py
@@ -0,0 +1,35 @@
+from typing import Optional
+
+from pydantic import Field
+
+from invokeai.app.services.board_records.board_records_common import BoardRecord
+
+
+class BoardDTO(BoardRecord):
+ """Deserialized board record with cover image URL and image count."""
+
+ cover_image_name: Optional[str] = Field(description="The name of the board's cover image.")
+ """The URL of the thumbnail of the most recent image in the board."""
+ image_count: int = Field(description="The number of images in the board.")
+ """The number of images in the board."""
+ asset_count: int = Field(description="The number of assets in the board.")
+ """The number of assets in the board."""
+ owner_username: Optional[str] = Field(default=None, description="The username of the board owner (for admin view).")
+ """The username of the board owner (for admin view)."""
+
+
+def board_record_to_dto(
+ board_record: BoardRecord,
+ cover_image_name: Optional[str],
+ image_count: int,
+ asset_count: int,
+ owner_username: Optional[str] = None,
+) -> BoardDTO:
+ """Converts a board record to a board DTO."""
+ return BoardDTO(
+ **board_record.model_dump(exclude={"cover_image_name"}),
+ cover_image_name=cover_image_name,
+ image_count=image_count,
+ asset_count=asset_count,
+ owner_username=owner_username,
+ )
diff --git a/invokeai/app/services/boards/boards_default.py b/invokeai/app/services/boards/boards_default.py
new file mode 100644
index 00000000000..71465815ef9
--- /dev/null
+++ b/invokeai/app/services/boards/boards_default.py
@@ -0,0 +1,119 @@
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
+from invokeai.app.services.boards.boards_base import BoardServiceABC
+from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class BoardService(BoardServiceABC):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def create(
+ self,
+ board_name: str,
+ user_id: str,
+ ) -> BoardDTO:
+ board_record = self.__invoker.services.board_records.save(board_name, user_id)
+ return board_record_to_dto(board_record, None, 0, 0)
+
+ def get_dto(self, board_id: str) -> BoardDTO:
+ board_record = self.__invoker.services.board_records.get(board_id)
+ cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
+ if cover_image:
+ cover_image_name = cover_image.image_name
+ else:
+ cover_image_name = None
+ image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
+ asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
+ return board_record_to_dto(board_record, cover_image_name, image_count, asset_count)
+
+ def update(
+ self,
+ board_id: str,
+ changes: BoardChanges,
+ ) -> BoardDTO:
+ board_record = self.__invoker.services.board_records.update(board_id, changes)
+ cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
+ if cover_image:
+ cover_image_name = cover_image.image_name
+ else:
+ cover_image_name = None
+
+ image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
+ asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(board_id)
+ return board_record_to_dto(board_record, cover_image_name, image_count, asset_count)
+
+ def delete(self, board_id: str) -> None:
+ self.__invoker.services.board_records.delete(board_id)
+
+ def get_many(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ offset: int = 0,
+ limit: int = 10,
+ include_archived: bool = False,
+ ) -> OffsetPaginatedResults[BoardDTO]:
+ board_records = self.__invoker.services.board_records.get_many(
+ user_id, is_admin, order_by, direction, offset, limit, include_archived
+ )
+ board_dtos = []
+ for r in board_records.items:
+ cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
+ if cover_image:
+ cover_image_name = cover_image.image_name
+ else:
+ cover_image_name = None
+
+ image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
+ asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
+
+ # For admin users, include owner username
+ owner_username = None
+ if is_admin:
+ owner = self.__invoker.services.users.get(r.user_id)
+ if owner:
+ owner_username = owner.display_name or owner.email
+
+ board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, owner_username))
+
+ return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
+
+ def get_all(
+ self,
+ user_id: str,
+ is_admin: bool,
+ order_by: BoardRecordOrderBy,
+ direction: SQLiteDirection,
+ include_archived: bool = False,
+ ) -> list[BoardDTO]:
+ board_records = self.__invoker.services.board_records.get_all(
+ user_id, is_admin, order_by, direction, include_archived
+ )
+ board_dtos = []
+ for r in board_records:
+ cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
+ if cover_image:
+ cover_image_name = cover_image.image_name
+ else:
+ cover_image_name = None
+
+ image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
+ asset_count = self.__invoker.services.board_image_records.get_asset_count_for_board(r.board_id)
+
+ # For admin users, include owner username
+ owner_username = None
+ if is_admin:
+ owner = self.__invoker.services.users.get(r.user_id)
+ if owner:
+ owner_username = owner.display_name or owner.email
+
+ board_dtos.append(board_record_to_dto(r, cover_image_name, image_count, asset_count, owner_username))
+
+ return board_dtos
diff --git a/invokeai/app/services/bulk_download/__init__.py b/invokeai/app/services/bulk_download/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/bulk_download/bulk_download_base.py b/invokeai/app/services/bulk_download/bulk_download_base.py
new file mode 100644
index 00000000000..6cd4ed0cbaf
--- /dev/null
+++ b/invokeai/app/services/bulk_download/bulk_download_base.py
@@ -0,0 +1,58 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+
+class BulkDownloadBase(ABC):
+ """Responsible for creating a zip file containing the images specified by the given image names or board id."""
+
+ @abstractmethod
+ def handler(
+ self,
+ image_names: Optional[list[str]],
+ board_id: Optional[str],
+ bulk_download_item_id: Optional[str],
+ user_id: str = "system",
+ ) -> None:
+ """
+ Create a zip file containing the images specified by the given image names or board id.
+
+ :param image_names: A list of image names to include in the zip file.
+ :param board_id: The ID of the board. If provided, all images associated with the board will be included in the zip file.
+ :param bulk_download_item_id: The bulk_download_item_id that will be used to retrieve the bulk download item when it is prepared, if none is provided a uuid will be generated.
+ :param user_id: The ID of the user who initiated the download.
+ """
+
+ @abstractmethod
+ def get_path(self, bulk_download_item_name: str) -> str:
+ """
+ Get the path to the bulk download file.
+
+ :param bulk_download_item_name: The name of the bulk download item.
+ :return: The path to the bulk download file.
+ """
+
+ @abstractmethod
+ def generate_item_id(self, board_id: Optional[str]) -> str:
+ """
+ Generate an item ID for a bulk download item.
+
+ :param board_id: The ID of the board whose name is to be included in the item id.
+ :return: The generated item ID.
+ """
+
+ @abstractmethod
+ def delete(self, bulk_download_item_name: str) -> None:
+ """
+ Delete the bulk download file.
+
+ :param bulk_download_item_name: The name of the bulk download item.
+ """
+
+ @abstractmethod
+ def get_owner(self, bulk_download_item_name: str) -> Optional[str]:
+ """
+ Get the user_id of the user who initiated the download.
+
+ :param bulk_download_item_name: The name of the bulk download item.
+ :return: The user_id of the owner, or None if not tracked.
+ """
diff --git a/invokeai/app/services/bulk_download/bulk_download_common.py b/invokeai/app/services/bulk_download/bulk_download_common.py
new file mode 100644
index 00000000000..68724eb228b
--- /dev/null
+++ b/invokeai/app/services/bulk_download/bulk_download_common.py
@@ -0,0 +1,25 @@
+DEFAULT_BULK_DOWNLOAD_ID = "default"
+
+
+class BulkDownloadException(Exception):
+ """Exception raised when a bulk download fails."""
+
+ def __init__(self, message="Bulk download failed"):
+ super().__init__(message)
+ self.message = message
+
+
+class BulkDownloadTargetException(BulkDownloadException):
+ """Exception raised when a bulk download target is not found."""
+
+ def __init__(self, message="The bulk download target was not found"):
+ super().__init__(message)
+ self.message = message
+
+
+class BulkDownloadParametersException(BulkDownloadException):
+ """Exception raised when a bulk download parameter is invalid."""
+
+ def __init__(self, message="No image names or board ID provided"):
+ super().__init__(message)
+ self.message = message
diff --git a/invokeai/app/services/bulk_download/bulk_download_default.py b/invokeai/app/services/bulk_download/bulk_download_default.py
new file mode 100644
index 00000000000..c037e9c5c15
--- /dev/null
+++ b/invokeai/app/services/bulk_download/bulk_download_default.py
@@ -0,0 +1,190 @@
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Optional, Union
+from zipfile import ZipFile
+
+from invokeai.app.services.board_records.board_records_common import BoardRecordNotFoundException
+from invokeai.app.services.bulk_download.bulk_download_base import BulkDownloadBase
+from invokeai.app.services.bulk_download.bulk_download_common import (
+ DEFAULT_BULK_DOWNLOAD_ID,
+ BulkDownloadException,
+ BulkDownloadParametersException,
+ BulkDownloadTargetException,
+)
+from invokeai.app.services.image_records.image_records_common import ImageRecordNotFoundException
+from invokeai.app.services.images.images_common import ImageDTO
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.util.misc import uuid_string
+
+
+class BulkDownloadService(BulkDownloadBase):
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def __init__(self):
+ self._temp_directory = TemporaryDirectory()
+ self._bulk_downloads_folder = Path(self._temp_directory.name) / "bulk_downloads"
+ self._bulk_downloads_folder.mkdir(parents=True, exist_ok=True)
+ # Track which user owns each download so the fetch endpoint can enforce ownership
+ self._download_owners: dict[str, str] = {}
+
+ def handler(
+ self,
+ image_names: Optional[list[str]],
+ board_id: Optional[str],
+ bulk_download_item_id: Optional[str],
+ user_id: str = "system",
+ ) -> None:
+ bulk_download_id: str = DEFAULT_BULK_DOWNLOAD_ID
+ bulk_download_item_id = bulk_download_item_id or uuid_string()
+ bulk_download_item_name = bulk_download_item_id + ".zip"
+
+ # Record ownership so the fetch endpoint can verify the caller
+ self._download_owners[bulk_download_item_name] = user_id
+
+ self._signal_job_started(bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id)
+
+ try:
+ image_dtos: list[ImageDTO] = []
+
+ if board_id:
+ image_dtos = self._board_handler(board_id)
+ elif image_names:
+ image_dtos = self._image_handler(image_names)
+ else:
+ raise BulkDownloadParametersException()
+
+ bulk_download_item_name: str = self._create_zip_file(image_dtos, bulk_download_item_id)
+ self._signal_job_completed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id)
+ except (
+ ImageRecordNotFoundException,
+ BoardRecordNotFoundException,
+ BulkDownloadException,
+ BulkDownloadParametersException,
+ ) as e:
+ self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e, user_id)
+ except Exception as e:
+ self._signal_job_failed(bulk_download_id, bulk_download_item_id, bulk_download_item_name, e, user_id)
+ self._invoker.services.logger.error("Problem bulk downloading images.")
+ raise e
+
+ def _image_handler(self, image_names: list[str]) -> list[ImageDTO]:
+ return [self._invoker.services.images.get_dto(image_name) for image_name in image_names]
+
+ def _board_handler(self, board_id: str) -> list[ImageDTO]:
+ image_names = self._invoker.services.board_image_records.get_all_board_image_names_for_board(
+ board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+ return self._image_handler(image_names)
+
+ def generate_item_id(self, board_id: Optional[str]) -> str:
+ return uuid_string() if board_id is None else self._get_clean_board_name(board_id) + "_" + uuid_string()
+
+ def _get_clean_board_name(self, board_id: str) -> str:
+ if board_id == "none":
+ return "Uncategorized"
+
+ return self._clean_string_to_path_safe(self._invoker.services.board_records.get(board_id).board_name)
+
+ def _create_zip_file(self, image_dtos: list[ImageDTO], bulk_download_item_id: str) -> str:
+ """
+ Create a zip file containing the images specified by the given image names or board id.
+ If download with the same bulk_download_id already exists, it will be overwritten.
+
+ :return: The name of the zip file.
+ """
+ zip_file_name = bulk_download_item_id + ".zip"
+ zip_file_path = self._bulk_downloads_folder / (zip_file_name)
+
+ with ZipFile(zip_file_path, "w") as zip_file:
+ for image_dto in image_dtos:
+ image_zip_path = Path(image_dto.image_category.value) / image_dto.image_name
+ image_disk_path = self._invoker.services.images.get_path(image_dto.image_name)
+ zip_file.write(image_disk_path, arcname=image_zip_path)
+
+ return str(zip_file_name)
+
+ # from https://stackoverflow.com/questions/7406102/create-sane-safe-filename-from-any-unsafe-string
+ def _clean_string_to_path_safe(self, s: str) -> str:
+ """Clean a string to be path safe."""
+ return "".join([c for c in s if c.isalpha() or c.isdigit() or c == " " or c == "_" or c == "-"]).rstrip()
+
+ def _signal_job_started(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> None:
+ """Signal that a bulk download job has started."""
+ if self._invoker:
+ assert bulk_download_id is not None
+ self._invoker.services.events.emit_bulk_download_started(
+ bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id=user_id
+ )
+
+ def _signal_job_completed(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> None:
+ """Signal that a bulk download job has completed."""
+ if self._invoker:
+ assert bulk_download_id is not None
+ assert bulk_download_item_name is not None
+ self._invoker.services.events.emit_bulk_download_complete(
+ bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id=user_id
+ )
+
+ def _signal_job_failed(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ exception: Exception,
+ user_id: str = "system",
+ ) -> None:
+ """Signal that a bulk download job has failed."""
+ if self._invoker:
+ assert bulk_download_id is not None
+ assert exception is not None
+ self._invoker.services.events.emit_bulk_download_error(
+ bulk_download_id, bulk_download_item_id, bulk_download_item_name, str(exception), user_id=user_id
+ )
+
+ def stop(self, *args, **kwargs):
+ self._temp_directory.cleanup()
+
+ def get_owner(self, bulk_download_item_name: str) -> Optional[str]:
+ return self._download_owners.get(bulk_download_item_name)
+
+ def delete(self, bulk_download_item_name: str) -> None:
+ path = self.get_path(bulk_download_item_name)
+ Path(path).unlink()
+ self._download_owners.pop(bulk_download_item_name, None)
+
+ def get_path(self, bulk_download_item_name: str) -> str:
+ path = str(self._bulk_downloads_folder / bulk_download_item_name)
+ if not self._is_valid_path(path):
+ raise BulkDownloadTargetException()
+ return path
+
+ def _is_valid_path(self, path: Union[str, Path]) -> bool:
+ """Validates the path given for a bulk download."""
+ path = path if isinstance(path, Path) else Path(path)
+
+ # Resolve the path to handle any path traversal attempts (e.g., ../)
+ resolved_path = path.resolve()
+
+ # The path may not traverse out of the bulk downloads folder or its subfolders
+ does_not_traverse = resolved_path.parent == self._bulk_downloads_folder.resolve()
+
+ # The path must exist and be a .zip file
+ does_exist = resolved_path.exists()
+ is_zip_file = resolved_path.suffix == ".zip"
+
+ return does_exist and is_zip_file and does_not_traverse
diff --git a/invokeai/app/services/client_state_persistence/client_state_persistence_base.py b/invokeai/app/services/client_state_persistence/client_state_persistence_base.py
new file mode 100644
index 00000000000..7be6841a790
--- /dev/null
+++ b/invokeai/app/services/client_state_persistence/client_state_persistence_base.py
@@ -0,0 +1,72 @@
+from abc import ABC, abstractmethod
+
+
+class ClientStatePersistenceABC(ABC):
+ """
+ Base class for client persistence implementations.
+ This class defines the interface for persisting client data per user.
+ """
+
+ @abstractmethod
+ def set_by_key(self, user_id: str, key: str, value: str) -> str:
+ """
+ Set a key-value pair for the client.
+
+ Args:
+ user_id (str): The user ID to set state for.
+ key (str): The key to set.
+ value (str): The value to set for the key.
+
+ Returns:
+ str: The value that was set.
+ """
+ pass
+
+ @abstractmethod
+ def get_by_key(self, user_id: str, key: str) -> str | None:
+ """
+ Get the value for a specific key of the client.
+
+ Args:
+ user_id (str): The user ID to get state for.
+ key (str): The key to retrieve the value for.
+
+ Returns:
+ str | None: The value associated with the key, or None if the key does not exist.
+ """
+ pass
+
+ @abstractmethod
+ def get_keys_by_prefix(self, user_id: str, prefix: str) -> list[str]:
+ """
+ Get all keys matching a prefix for a user.
+
+ Args:
+ user_id (str): The user ID to get keys for.
+ prefix (str): The prefix to filter keys by.
+
+ Returns:
+ list[str]: A list of keys matching the prefix.
+ """
+ pass
+
+ @abstractmethod
+ def delete_by_key(self, user_id: str, key: str) -> None:
+ """
+ Delete a specific key-value pair for a user.
+
+ Args:
+ user_id (str): The user ID to delete state for.
+ key (str): The key to delete.
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, user_id: str) -> None:
+ """
+ Delete all client state for a user.
+
+ Args:
+ user_id (str): The user ID to delete state for.
+ """
+ pass
diff --git a/invokeai/app/services/client_state_persistence/client_state_persistence_sqlite.py b/invokeai/app/services/client_state_persistence/client_state_persistence_sqlite.py
new file mode 100644
index 00000000000..7605de829d9
--- /dev/null
+++ b/invokeai/app/services/client_state_persistence/client_state_persistence_sqlite.py
@@ -0,0 +1,80 @@
+from invokeai.app.services.client_state_persistence.client_state_persistence_base import ClientStatePersistenceABC
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class ClientStatePersistenceSqlite(ClientStatePersistenceABC):
+ """
+ SQLite implementation for client state persistence.
+ This class stores client state data per user to prevent data leakage between users.
+ """
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def set_by_key(self, user_id: str, key: str, value: str) -> str:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ INSERT INTO client_state (user_id, key, value)
+ VALUES (?, ?, ?)
+ ON CONFLICT(user_id, key) DO UPDATE
+ SET value = excluded.value;
+ """,
+ (user_id, key, value),
+ )
+
+ return value
+
+ def get_by_key(self, user_id: str, key: str) -> str | None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT value FROM client_state
+ WHERE user_id = ? AND key = ?
+ """,
+ (user_id, key),
+ )
+ row = cursor.fetchone()
+ if row is None:
+ return None
+ return row[0]
+
+ def get_keys_by_prefix(self, user_id: str, prefix: str) -> list[str]:
+ # Escape LIKE wildcards (%, _) and the escape char itself so callers can pass
+ # arbitrary strings as a literal prefix without accidental pattern matching.
+ escaped_prefix = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT key FROM client_state
+ WHERE user_id = ? AND key LIKE ? ESCAPE '\\'
+ ORDER BY updated_at DESC
+ """,
+ (user_id, f"{escaped_prefix}%"),
+ )
+ return [row[0] for row in cursor.fetchall()]
+
+ def delete_by_key(self, user_id: str, key: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ DELETE FROM client_state
+ WHERE user_id = ? AND key = ?
+ """,
+ (user_id, key),
+ )
+
+ def delete(self, user_id: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ DELETE FROM client_state
+ WHERE user_id = ?
+ """,
+ (user_id,),
+ )
diff --git a/invokeai/app/services/config/__init__.py b/invokeai/app/services/config/__init__.py
new file mode 100644
index 00000000000..df1acbf1047
--- /dev/null
+++ b/invokeai/app/services/config/__init__.py
@@ -0,0 +1,6 @@
+"""Init file for InvokeAI configure package."""
+
+from invokeai.app.services.config.config_common import PagingArgumentParser
+from invokeai.app.services.config.config_default import InvokeAIAppConfig, get_config
+
+__all__ = ["InvokeAIAppConfig", "get_config", "PagingArgumentParser"]
diff --git a/invokeai/app/services/config/config_common.py b/invokeai/app/services/config/config_common.py
new file mode 100644
index 00000000000..0765b93f2cf
--- /dev/null
+++ b/invokeai/app/services/config/config_common.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2023 Lincoln Stein (https://github.com/lstein) and the InvokeAI Development Team
+
+"""
+Base class for the InvokeAI configuration system.
+It defines a type of pydantic BaseSettings object that
+is able to read and write from an omegaconf-based config file,
+with overriding of settings from environment variables and/or
+the command line.
+"""
+
+from __future__ import annotations
+
+import argparse
+import pydoc
+
+
+class PagingArgumentParser(argparse.ArgumentParser):
+ """
+ A custom ArgumentParser that uses pydoc to page its output.
+ It also supports reading defaults from an init file.
+ """
+
+ def print_help(self, file=None) -> None:
+ text = self.format_help()
+ pydoc.pager(text)
diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py
new file mode 100644
index 00000000000..e6cc7c2798c
--- /dev/null
+++ b/invokeai/app/services/config/config_default.py
@@ -0,0 +1,688 @@
+# TODO(psyche): pydantic-settings supports YAML settings sources. If we can figure out a way to integrate the YAML
+# migration logic, we could use that for simpler config loading.
+
+from __future__ import annotations
+
+import copy
+import filecmp
+import locale
+import os
+import re
+import shutil
+from functools import lru_cache
+from pathlib import Path
+from typing import Any, Literal, Optional
+
+import yaml
+from pydantic import BaseModel, Field, PrivateAttr, field_validator
+from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict
+
+import invokeai.configs as model_configs
+from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS
+from invokeai.frontend.cli.arg_parser import InvokeAIArgs
+
+INIT_FILE = Path("invokeai.yaml")
+API_KEYS_FILE = Path("api_keys.yaml")
+DB_FILE = Path("invokeai.db")
+LEGACY_INIT_FILE = Path("invokeai.init")
+PRECISION = Literal["auto", "float16", "bfloat16", "float32"]
+ATTENTION_TYPE = Literal["auto", "normal", "xformers", "sliced", "torch-sdp"]
+ATTENTION_SLICE_SIZE = Literal["auto", "balanced", "max", 1, 2, 3, 4, 5, 6, 7, 8]
+LOG_FORMAT = Literal["plain", "color", "syslog", "legacy"]
+LOG_LEVEL = Literal["debug", "info", "warning", "error", "critical"]
+IMAGE_SUBFOLDER_STRATEGY = Literal["flat", "date", "type", "hash"]
+CONFIG_SCHEMA_VERSION = "4.0.3"
+EXTERNAL_PROVIDER_CONFIG_FIELDS = (
+ "external_alibabacloud_api_key",
+ "external_alibabacloud_base_url",
+ "external_gemini_api_key",
+ "external_gemini_base_url",
+ "external_openai_api_key",
+ "external_openai_base_url",
+ "external_seedream_api_key",
+ "external_seedream_base_url",
+)
+
+
+class URLRegexTokenPair(BaseModel):
+ url_regex: str = Field(description="Regular expression to match against the URL")
+ token: str = Field(description="Token to use when the URL matches the regex")
+
+ @field_validator("url_regex")
+ @classmethod
+ def validate_url_regex(cls, v: str) -> str:
+ """Validate that the value is a valid regex."""
+ try:
+ re.compile(v)
+ except re.error as e:
+ raise ValueError(f"Invalid regex: {e}")
+ return v
+
+
+class InvokeAIAppConfig(BaseSettings):
+ """Invoke's global app configuration.
+
+ Typically, you won't need to interact with this class directly. Instead, use the `get_config` function from `invokeai.app.services.config` to get a singleton config object.
+
+ Attributes:
+ host: IP address to bind to. Use `0.0.0.0` to serve to your local network.
+ port: Port to bind to.
+ allow_origins: Allowed CORS origins.
+ allow_credentials: Allow CORS credentials.
+ allow_methods: Methods allowed for CORS.
+ allow_headers: Headers allowed for CORS.
+ ssl_certfile: SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.
+ ssl_keyfile: SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.
+ log_tokenization: Enable logging of parsed prompt tokens.
+ patchmatch: Enable patchmatch inpaint code.
+ models_dir: Path to the models directory.
+ convert_cache_dir: Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).
+ download_cache_dir: Path to the directory that contains dynamically downloaded models.
+ legacy_conf_dir: Path to directory of legacy checkpoint config files.
+ db_dir: Path to InvokeAI databases directory.
+ outputs_dir: Path to directory for outputs.
+ image_subfolder_strategy: Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance. Valid values: `flat`, `date`, `type`, `hash`
+ custom_nodes_dir: Path to directory for custom nodes.
+ style_presets_dir: Path to directory for style presets.
+ workflow_thumbnails_dir: Path to directory for workflow thumbnails.
+ log_handlers: Log handler. Valid options are "console", "file=", "syslog=path|address:host:port", "http=".
+ log_format: Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style. Valid values: `plain`, `color`, `syslog`, `legacy`
+ log_level: Emit logging messages at this level or higher. Valid values: `debug`, `info`, `warning`, `error`, `critical`
+ log_sql: Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.
+ log_level_network: Log level for network-related messages. 'info' and 'debug' are very verbose. Valid values: `debug`, `info`, `warning`, `error`, `critical`
+ use_memory_db: Use in-memory database. Useful for development.
+ dev_reload: Automatically reload when Python sources are changed. Does not reload node definitions.
+ profile_graphs: Enable graph profiling using `cProfile`.
+ profile_prefix: An optional prefix for profile output files.
+ profiles_dir: Path to profiles output directory.
+ max_cache_ram_gb: The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.
+ max_cache_vram_gb: The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.
+ log_memory_usage: If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.
+ model_cache_keep_alive_min: How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.
+ device_working_mem_gb: The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.
+ enable_partial_loading: Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.
+ keep_ram_copy_of_weights: Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.
+ ram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.
+ vram: DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.
+ lazy_offload: DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.
+ pytorch_cuda_alloc_conf: Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to "backend:cudaMallocAsync" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.
+ device: Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)
+ precision: Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system. Valid values: `auto`, `float16`, `bfloat16`, `float32`
+ sequential_guidance: Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.
+ attention_type: Attention type. Valid values: `auto`, `normal`, `xformers`, `sliced`, `torch-sdp`
+ attention_slice_size: Slice size, valid when attention_type=="sliced". Valid values: `auto`, `balanced`, `max`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`
+ force_tiled_decode: Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).
+ pil_compress_level: The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.
+ max_queue_size: Maximum number of items in the session queue.
+ clear_queue_on_startup: Empties session queue on startup. If true, disables `max_queue_history`.
+ max_queue_history: Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.
+ allow_nodes: List of nodes to allow. Omit to allow all.
+ deny_nodes: List of nodes to deny. Omit to deny none.
+ node_cache_size: How many cached nodes to keep in memory.
+ hashing_algorithm: Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3. Valid values: `blake3_multi`, `blake3_single`, `random`, `md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `blake2b`, `blake2s`, `sha3_224`, `sha3_256`, `sha3_384`, `sha3_512`, `shake_128`, `shake_256`
+ remote_api_tokens: List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.
+ scan_models_on_startup: Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.
+ unsafe_disable_picklescan: UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.
+ allow_unknown_models: Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.
+ multiuser: Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.
+ strict_password_checking: Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.
+ external_alibabacloud_api_key: API key for Alibaba Cloud DashScope image generation.
+ external_alibabacloud_base_url: Base URL override for Alibaba Cloud DashScope image generation.
+ external_gemini_api_key: API key for Gemini image generation.
+ external_openai_api_key: API key for OpenAI image generation.
+ external_gemini_base_url: Base URL override for Gemini image generation.
+ external_openai_base_url: Base URL override for OpenAI image generation.
+ external_seedream_api_key: API key for Seedream image generation.
+ external_seedream_base_url: Base URL override for Seedream image generation.
+ """
+
+ _root: Optional[Path] = PrivateAttr(default=None)
+ _config_file: Optional[Path] = PrivateAttr(default=None)
+
+ # fmt: off
+
+ # INTERNAL
+ schema_version: str = Field(default=CONFIG_SCHEMA_VERSION, description="Schema version of the config file. This is not a user-configurable setting.")
+ # This is only used during v3 models.yaml migration
+ legacy_models_yaml_path: Optional[Path] = Field(default=None, description="Path to the legacy models.yaml file. This is not a user-configurable setting.")
+
+ # WEB
+ host: str = Field(default="127.0.0.1", description="IP address to bind to. Use `0.0.0.0` to serve to your local network.")
+ port: int = Field(default=9090, description="Port to bind to.")
+ allow_origins: list[str] = Field(default=[], description="Allowed CORS origins.")
+ allow_credentials: bool = Field(default=True, description="Allow CORS credentials.")
+ allow_methods: list[str] = Field(default=["*"], description="Methods allowed for CORS.")
+ allow_headers: list[str] = Field(default=["*"], description="Headers allowed for CORS.")
+ ssl_certfile: Optional[Path] = Field(default=None, description="SSL certificate file for HTTPS. See https://www.uvicorn.dev/settings/#https.")
+ ssl_keyfile: Optional[Path] = Field(default=None, description="SSL key file for HTTPS. See https://www.uvicorn.dev/settings/#https.")
+
+ # MISC FEATURES
+ log_tokenization: bool = Field(default=False, description="Enable logging of parsed prompt tokens.")
+ patchmatch: bool = Field(default=True, description="Enable patchmatch inpaint code.")
+
+ # PATHS
+ models_dir: Path = Field(default=Path("models"), description="Path to the models directory.")
+ convert_cache_dir: Path = Field(default=Path("models/.convert_cache"), description="Path to the converted models cache directory (DEPRECATED, but do not delete because it is needed for migration from previous versions).")
+ download_cache_dir: Path = Field(default=Path("models/.download_cache"), description="Path to the directory that contains dynamically downloaded models.")
+ legacy_conf_dir: Path = Field(default=Path("configs"), description="Path to directory of legacy checkpoint config files.")
+ db_dir: Path = Field(default=Path("databases"), description="Path to InvokeAI databases directory.")
+ outputs_dir: Path = Field(default=Path("outputs"), description="Path to directory for outputs.")
+ image_subfolder_strategy: IMAGE_SUBFOLDER_STRATEGY = Field(default="flat", description="Strategy for organizing images into subfolders. 'flat' stores all images in a single folder. 'date' organizes by YYYY/MM/DD. 'type' organizes by image category. 'hash' uses first 2 characters of UUID for filesystem performance.")
+ custom_nodes_dir: Path = Field(default=Path("nodes"), description="Path to directory for custom nodes.")
+ style_presets_dir: Path = Field(default=Path("style_presets"), description="Path to directory for style presets.")
+ workflow_thumbnails_dir: Path = Field(default=Path("workflow_thumbnails"), description="Path to directory for workflow thumbnails.")
+
+ # LOGGING
+ log_handlers: list[str] = Field(default=["console"], description='Log handler. Valid options are "console", "file=", "syslog=path|address:host:port", "http=".')
+ # note - would be better to read the log_format values from logging.py, but this creates circular dependencies issues
+ log_format: LOG_FORMAT = Field(default="color", description='Log format. Use "plain" for text-only, "color" for colorized output, "legacy" for 2.3-style logging and "syslog" for syslog-style.')
+ log_level: LOG_LEVEL = Field(default="info", description="Emit logging messages at this level or higher.")
+ log_sql: bool = Field(default=False, description="Log SQL queries. `log_level` must be `debug` for this to do anything. Extremely verbose.")
+ log_level_network: LOG_LEVEL = Field(default='warning', description="Log level for network-related messages. 'info' and 'debug' are very verbose.")
+
+ # Development
+ use_memory_db: bool = Field(default=False, description="Use in-memory database. Useful for development.")
+ dev_reload: bool = Field(default=False, description="Automatically reload when Python sources are changed. Does not reload node definitions.")
+ profile_graphs: bool = Field(default=False, description="Enable graph profiling using `cProfile`.")
+ profile_prefix: Optional[str] = Field(default=None, description="An optional prefix for profile output files.")
+ profiles_dir: Path = Field(default=Path("profiles"), description="Path to profiles output directory.")
+
+ # CACHE
+ max_cache_ram_gb: Optional[float] = Field(default=None, gt=0, description="The maximum amount of CPU RAM to use for model caching in GB. If unset, the limit will be configured based on the available RAM. In most cases, it is recommended to leave this unset.")
+ max_cache_vram_gb: Optional[float] = Field(default=None, ge=0, description="The amount of VRAM to use for model caching in GB. If unset, the limit will be configured based on the available VRAM and the device_working_mem_gb. In most cases, it is recommended to leave this unset.")
+ log_memory_usage: bool = Field(default=False, description="If True, a memory snapshot will be captured before and after every model cache operation, and the result will be logged (at debug level). There is a time cost to capturing the memory snapshots, so it is recommended to only enable this feature if you are actively inspecting the model cache's behaviour.")
+ model_cache_keep_alive_min: float = Field(default=0, ge=0, description="How long to keep models in cache after last use, in minutes. A value of 0 (the default) means models are kept in cache indefinitely. If no model generations occur within the timeout period, the model cache is cleared using the same logic as the 'Clear Model Cache' button.")
+ device_working_mem_gb: float = Field(default=3, description="The amount of working memory to keep available on the compute device (in GB). Has no effect if running on CPU. If you are experiencing OOM errors, try increasing this value.")
+ enable_partial_loading: bool = Field(default=True, description="Enable partial loading of models. This enables models to run with reduced VRAM requirements (at the cost of slower speed) by streaming the model from RAM to VRAM as its used. In some edge cases, partial loading can cause models to run more slowly if they were previously being fully loaded into VRAM.")
+ keep_ram_copy_of_weights: bool = Field(default=True, description="Whether to keep a full RAM copy of a model's weights when the model is loaded in VRAM. Keeping a RAM copy increases average RAM usage, but speeds up model switching and LoRA patching (assuming there is sufficient RAM). Set this to False if RAM pressure is consistently high.")
+ # Deprecated CACHE configs
+ ram: Optional[float] = Field(default=None, gt=0, description="DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_ram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.")
+ vram: Optional[float] = Field(default=None, ge=0, description="DEPRECATED: This setting is no longer used. It has been replaced by `max_cache_vram_gb`, but most users will not need to use this config since automatic cache size limits should work well in most cases. This config setting will be removed once the new model cache behavior is stable.")
+ lazy_offload: bool = Field(default=True, description="DEPRECATED: This setting is no longer used. Lazy-offloading is enabled by default. This config setting will be removed once the new model cache behavior is stable.")
+
+ # PyTorch Memory Allocator
+ pytorch_cuda_alloc_conf: Optional[str] = Field(default=None, description="Configure the Torch CUDA memory allocator. This will impact peak reserved VRAM usage and performance. Setting to \"backend:cudaMallocAsync\" works well on many systems. The optimal configuration is highly dependent on the system configuration (device type, VRAM, CUDA driver version, etc.), so must be tuned experimentally.")
+
+ # DEVICE
+ device: str = Field(default="auto", description="Preferred execution device. `auto` will choose the device depending on the hardware platform and the installed torch capabilities. Valid values: `auto`, `cpu`, `cuda`, `mps`, `cuda:N` (where N is a device number)", pattern=r"^(auto|cpu|mps|cuda(:\d+)?)$")
+ precision: PRECISION = Field(default="auto", description="Floating point precision. `float16` will consume half the memory of `float32` but produce slightly lower-quality images. The `auto` setting will guess the proper precision based on your video card and operating system.")
+
+ # GENERATION
+ sequential_guidance: bool = Field(default=False, description="Whether to calculate guidance in serial instead of in parallel, lowering memory requirements.")
+ attention_type: ATTENTION_TYPE = Field(default="auto", description="Attention type.")
+ attention_slice_size: ATTENTION_SLICE_SIZE = Field(default="auto", description='Slice size, valid when attention_type=="sliced".')
+ force_tiled_decode: bool = Field(default=False, description="Whether to enable tiled VAE decode (reduces memory consumption with some performance penalty).")
+ pil_compress_level: int = Field(default=1, description="The compress_level setting of PIL.Image.save(), used for PNG encoding. All settings are lossless. 0 = no compression, 1 = fastest with slightly larger filesize, 9 = slowest with smallest filesize. 1 is typically the best setting.")
+ max_queue_size: int = Field(default=10000, gt=0, description="Maximum number of items in the session queue.")
+ clear_queue_on_startup: bool = Field(default=False, description="Empties session queue on startup. If true, disables `max_queue_history`.")
+ max_queue_history: Optional[int] = Field(default=None, ge=0, description="Keep the last N completed, failed, and canceled queue items. Older items are deleted on startup. Set to 0 to prune all terminal items. Ignored if `clear_queue_on_startup` is true.")
+
+ # NODES
+ allow_nodes: Optional[list[str]] = Field(default=None, description="List of nodes to allow. Omit to allow all.")
+ deny_nodes: Optional[list[str]] = Field(default=None, description="List of nodes to deny. Omit to deny none.")
+ node_cache_size: int = Field(default=512, description="How many cached nodes to keep in memory.")
+
+ # MODEL INSTALL
+ hashing_algorithm: HASHING_ALGORITHMS = Field(default="blake3_single", description="Model hashing algorthim for model installs. 'blake3_multi' is best for SSDs. 'blake3_single' is best for spinning disk HDDs. 'random' disables hashing, instead assigning a UUID to models. Useful when using a memory db to reduce model installation time, or if you don't care about storing stable hashes for models. Alternatively, any other hashlib algorithm is accepted, though these are not nearly as performant as blake3.")
+ remote_api_tokens: Optional[list[URLRegexTokenPair]] = Field(default=None, description="List of regular expression and token pairs used when downloading models from URLs. The download URL is tested against the regex, and if it matches, the token is provided in as a Bearer token.")
+ scan_models_on_startup: bool = Field(default=False, description="Scan the models directory on startup, registering orphaned models. This is typically only used in conjunction with `use_memory_db` for testing purposes.")
+ unsafe_disable_picklescan: bool = Field(default=False, description="UNSAFE. Disable the picklescan security check during model installation. Recommended only for development and testing purposes. This will allow arbitrary code execution during model installation, so should never be used in production.")
+ allow_unknown_models: bool = Field(default=True, description="Allow installation of models that we are unable to identify. If enabled, models will be marked as `unknown` in the database, and will not have any metadata associated with them. If disabled, unknown models will be rejected during installation.")
+
+ # MULTIUSER
+ multiuser: bool = Field(default=False, description="Enable multiuser support. When disabled, the application runs in single-user mode using a default system account with administrator privileges. When enabled, requires user authentication and authorization.")
+ strict_password_checking: bool = Field(default=False, description="Enforce strict password requirements. When True, passwords must contain uppercase, lowercase, and numbers. When False (default), any password is accepted but its strength (weak/moderate/strong) is reported to the user.")
+
+ # EXTERNAL PROVIDERS
+ external_alibabacloud_api_key: Optional[str] = Field(default=None, description="API key for Alibaba Cloud DashScope image generation.")
+ external_alibabacloud_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Alibaba Cloud DashScope image generation."
+ )
+ external_gemini_api_key: Optional[str] = Field(default=None, description="API key for Gemini image generation.")
+ external_openai_api_key: Optional[str] = Field(default=None, description="API key for OpenAI image generation.")
+ external_gemini_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Gemini image generation."
+ )
+ external_openai_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for OpenAI image generation."
+ )
+ external_seedream_api_key: Optional[str] = Field(
+ default=None, description="API key for Seedream image generation."
+ )
+ external_seedream_base_url: Optional[str] = Field(
+ default=None, description="Base URL override for Seedream image generation."
+ )
+
+ # fmt: on
+
+ model_config = SettingsConfigDict(env_prefix="INVOKEAI_", env_ignore_empty=True)
+
+ def update_config(self, config: dict[str, Any] | InvokeAIAppConfig, clobber: bool = True) -> None:
+ """Updates the config, overwriting existing values.
+
+ Args:
+ config: A dictionary of config settings, or instance of `InvokeAIAppConfig`. If an instance of \
+ `InvokeAIAppConfig`, only the explicitly set fields will be merged into the singleton config.
+ clobber: If `True`, overwrite existing values. If `False`, only update fields that are not already set.
+ """
+
+ if isinstance(config, dict):
+ new_config = self.model_validate(config)
+ else:
+ new_config = config
+
+ for field_name in new_config.model_fields_set:
+ new_value = getattr(new_config, field_name)
+ current_value = getattr(self, field_name)
+
+ if field_name in self.model_fields_set and not clobber:
+ continue
+
+ if new_value != current_value:
+ setattr(self, field_name, new_value)
+
+ def write_file(self, dest_path: Path, as_example: bool = False) -> None:
+ """Write the current configuration to file. This will overwrite the existing file.
+
+ A `meta` stanza is added to the top of the file, containing metadata about the config file. This is not stored in the config object.
+
+ Args:
+ dest_path: Path to write the config to.
+ """
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(dest_path, "w") as file:
+ # Meta fields should be written in a separate stanza - skip legacy_models_yaml_path
+ meta_dict = self.model_dump(mode="json", include={"schema_version"})
+
+ # User settings
+ config_dict = self.model_dump(
+ mode="json",
+ exclude_unset=False if as_example else True,
+ exclude_defaults=False if as_example else True,
+ exclude_none=True if as_example else False,
+ exclude={"schema_version", "legacy_models_yaml_path"},
+ )
+
+ if as_example:
+ file.write("# This is an example file with default and example settings.\n")
+ file.write("# You should not copy this whole file into your config.\n")
+ file.write("# Only add the settings you need to change to your config file.\n\n")
+ file.write("# Internal metadata - do not edit:\n")
+ file.write(yaml.dump(meta_dict, sort_keys=False))
+ file.write("\n")
+ file.write("# Put user settings here - see https://invoke.ai/configuration/invokeai-yaml/:\n")
+ if len(config_dict) > 0:
+ file.write(yaml.dump(config_dict, sort_keys=False))
+
+ def _resolve(self, partial_path: Path) -> Path:
+ return (self.root_path / partial_path).resolve()
+
+ @property
+ def root_path(self) -> Path:
+ """Path to the runtime root directory, resolved to an absolute path."""
+ if self._root:
+ root = Path(self._root).expanduser().absolute()
+ else:
+ root = self.find_root().expanduser().absolute()
+ self._root = root # insulate ourselves from relative paths that may change
+ return root.resolve()
+
+ @property
+ def config_file_path(self) -> Path:
+ """Path to invokeai.yaml, resolved to an absolute path.."""
+ resolved_path = self._resolve(self._config_file or INIT_FILE)
+ assert resolved_path is not None
+ return resolved_path
+
+ @property
+ def api_keys_file_path(self) -> Path:
+ """Path to api_keys.yaml, resolved to an absolute path.."""
+ resolved_path = self._resolve(API_KEYS_FILE)
+ assert resolved_path is not None
+ return resolved_path
+
+ @property
+ def outputs_path(self) -> Optional[Path]:
+ """Path to the outputs directory, resolved to an absolute path.."""
+ return self._resolve(self.outputs_dir)
+
+ @property
+ def db_path(self) -> Path:
+ """Path to the invokeai.db file, resolved to an absolute path.."""
+ db_dir = self._resolve(self.db_dir)
+ assert db_dir is not None
+ return db_dir / DB_FILE
+
+ @property
+ def legacy_conf_path(self) -> Path:
+ """Path to directory of legacy configuration files (e.g. v1-inference.yaml), resolved to an absolute path.."""
+ return self._resolve(self.legacy_conf_dir)
+
+ @property
+ def models_path(self) -> Path:
+ """Path to the models directory, resolved to an absolute path.."""
+ return self._resolve(self.models_dir)
+
+ @property
+ def style_presets_path(self) -> Path:
+ """Path to the style presets directory, resolved to an absolute path.."""
+ return self._resolve(self.style_presets_dir)
+
+ @property
+ def workflow_thumbnails_path(self) -> Path:
+ """Path to the workflow thumbnails directory, resolved to an absolute path.."""
+ return self._resolve(self.workflow_thumbnails_dir)
+
+ @property
+ def convert_cache_path(self) -> Path:
+ """Path to the converted cache models directory, resolved to an absolute path.."""
+ return self._resolve(self.convert_cache_dir)
+
+ @property
+ def download_cache_path(self) -> Path:
+ """Path to the downloaded models directory, resolved to an absolute path.."""
+ return self._resolve(self.download_cache_dir)
+
+ @property
+ def custom_nodes_path(self) -> Path:
+ """Path to the custom nodes directory, resolved to an absolute path.."""
+ custom_nodes_path = self._resolve(self.custom_nodes_dir)
+ assert custom_nodes_path is not None
+ return custom_nodes_path
+
+ @property
+ def profiles_path(self) -> Path:
+ """Path to the graph profiles directory, resolved to an absolute path.."""
+ return self._resolve(self.profiles_dir)
+
+ @staticmethod
+ def find_root() -> Path:
+ """Choose the runtime root directory when not specified on command line or init file."""
+ if os.environ.get("INVOKEAI_ROOT"):
+ root = Path(os.environ["INVOKEAI_ROOT"])
+ elif venv := os.environ.get("VIRTUAL_ENV", None):
+ root = Path(venv).parent.resolve()
+ else:
+ root = Path("~/invokeai").expanduser().resolve()
+ return root
+
+
+class DefaultInvokeAIAppConfig(InvokeAIAppConfig):
+ """A version of `InvokeAIAppConfig` that does not automatically parse any settings from environment variables
+ or any file.
+
+ This is useful for writing out a default config file.
+
+ Note that init settings are set if provided.
+ """
+
+ @classmethod
+ def settings_customise_sources(
+ cls,
+ settings_cls: type[BaseSettings],
+ init_settings: PydanticBaseSettingsSource,
+ env_settings: PydanticBaseSettingsSource,
+ dotenv_settings: PydanticBaseSettingsSource,
+ file_secret_settings: PydanticBaseSettingsSource,
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
+ return (init_settings,)
+
+
+def migrate_v3_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
+ """Migrate a v3 config dictionary to a v4.0.0.
+
+ Args:
+ config_dict: A dictionary of settings from a v3 config file.
+
+ Returns:
+ An `InvokeAIAppConfig` config dict.
+
+ """
+ parsed_config_dict: dict[str, Any] = {}
+ for _category_name, category_dict in config_dict["InvokeAI"].items():
+ for k, v in category_dict.items():
+ # `outdir` was renamed to `outputs_dir` in v4
+ if k == "outdir":
+ parsed_config_dict["outputs_dir"] = v
+ # `max_cache_size` was renamed to `ram` some time in v3, but both names were used
+ if k == "max_cache_size" and "ram" not in category_dict:
+ parsed_config_dict["ram"] = v
+ # `max_vram_cache_size` was renamed to `vram` some time in v3, but both names were used
+ if k == "max_vram_cache_size" and "vram" not in category_dict:
+ parsed_config_dict["vram"] = v
+ # autocast was removed in v4.0.1
+ if k == "precision" and v == "autocast":
+ parsed_config_dict["precision"] = "auto"
+ if k == "conf_path":
+ parsed_config_dict["legacy_models_yaml_path"] = v
+ if k == "legacy_conf_dir":
+ # The old default for this was "configs/stable-diffusion" ("configs\stable-diffusion" on Windows).
+ if v == "configs/stable-diffusion" or v == "configs\\stable-diffusion":
+ # If if the incoming config has the default value, skip
+ continue
+ elif Path(v).name == "stable-diffusion":
+ # Else if the path ends in "stable-diffusion", we assume the parent is the new correct path.
+ parsed_config_dict["legacy_conf_dir"] = str(Path(v).parent)
+ else:
+ # Else we do not attempt to migrate this setting
+ parsed_config_dict["legacy_conf_dir"] = v
+ elif k in InvokeAIAppConfig.model_fields:
+ # skip unknown fields
+ parsed_config_dict[k] = v
+ parsed_config_dict["schema_version"] = "4.0.0"
+ return parsed_config_dict
+
+
+def migrate_v4_0_0_to_4_0_1_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
+ """Migrate v4.0.0 config dictionary to a v4.0.1 config dictionary
+
+ Args:
+ config_dict: A dictionary of settings from a v4.0.0 config file.
+
+ Returns:
+ A config dict with the settings migrated to v4.0.1.
+ """
+ parsed_config_dict: dict[str, Any] = copy.deepcopy(config_dict)
+ # precision "autocast" was replaced by "auto" in v4.0.1
+ if parsed_config_dict.get("precision") == "autocast":
+ parsed_config_dict["precision"] = "auto"
+ parsed_config_dict["schema_version"] = "4.0.1"
+ return parsed_config_dict
+
+
+def migrate_v4_0_1_to_4_0_2_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
+ """Migrate v4.0.1 config dictionary to a v4.0.2 config dictionary.
+
+ Args:
+ config_dict: A dictionary of settings from a v4.0.1 config file.
+
+ Returns:
+ An config dict with the settings migrated to v4.0.2.
+ """
+ parsed_config_dict: dict[str, Any] = copy.deepcopy(config_dict)
+ # convert_cache was removed in 4.0.2
+ parsed_config_dict.pop("convert_cache", None)
+ parsed_config_dict["schema_version"] = "4.0.2"
+ return parsed_config_dict
+
+
+def migrate_v4_0_2_to_4_0_3_config_dict(config_dict: dict[str, Any]) -> dict[str, Any]:
+ """Migrate v4.0.2 config dictionary to a v4.0.3 config dictionary.
+
+ Args:
+ config_dict: A dictionary of settings from a v4.0.2 config file.
+
+ Returns:
+ A config dict with the settings migrated to v4.0.3.
+ """
+ parsed_config_dict: dict[str, Any] = copy.deepcopy(config_dict)
+ parsed_config_dict["schema_version"] = "4.0.3"
+ return parsed_config_dict
+
+
+def load_and_migrate_config(config_path: Path) -> InvokeAIAppConfig:
+ """Load and migrate a config file to the latest version.
+
+ Args:
+ config_path: Path to the config file.
+
+ Returns:
+ An instance of `InvokeAIAppConfig` with the loaded and migrated settings.
+ """
+ assert config_path.suffix == ".yaml"
+ with open(config_path, "rt", encoding=locale.getpreferredencoding()) as file:
+ loaded_config_dict: dict[str, Any] = yaml.safe_load(file)
+
+ assert isinstance(loaded_config_dict, dict)
+
+ migrated = False
+ if "InvokeAI" in loaded_config_dict:
+ migrated = True
+ loaded_config_dict = migrate_v3_config_dict(loaded_config_dict) # pyright: ignore [reportUnknownArgumentType]
+ if loaded_config_dict["schema_version"] == "4.0.0":
+ migrated = True
+ loaded_config_dict = migrate_v4_0_0_to_4_0_1_config_dict(loaded_config_dict)
+ if loaded_config_dict["schema_version"] == "4.0.1":
+ migrated = True
+ loaded_config_dict = migrate_v4_0_1_to_4_0_2_config_dict(loaded_config_dict)
+ if loaded_config_dict["schema_version"] == "4.0.2":
+ migrated = True
+ loaded_config_dict = migrate_v4_0_2_to_4_0_3_config_dict(loaded_config_dict)
+
+ if migrated:
+ shutil.copy(config_path, config_path.with_suffix(".yaml.bak"))
+ try:
+ # load and write without environment variables
+ migrated_config = DefaultInvokeAIAppConfig.model_validate(loaded_config_dict)
+ migrated_config.write_file(config_path)
+ except Exception as e:
+ shutil.copy(config_path.with_suffix(".yaml.bak"), config_path)
+ raise RuntimeError(f"Failed to load and migrate v3 config file {config_path}: {e}") from e
+
+ try:
+ # Meta is not included in the model fields, so we need to validate it separately
+ config = InvokeAIAppConfig.model_validate(loaded_config_dict)
+ assert config.schema_version == CONFIG_SCHEMA_VERSION, (
+ f"Invalid schema version, expected {CONFIG_SCHEMA_VERSION}: {config.schema_version}"
+ )
+ return config
+ except Exception as e:
+ raise RuntimeError(f"Failed to load config file {config_path}: {e}") from e
+
+
+def load_external_api_keys(api_keys_file_path: Path) -> dict[str, str]:
+ """Load external provider config (API keys and base URLs) from a dedicated YAML file."""
+ if not api_keys_file_path.exists():
+ return {}
+
+ with open(api_keys_file_path, "rt", encoding=locale.getpreferredencoding()) as file:
+ loaded_api_keys: Any = yaml.safe_load(file)
+
+ if loaded_api_keys is None:
+ return {}
+
+ if not isinstance(loaded_api_keys, dict):
+ raise RuntimeError(f"Failed to load api keys file {api_keys_file_path}: expected a mapping")
+
+ parsed_api_keys: dict[str, str] = {}
+ for field_name in EXTERNAL_PROVIDER_CONFIG_FIELDS:
+ value = loaded_api_keys.get(field_name)
+ if value is None:
+ continue
+ if not isinstance(value, str):
+ raise RuntimeError(
+ f"Failed to load api keys file {api_keys_file_path}: value for '{field_name}' must be a string"
+ )
+ stripped_value = value.strip()
+ if stripped_value:
+ parsed_api_keys[field_name] = stripped_value
+
+ return parsed_api_keys
+
+
+@lru_cache(maxsize=1)
+def get_config() -> InvokeAIAppConfig:
+ """Get the global singleton app config.
+
+ When first called, this function:
+ - Creates a config object. `pydantic-settings` handles merging of settings from environment variables, but not the init file.
+ - Retrieves any provided CLI args from the InvokeAIArgs class. It does not _parse_ the CLI args; that is done in the main entrypoint.
+ - Sets the root dir, if provided via CLI args.
+ - Logs in to HF if there is no valid token already.
+ - Copies all legacy configs to the legacy conf dir (needed for conversion from ckpt to diffusers).
+ - Reads and merges in settings from the config file if it exists, else writes out a default config file.
+
+ On subsequent calls, the object is returned from the cache.
+ """
+ # This object includes environment variables, as parsed by pydantic-settings
+ config = InvokeAIAppConfig()
+ env_fields_set = set(config.model_fields_set)
+
+ args = InvokeAIArgs.args
+
+ # This flag serves as a proxy for whether the config was retrieved in the context of the full application or not.
+ # If it is False, we should just return a default config and not set the root, log in to HF, etc.
+ if not InvokeAIArgs.did_parse:
+ return config
+
+ # Set CLI args
+ if root := getattr(args, "root", None):
+ config._root = Path(root)
+ if config_file := getattr(args, "config_file", None):
+ config._config_file = Path(config_file)
+
+ # Create the example config file, with some extra example values provided
+ example_config = DefaultInvokeAIAppConfig()
+ example_config.remote_api_tokens = [
+ URLRegexTokenPair(url_regex="cool-models.com", token="my_secret_token"),
+ URLRegexTokenPair(url_regex="nifty-models.com", token="some_other_token"),
+ ]
+ example_config.write_file(config.config_file_path.with_suffix(".example.yaml"), as_example=True)
+
+ # Copy all legacy configs only if needed
+ # We know `__path__[0]` is correct here
+ configs_src = Path(model_configs.__path__[0]) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
+ dest_path = config.legacy_conf_path
+
+ # Create destination (we don't need to check for existence)
+ dest_path.mkdir(parents=True, exist_ok=True)
+
+ # Compare directories recursively
+ comparison = filecmp.dircmp(configs_src, dest_path)
+ need_copy = any(
+ [
+ comparison.left_only, # Files exist only in source
+ comparison.diff_files, # Files that differ
+ comparison.common_funny, # Files that couldn't be compared
+ ]
+ )
+
+ if need_copy:
+ # Get permissions from destination directory
+ dest_mode = dest_path.stat().st_mode
+
+ # Copy directory tree
+ shutil.copytree(configs_src, dest_path, dirs_exist_ok=True)
+
+ # Set permissions on copied files to match destination directory
+ dest_path.chmod(dest_mode)
+ for p in dest_path.glob("**/*"):
+ p.chmod(dest_mode)
+
+ if config.config_file_path.exists():
+ config_from_file = load_and_migrate_config(config.config_file_path)
+ # Clobbering here will overwrite any settings that were set via environment variables
+ config.update_config(config_from_file, clobber=False)
+ else:
+ # We should never write env vars to the config file
+ default_config = DefaultInvokeAIAppConfig()
+ default_config.write_file(config.config_file_path, as_example=False)
+
+ api_keys_from_file = load_external_api_keys(config.api_keys_file_path)
+ if api_keys_from_file:
+ # API keys file should take precedence over invokeai.yaml, but not over environment variables.
+ api_keys_to_apply = {key: value for key, value in api_keys_from_file.items() if key not in env_fields_set}
+ if api_keys_to_apply:
+ config.update_config(api_keys_to_apply, clobber=True)
+
+ return config
diff --git a/invokeai/app/services/download/__init__.py b/invokeai/app/services/download/__init__.py
new file mode 100644
index 00000000000..48ded7d5496
--- /dev/null
+++ b/invokeai/app/services/download/__init__.py
@@ -0,0 +1,20 @@
+"""Init file for download queue."""
+
+from invokeai.app.services.download.download_base import (
+ DownloadJob,
+ DownloadJobStatus,
+ DownloadQueueServiceBase,
+ MultiFileDownloadJob,
+ UnknownJobIDException,
+)
+from invokeai.app.services.download.download_default import DownloadQueueService, TqdmProgress
+
+__all__ = [
+ "DownloadJob",
+ "MultiFileDownloadJob",
+ "DownloadQueueServiceBase",
+ "DownloadQueueService",
+ "TqdmProgress",
+ "DownloadJobStatus",
+ "UnknownJobIDException",
+]
diff --git a/invokeai/app/services/download/download_base.py b/invokeai/app/services/download/download_base.py
new file mode 100644
index 00000000000..1798fd69df5
--- /dev/null
+++ b/invokeai/app/services/download/download_base.py
@@ -0,0 +1,368 @@
+# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team
+"""Model download service."""
+
+from abc import ABC, abstractmethod
+from enum import Enum
+from functools import total_ordering
+from pathlib import Path
+from typing import Any, Callable, List, Optional, Set, Union
+
+from pydantic import BaseModel, Field, PrivateAttr
+from pydantic.networks import AnyHttpUrl
+
+from invokeai.backend.model_manager.metadata import RemoteModelFile
+
+
+class DownloadJobStatus(str, Enum):
+ """State of a download job."""
+
+ WAITING = "waiting" # not enqueued, will not run
+ RUNNING = "running" # actively downloading
+ PAUSED = "paused" # paused, can be resumed
+ COMPLETED = "completed" # finished running
+ CANCELLED = "cancelled" # user cancelled
+ ERROR = "error" # terminated with an error message
+
+
+class DownloadJobCancelledException(Exception):
+ """This exception is raised when a download job is cancelled."""
+
+
+class UnknownJobIDException(Exception):
+ """This exception is raised when an invalid job id is referened."""
+
+
+class ServiceInactiveException(Exception):
+ """This exception is raised when user attempts to initiate a download before the service is started."""
+
+
+SingleFileDownloadEventHandler = Callable[["DownloadJob"], None]
+SingleFileDownloadExceptionHandler = Callable[["DownloadJob", Optional[Exception]], None]
+MultiFileDownloadEventHandler = Callable[["MultiFileDownloadJob"], None]
+MultiFileDownloadExceptionHandler = Callable[["MultiFileDownloadJob", Optional[Exception]], None]
+DownloadEventHandler = Union[SingleFileDownloadEventHandler, MultiFileDownloadEventHandler]
+DownloadExceptionHandler = Union[SingleFileDownloadExceptionHandler, MultiFileDownloadExceptionHandler]
+
+
+class DownloadJobBase(BaseModel):
+ """Base of classes to monitor and control downloads."""
+
+ # automatically assigned on creation
+ id: int = Field(description="Numeric ID of this job", default=-1) # default id is a sentinel
+
+ dest: Path = Field(description="Initial destination of downloaded model on local disk; a directory or file path")
+ download_path: Optional[Path] = Field(default=None, description="Final location of downloaded file or directory")
+ status: DownloadJobStatus = Field(default=DownloadJobStatus.WAITING, description="Status of the download")
+ bytes: int = Field(default=0, description="Bytes downloaded so far")
+ total_bytes: int = Field(default=0, description="Total file size (bytes)")
+
+ # set when an error occurs
+ error_type: Optional[str] = Field(default=None, description="Name of exception that caused an error")
+ error: Optional[str] = Field(default=None, description="Traceback of the exception that caused an error")
+
+ # internal flag
+ _cancelled: bool = PrivateAttr(default=False)
+ _paused: bool = PrivateAttr(default=False)
+
+ # optional event handlers passed in on creation
+ _on_start: Optional[DownloadEventHandler] = PrivateAttr(default=None)
+ _on_progress: Optional[DownloadEventHandler] = PrivateAttr(default=None)
+ _on_complete: Optional[DownloadEventHandler] = PrivateAttr(default=None)
+ _on_cancelled: Optional[DownloadEventHandler] = PrivateAttr(default=None)
+ _on_error: Optional[DownloadExceptionHandler] = PrivateAttr(default=None)
+
+ def cancel(self) -> None:
+ """Call to cancel the job."""
+ self._cancelled = True
+ self._paused = False
+
+ def pause(self) -> None:
+ """Pause the job, preserving partial downloads."""
+ self._paused = True
+ self._cancelled = True
+
+ # cancelled and the callbacks are private attributes in order to prevent
+ # them from being serialized and/or used in the Json Schema
+ @property
+ def cancelled(self) -> bool:
+ """Call to cancel the job."""
+ return self._cancelled
+
+ @property
+ def paused(self) -> bool:
+ """Return true if job is paused."""
+ return self._paused
+
+ @property
+ def complete(self) -> bool:
+ """Return true if job completed without errors."""
+ return self.status == DownloadJobStatus.COMPLETED
+
+ @property
+ def waiting(self) -> bool:
+ """Return true if the job is waiting to run."""
+ return self.status == DownloadJobStatus.WAITING
+
+ @property
+ def running(self) -> bool:
+ """Return true if the job is running."""
+ return self.status == DownloadJobStatus.RUNNING
+
+ @property
+ def errored(self) -> bool:
+ """Return true if the job is errored."""
+ return self.status == DownloadJobStatus.ERROR
+
+ @property
+ def in_terminal_state(self) -> bool:
+ """Return true if job has finished, one way or another."""
+ return self.status not in [DownloadJobStatus.WAITING, DownloadJobStatus.RUNNING]
+
+ @property
+ def on_start(self) -> Optional[DownloadEventHandler]:
+ """Return the on_start event handler."""
+ return self._on_start
+
+ @property
+ def on_progress(self) -> Optional[DownloadEventHandler]:
+ """Return the on_progress event handler."""
+ return self._on_progress
+
+ @property
+ def on_complete(self) -> Optional[DownloadEventHandler]:
+ """Return the on_complete event handler."""
+ return self._on_complete
+
+ @property
+ def on_error(self) -> Optional[DownloadExceptionHandler]:
+ """Return the on_error event handler."""
+ return self._on_error
+
+ @property
+ def on_cancelled(self) -> Optional[DownloadEventHandler]:
+ """Return the on_cancelled event handler."""
+ return self._on_cancelled
+
+ def set_callbacks(
+ self,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> None:
+ """Set the callbacks for download events."""
+ self._on_start = on_start
+ self._on_progress = on_progress
+ self._on_complete = on_complete
+ self._on_error = on_error
+ self._on_cancelled = on_cancelled
+
+
+@total_ordering
+class DownloadJob(DownloadJobBase):
+ """Class to monitor and control a model download request."""
+
+ # required variables to be passed in on creation
+ source: AnyHttpUrl = Field(description="Where to download from. Specific types specified in child classes.")
+ access_token: Optional[str] = Field(default=None, description="authorization token for protected resources")
+ priority: int = Field(default=10, description="Queue priority; lower values are higher priority")
+
+ # set internally during download process
+ job_started: Optional[str] = Field(default=None, description="Timestamp for when the download job started")
+ job_ended: Optional[str] = Field(
+ default=None, description="Timestamp for when the download job ende1d (completed or errored)"
+ )
+ content_type: Optional[str] = Field(default=None, description="Content type of downloaded file")
+ canonical_url: Optional[str] = Field(default=None, description="Canonical URL to request on resume")
+ etag: Optional[str] = Field(default=None, description="ETag from the remote server, if available")
+ last_modified: Optional[str] = Field(default=None, description="Last-Modified from the remote server, if available")
+ final_url: Optional[str] = Field(default=None, description="Final resolved URL after redirects, if available")
+ expected_total_bytes: Optional[int] = Field(default=None, description="Expected total size of the download")
+ resume_required: bool = Field(default=False, description="True if server refused resume; restart required")
+ resume_message: Optional[str] = Field(default=None, description="Message explaining why resume is required")
+ resume_from_scratch: bool = Field(
+ default=False,
+ description="True if resume metadata existed but the partial file was missing and the download restarted from the beginning",
+ )
+
+ def __hash__(self) -> int:
+ """Return hash of the string representation of this object, for indexing."""
+ return hash(str(self))
+
+ def __le__(self, other: "DownloadJob") -> bool:
+ """Return True if this job's priority is less than another's."""
+ return self.priority <= other.priority
+
+
+class MultiFileDownloadJob(DownloadJobBase):
+ """Class to monitor and control multifile downloads."""
+
+ download_parts: Set[DownloadJob] = Field(default_factory=set, description="List of download parts.")
+
+
+class DownloadQueueServiceBase(ABC):
+ """Multithreaded queue for downloading models via URL."""
+
+ @abstractmethod
+ def start(self, *args: Any, **kwargs: Any) -> None:
+ """Start the download worker threads."""
+
+ @abstractmethod
+ def stop(self, *args: Any, **kwargs: Any) -> None:
+ """Stop the download worker threads."""
+
+ @abstractmethod
+ def download(
+ self,
+ source: AnyHttpUrl,
+ dest: Path,
+ priority: int = 10,
+ access_token: Optional[str] = None,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> DownloadJob:
+ """
+ Create and enqueue download job.
+
+ :param source: Source of the download as a URL.
+ :param dest: Path to download to. See below.
+ :param on_start, on_progress, on_complete, on_error: Callbacks for the indicated
+ events.
+ :returns: A DownloadJob object for monitoring the state of the download.
+
+ The `dest` argument is a Path object. Its behavior is:
+
+ 1. If the path exists and is a directory, then the URL contents will be downloaded
+ into that directory using the filename indicated in the response's `Content-Disposition` field.
+ If no content-disposition is present, then the last component of the URL will be used (similar to
+ wget's behavior).
+ 2. If the path does not exist, then it is taken as the name of a new file to create with the downloaded
+ content.
+ 3. If the path exists and is an existing file, then the downloader will try to resume the download from
+ the end of the existing file.
+
+ """
+ pass
+
+ @abstractmethod
+ def multifile_download(
+ self,
+ parts: List[RemoteModelFile],
+ dest: Path,
+ access_token: Optional[str] = None,
+ submit_job: bool = True,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> MultiFileDownloadJob:
+ """
+ Create and enqueue a multifile download job.
+
+ :param parts: Set of URL / filename pairs
+ :param dest: Path to download to. See below.
+ :param access_token: Access token to download the indicated files. If not provided,
+ each file's URL may be matched to an access token using the config file matching
+ system.
+ :param submit_job: If true [default] then submit the job for execution. Otherwise,
+ you will need to pass the job to submit_multifile_download().
+ :param on_start, on_progress, on_complete, on_error: Callbacks for the indicated
+ events.
+ :returns: A MultiFileDownloadJob object for monitoring the state of the download.
+
+ The `dest` argument is a Path object pointing to a directory. All downloads
+ with be placed inside this directory. The callbacks will receive the
+ MultiFileDownloadJob.
+ """
+ pass
+
+ @abstractmethod
+ def submit_multifile_download(self, job: MultiFileDownloadJob) -> None:
+ """
+ Enqueue a previously-created multi-file download job.
+
+ :param job: A MultiFileDownloadJob created with multifile_download()
+ """
+ pass
+
+ @abstractmethod
+ def submit_download_job(
+ self,
+ job: DownloadJob,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> None:
+ """
+ Enqueue a download job.
+
+ :param job: The DownloadJob
+ :param on_start, on_progress, on_complete, on_error: Callbacks for the indicated
+ events.
+ """
+ pass
+
+ @abstractmethod
+ def list_jobs(self) -> List[DownloadJob]:
+ """
+ List active download jobs.
+
+ :returns List[DownloadJob]: List of download jobs whose state is not "completed."
+ """
+ pass
+
+ @abstractmethod
+ def id_to_job(self, id: int) -> DownloadJob:
+ """
+ Return the DownloadJob corresponding to the integer ID.
+
+ :param id: ID of the DownloadJob.
+
+ Exceptions:
+ * UnknownJobIDException
+ """
+ pass
+
+ @abstractmethod
+ def cancel_all_jobs(self) -> None:
+ """Cancel all active and enquedjobs."""
+ pass
+
+ @abstractmethod
+ def prune_jobs(self) -> None:
+ """Prune completed and errored queue items from the job list."""
+ pass
+
+ @abstractmethod
+ def cancel_job(self, job: DownloadJobBase) -> None:
+ """Cancel the job, clearing partial downloads and putting it into ERROR state."""
+ pass
+
+ def pause_job(self, job: DownloadJobBase) -> None: # noqa D401
+ """Pause the job, preserving partial downloads."""
+ raise NotImplementedError
+
+ @abstractmethod
+ def join(self) -> None:
+ """Wait until all jobs are off the queue."""
+ pass
+
+ @abstractmethod
+ def wait_for_job(self, job: DownloadJobBase, timeout: int = 0) -> DownloadJobBase:
+ """Wait until the indicated download job has reached a terminal state.
+
+ This will block until the indicated install job has completed,
+ been cancelled, or errored out.
+
+ :param job: The job to wait on.
+ :param timeout: Wait up to indicated number of seconds. Raise a TimeoutError if
+ the job hasn't completed within the indicated time.
+ """
+ pass
diff --git a/invokeai/app/services/download/download_default.py b/invokeai/app/services/download/download_default.py
new file mode 100644
index 00000000000..13e86d18284
--- /dev/null
+++ b/invokeai/app/services/download/download_default.py
@@ -0,0 +1,831 @@
+# Copyright (c) 2023,2026 Lincoln D. Stein
+"""Implementation of multithreaded download queue for invokeai."""
+
+import os
+import re
+import threading
+import time
+import traceback
+from pathlib import Path
+from queue import Empty, PriorityQueue
+from shutil import disk_usage
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Set
+from urllib.parse import urlparse
+
+import requests
+from pydantic.networks import AnyHttpUrl
+from requests import HTTPError
+from tqdm import tqdm
+
+from invokeai.app.services.config import InvokeAIAppConfig, get_config
+from invokeai.app.services.download.download_base import (
+ DownloadEventHandler,
+ DownloadExceptionHandler,
+ DownloadJob,
+ DownloadJobBase,
+ DownloadJobCancelledException,
+ DownloadJobStatus,
+ DownloadQueueServiceBase,
+ MultiFileDownloadJob,
+ ServiceInactiveException,
+ UnknownJobIDException,
+)
+from invokeai.app.util.misc import get_iso_timestamp
+from invokeai.backend.model_manager.metadata import RemoteModelFile
+from invokeai.backend.util.logging import InvokeAILogger
+
+if TYPE_CHECKING:
+ from invokeai.app.services.events.events_base import EventServiceBase
+
+# Maximum number of bytes to download during each call to requests.iter_content()
+DOWNLOAD_CHUNK_SIZE = 100000
+
+
+class DownloadQueueService(DownloadQueueServiceBase):
+ """Class for queued download of models."""
+
+ def __init__(
+ self,
+ max_parallel_dl: int = 5,
+ app_config: Optional[InvokeAIAppConfig] = None,
+ event_bus: Optional["EventServiceBase"] = None,
+ requests_session: Optional[requests.sessions.Session] = None,
+ ):
+ """
+ Initialize DownloadQueue.
+
+ :param app_config: InvokeAIAppConfig object
+ :param max_parallel_dl: Number of simultaneous downloads allowed [5].
+ :param requests_session: Optional requests.sessions.Session object, for unit tests.
+ """
+ self._app_config = app_config or get_config()
+ self._jobs: Dict[int, DownloadJob] = {}
+ self._download_part2parent: Dict[int, MultiFileDownloadJob] = {}
+ self._mfd_pending: Dict[int, list[DownloadJob]] = {}
+ self._mfd_active: Dict[int, DownloadJob] = {}
+ self._next_job_id = 0
+ self._queue: PriorityQueue[DownloadJob] = PriorityQueue()
+ self._stop_event = threading.Event()
+ self._job_terminated_event = threading.Event()
+ self._worker_pool: Set[threading.Thread] = set()
+ self._lock = threading.Lock()
+ self._logger = InvokeAILogger.get_logger("DownloadQueueService")
+ self._event_bus = event_bus
+ self._requests = requests_session or requests.Session()
+ self._accept_download_requests = False
+ self._max_parallel_dl = max_parallel_dl
+
+ def start(self, *args: Any, **kwargs: Any) -> None:
+ """Start the download worker threads."""
+ with self._lock:
+ if self._worker_pool:
+ raise Exception("Attempt to start the download service twice")
+ self._stop_event.clear()
+ self._start_workers(self._max_parallel_dl)
+ self._accept_download_requests = True
+
+ def stop(self, *args: Any, **kwargs: Any) -> None:
+ """Stop the download worker threads."""
+ with self._lock:
+ if not self._worker_pool:
+ return
+ self._accept_download_requests = False # reject attempts to add new jobs to queue
+ queued_jobs = [x for x in self.list_jobs() if x.status == DownloadJobStatus.WAITING]
+ active_jobs = [x for x in self.list_jobs() if x.status == DownloadJobStatus.RUNNING]
+ if queued_jobs:
+ self._logger.warning(f"Cancelling {len(queued_jobs)} queued downloads")
+ if active_jobs:
+ self._logger.info(f"Waiting for {len(active_jobs)} active download jobs to complete")
+ with self._queue.mutex:
+ self._queue.queue.clear()
+ self.cancel_all_jobs()
+ self._stop_event.set()
+ for thread in self._worker_pool:
+ thread.join()
+ self._worker_pool.clear()
+
+ def submit_download_job(
+ self,
+ job: DownloadJob,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> None:
+ """Enqueue a download job."""
+ if not self._accept_download_requests:
+ raise ServiceInactiveException(
+ "The download service is not currently accepting requests. Please call start() to initialize the service."
+ )
+ if job.id == -1:
+ job.id = self._next_id()
+ job.set_callbacks(
+ on_start=on_start,
+ on_progress=on_progress,
+ on_complete=on_complete,
+ on_cancelled=on_cancelled,
+ on_error=on_error,
+ )
+ self._jobs[job.id] = job
+ self._queue.put(job)
+
+ def pause_job(self, job: DownloadJobBase) -> None:
+ """Pause the indicated job, preserving partial downloads."""
+ if job.status in [DownloadJobStatus.WAITING, DownloadJobStatus.RUNNING]:
+ job.pause()
+
+ def download(
+ self,
+ source: AnyHttpUrl,
+ dest: Path,
+ priority: int = 10,
+ access_token: Optional[str] = None,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> DownloadJob:
+ """Create and enqueue a download job and return it."""
+ if not self._accept_download_requests:
+ raise ServiceInactiveException(
+ "The download service is not currently accepting requests. Please call start() to initialize the service."
+ )
+ job = DownloadJob(
+ source=source,
+ dest=dest,
+ priority=priority,
+ access_token=access_token or self._lookup_access_token(source),
+ )
+ self.submit_download_job(
+ job,
+ on_start=on_start,
+ on_progress=on_progress,
+ on_complete=on_complete,
+ on_cancelled=on_cancelled,
+ on_error=on_error,
+ )
+ return job
+
+ def multifile_download(
+ self,
+ parts: List[RemoteModelFile],
+ dest: Path,
+ access_token: Optional[str] = None,
+ submit_job: bool = True,
+ on_start: Optional[DownloadEventHandler] = None,
+ on_progress: Optional[DownloadEventHandler] = None,
+ on_complete: Optional[DownloadEventHandler] = None,
+ on_cancelled: Optional[DownloadEventHandler] = None,
+ on_error: Optional[DownloadExceptionHandler] = None,
+ ) -> MultiFileDownloadJob:
+ mfdj = MultiFileDownloadJob(dest=dest, id=self._next_id())
+ mfdj.set_callbacks(
+ on_start=on_start,
+ on_progress=on_progress,
+ on_complete=on_complete,
+ on_cancelled=on_cancelled,
+ on_error=on_error,
+ )
+
+ for part in parts:
+ url = part.url
+ path = dest / part.path
+ assert path.is_relative_to(dest), "only relative download paths accepted"
+ job = DownloadJob(
+ source=url,
+ dest=path,
+ access_token=access_token or self._lookup_access_token(url),
+ )
+ job.id = self._next_id() # pre-assign ID so _download_part2parent can be keyed by ID
+ if part.size and part.size > 0:
+ job.total_bytes = part.size
+ job.expected_total_bytes = part.size
+ job.canonical_url = str(url)
+ mfdj.download_parts.add(job)
+ self._download_part2parent[job.id] = mfdj
+ if submit_job:
+ self.submit_multifile_download(mfdj)
+ return mfdj
+
+ def submit_multifile_download(self, job: MultiFileDownloadJob) -> None:
+ pending = sorted(job.download_parts, key=lambda j: str(j.source))
+ self._mfd_pending[job.id] = list(pending)
+ self._mfd_active.pop(job.id, None)
+ self._submit_next_mfd_part(job)
+
+ def _submit_next_mfd_part(self, job: MultiFileDownloadJob) -> None:
+ pending = self._mfd_pending.get(job.id, [])
+ if not pending:
+ return
+ if self._mfd_active.get(job.id) is not None:
+ return
+ download_job = pending.pop(0)
+ self._mfd_active[job.id] = download_job
+ self.submit_download_job(
+ download_job,
+ on_start=self._mfd_started,
+ on_progress=self._mfd_progress,
+ on_complete=self._mfd_complete,
+ on_cancelled=self._mfd_cancelled,
+ on_error=self._mfd_error,
+ )
+
+ def join(self) -> None:
+ """Wait for all jobs to complete."""
+ self._queue.join()
+
+ def _next_id(self) -> int:
+ with self._lock:
+ id = self._next_job_id
+ self._next_job_id += 1
+ return id
+
+ def list_jobs(self) -> List[DownloadJob]:
+ """List all the jobs."""
+ return list(self._jobs.values())
+
+ def prune_jobs(self) -> None:
+ """Prune completed and errored queue items from the job list."""
+ with self._lock:
+ to_delete = set()
+ for job_id, job in self._jobs.items():
+ if job.in_terminal_state:
+ to_delete.add(job_id)
+ for job_id in to_delete:
+ del self._jobs[job_id]
+
+ def id_to_job(self, id: int) -> DownloadJob:
+ """Translate a job ID into a DownloadJob object."""
+ try:
+ return self._jobs[id]
+ except KeyError as excp:
+ raise UnknownJobIDException("Unrecognized job") from excp
+
+ def cancel_job(self, job: DownloadJobBase) -> None:
+ """
+ Cancel the indicated job.
+
+ If it is running it will be stopped.
+ job.status will be set to DownloadJobStatus.CANCELLED
+ """
+ if job.status in [DownloadJobStatus.WAITING, DownloadJobStatus.RUNNING]:
+ job.cancel()
+
+ def cancel_all_jobs(self) -> None:
+ """Cancel all jobs (those not in enqueued, running or paused state)."""
+ for job in self._jobs.values():
+ if not job.in_terminal_state:
+ self.cancel_job(job)
+
+ def wait_for_job(self, job: DownloadJobBase, timeout: int = 0) -> DownloadJobBase:
+ """Block until the indicated job has reached terminal state, or when timeout limit reached."""
+ start = time.time()
+ while not job.in_terminal_state:
+ if self._job_terminated_event.wait(timeout=0.25): # in case we miss an event
+ self._job_terminated_event.clear()
+ if timeout > 0 and time.time() - start > timeout:
+ raise TimeoutError("Timeout exceeded")
+ return job
+
+ def _start_workers(self, max_workers: int) -> None:
+ """Start the requested number of worker threads."""
+ self._stop_event.clear()
+ for i in range(0, max_workers): # noqa B007
+ worker = threading.Thread(target=self._download_next_item, daemon=True)
+ self._logger.debug(f"Download queue worker thread {worker.name} starting.")
+ worker.start()
+ self._worker_pool.add(worker)
+
+ def _download_next_item(self) -> None:
+ """Worker thread gets next job on priority queue."""
+ done = False
+ while not done:
+ if self._stop_event.is_set():
+ done = True
+ continue
+ try:
+ job = self._queue.get(timeout=1)
+ except Empty:
+ continue
+ try:
+ if job.cancelled:
+ raise DownloadJobCancelledException("Job was cancelled before start")
+ job.job_started = get_iso_timestamp()
+ self._do_download(job)
+ if job.status != DownloadJobStatus.COMPLETED:
+ self._signal_job_complete(job)
+ except DownloadJobCancelledException:
+ if job.paused:
+ self._signal_job_paused(job)
+ else:
+ self._signal_job_cancelled(job)
+ self._cleanup_cancelled_job(job)
+ except Exception as excp:
+ job.error_type = excp.__class__.__name__ + f"({str(excp)})"
+ job.error = traceback.format_exc()
+ self._signal_job_error(job, excp)
+ finally:
+ job.job_ended = get_iso_timestamp()
+ self._job_terminated_event.set() # signal a change to terminal state
+ self._download_part2parent.pop(job.id, None) # if this is a subpart of a multipart job, remove it
+ self._queue.task_done()
+
+ self._logger.debug(f"Download queue worker thread {threading.current_thread().name} exiting.")
+
+ def _do_download(self, job: DownloadJob) -> None:
+ """Do the actual download."""
+
+ url = job.canonical_url or str(job.source)
+ header = {"Authorization": f"Bearer {job.access_token}"} if job.access_token else {}
+ had_resume_metadata = bool(job.etag or job.last_modified)
+ open_mode = "wb"
+ resume_from = 0
+
+ if not job.dest.is_dir():
+ job.download_path = job.dest
+ in_progress_path = self._in_progress_path(job.download_path)
+ if in_progress_path.exists():
+ resume_from = in_progress_path.stat().st_size
+ job.bytes = resume_from
+ self._logger.debug(
+ f"Resume check: in-progress file found at {in_progress_path} size={resume_from} bytes"
+ )
+ if resume_from > 0:
+ if job.etag:
+ header["If-Range"] = job.etag
+ elif job.last_modified:
+ header["If-Range"] = job.last_modified
+ header["Range"] = f"bytes={resume_from}-"
+ open_mode = "ab"
+ else:
+ self._logger.debug(f"Resume check: no in-progress file at {in_progress_path}")
+ elif job.download_path:
+ # Resume for directory downloads when we already know the filename.
+ in_progress_path = self._in_progress_path(job.download_path)
+ if in_progress_path.exists():
+ resume_from = in_progress_path.stat().st_size
+ job.bytes = resume_from
+ self._logger.debug(
+ f"Resume check (dir): in-progress file found at {in_progress_path} size={resume_from} bytes"
+ )
+ if resume_from > 0:
+ if job.etag:
+ header["If-Range"] = job.etag
+ elif job.last_modified:
+ header["If-Range"] = job.last_modified
+ header["Range"] = f"bytes={resume_from}-"
+ open_mode = "ab"
+ else:
+ self._logger.debug(f"Resume check (dir): no in-progress file at {in_progress_path}")
+ elif job.dest.is_dir():
+ # Attempt to infer a single in-progress file from disk for directory downloads.
+ try:
+ candidates = sorted(job.dest.glob("*.downloading"))
+ except OSError:
+ candidates = []
+ if len(candidates) == 1:
+ inferred = candidates[0].with_name(candidates[0].name.removesuffix(".downloading"))
+ job.download_path = inferred
+ try:
+ resume_from = candidates[0].stat().st_size
+ except FileNotFoundError:
+ # The .downloading file was renamed/deleted between glob and stat (race condition); skip resume.
+ job.download_path = None
+ else:
+ job.bytes = resume_from
+ self._logger.debug(
+ f"Resume check (dir): inferred in-progress file path={candidates[0]} size={resume_from} bytes"
+ )
+ if resume_from > 0:
+ if job.etag:
+ header["If-Range"] = job.etag
+ elif job.last_modified:
+ header["If-Range"] = job.last_modified
+ header["Range"] = f"bytes={resume_from}-"
+ open_mode = "ab"
+ else:
+ self._logger.debug(
+ "Resume check (dir): no prior download_path available; cannot resume from disk "
+ f"(candidates={len(candidates)})"
+ )
+
+ if resume_from == 0:
+ job.bytes = 0
+ if had_resume_metadata:
+ job.resume_from_scratch = True
+ job.resume_message = "Partial file missing. Restarted download from the beginning."
+
+ # Make a streaming request. This will retrieve headers including
+ # content-length and content-disposition, but not fetch any content itself
+ resp = self._requests.get(str(url), headers=header, stream=True)
+ job.final_url = str(resp.url) if resp.url else None
+ self._logger.debug(
+ "Resume response: "
+ f"status={resp.status_code} "
+ f"content_length={resp.headers.get('content-length')} "
+ f"content_range={resp.headers.get('Content-Range')} "
+ f"etag={resp.headers.get('ETag')} "
+ f"last_modified={resp.headers.get('Last-Modified')}"
+ )
+ if resp.status_code == 416 and resume_from > 0:
+ # Range not satisfiable - local partial is already complete
+ expected = job.expected_total_bytes or job.total_bytes or resume_from
+ if resume_from == expected:
+ job.total_bytes = expected
+ job.bytes = resume_from
+ job.download_path = job.download_path or job.dest
+ self._signal_job_started(job)
+ self._signal_job_complete(job)
+ return
+ job.resume_required = True
+ job.resume_message = "Resume refused by server. Restart required."
+ job.pause()
+ raise DownloadJobCancelledException("Resume refused by server. Restart required.")
+ if not resp.ok:
+ host = urlparse(str(resp.url or url)).netloc
+ status = resp.status_code
+ reason = resp.reason
+ if status >= 500:
+ self._logger.error(f"Remote server error from {host}: HTTP {status} {reason}")
+ raise HTTPError(reason)
+ self._logger.error(f"Download failed from {host}: HTTP {status} {reason}")
+ raise HTTPError(reason)
+
+ job.content_type = resp.headers.get("Content-Type")
+ job.etag = resp.headers.get("ETag") or job.etag
+ job.last_modified = resp.headers.get("Last-Modified") or job.last_modified
+ content_length = int(resp.headers.get("content-length", 0))
+
+ if job.dest.is_dir():
+ parsed_url = urlparse(str(url))
+ file_name = os.path.basename(parsed_url.path) # default is to use the last bit of the URL
+
+ if match := re.search('filename="(.+)"', resp.headers.get("Content-Disposition", "")):
+ remote_name = match.group(1)
+ if self._validate_filename(job.dest.as_posix(), remote_name):
+ file_name = remote_name
+
+ job.download_path = job.dest / file_name
+
+ else:
+ job.dest.parent.mkdir(parents=True, exist_ok=True)
+ job.download_path = job.dest
+
+ assert job.download_path
+
+ in_progress_path = self._in_progress_path(job.download_path)
+
+ if resume_from > 0 and resp.status_code == 200:
+ # Server ignored Range. Restart download from scratch.
+ job.resume_required = True
+ job.resume_message = "Resume refused by server. Restart required."
+ job.pause()
+ raise DownloadJobCancelledException("Resume refused by server. Restart required.")
+
+ if resume_from > 0 and resp.status_code == 206:
+ content_range = resp.headers.get("Content-Range", "")
+ total_from_range = None
+ if match := re.match(r"bytes\s+\d+-\d+/(\d+)", content_range):
+ total_from_range = int(match.group(1))
+ if total_from_range is not None:
+ job.total_bytes = total_from_range
+ else:
+ job.total_bytes = resume_from + content_length
+ job.bytes = resume_from
+ job.expected_total_bytes = job.total_bytes
+ else:
+ job.total_bytes = content_length
+ job.expected_total_bytes = content_length
+
+ if job.download_path.exists() and resume_from == 0:
+ existing_size = job.download_path.stat().st_size
+ if job.total_bytes > 0 and existing_size == job.total_bytes:
+ job.bytes = existing_size
+ self._signal_job_started(job)
+ self._signal_job_complete(job)
+ return
+ # Existing file does not match expected size; treat as corrupt and restart.
+ self._logger.debug(
+ "Resume check: existing file size mismatch; deleting and restarting "
+ f"path={job.download_path} existing_size={existing_size} expected={job.total_bytes}"
+ )
+ job.download_path.unlink()
+
+ free_space = disk_usage(job.download_path.parent).free
+ GB = 2**30
+ remaining_bytes = max(job.total_bytes - job.bytes, 0)
+ if free_space < remaining_bytes:
+ raise RuntimeError(
+ f"Free disk space {free_space / GB:.2f} GB is not enough for download of {remaining_bytes / GB:.2f} GB."
+ )
+
+ # Don't clobber an existing file. See commit 82c2c85202f88c6d24ff84710f297cfc6ae174af
+ # for code that instead resumes an interrupted download.
+ if job.download_path.exists() and resume_from == 0:
+ raise OSError(f"[Errno 17] File {job.download_path} exists")
+
+ # append ".downloading" to the path
+ # signal caller that the download is starting. At this point, key fields such as
+ # download_path and total_bytes will be populated. We call it here because the might
+ # discover that the local file is already complete and generate a COMPLETED status.
+ self._signal_job_started(job)
+
+ expected_total = job.total_bytes or job.expected_total_bytes or content_length
+ # "range not satisfiable" - local file is at least as large as the remote file
+ if resp.status_code == 416 or (expected_total > 0 and job.bytes >= expected_total):
+ self._logger.info(f"{job.download_path}: complete file found. Skipping.")
+ return
+
+ # "partial content" - local file is smaller than remote file
+ elif resp.status_code == 206 or job.bytes > 0:
+ self._logger.info(f"{job.download_path}: partial file found. Resuming")
+
+ # some other error
+ elif resp.status_code != 200:
+ host = urlparse(str(resp.url or url)).netloc
+ status = resp.status_code
+ reason = resp.reason
+ if status >= 500:
+ self._logger.error(f"Remote server error from {host}: HTTP {status} {reason}")
+ raise HTTPError(reason)
+ self._logger.error(f"Download failed from {host}: HTTP {status} {reason}")
+ raise HTTPError(reason)
+
+ self._logger.debug(f"{job.source}: Downloading {job.download_path}")
+ report_delta = job.total_bytes / 100 # report every 1% change
+ last_report_bytes = 0
+
+ # DOWNLOAD LOOP
+ with open(in_progress_path, open_mode) as file:
+ for data in resp.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
+ if job.cancelled:
+ raise DownloadJobCancelledException("Job was cancelled at caller's request")
+
+ job.bytes += file.write(data)
+ if (job.bytes - last_report_bytes >= report_delta) or (job.bytes >= job.total_bytes):
+ last_report_bytes = job.bytes
+ self._signal_job_progress(job)
+
+ if job.total_bytes > 0 and job.bytes < job.total_bytes:
+ job.resume_required = True
+ job.resume_message = "Download interrupted. Resume required."
+ job.pause()
+ raise DownloadJobCancelledException("Download interrupted. Resume required.")
+
+ # if we get here we are done and can rename the file to the original dest
+ self._logger.debug(f"{job.source}: saved to {job.download_path} (bytes={job.bytes})")
+ in_progress_path.rename(job.download_path)
+
+ def _validate_filename(self, directory: str, filename: str) -> bool:
+ pc_name_max = get_pc_name_max(directory)
+ pc_path_max = get_pc_path_max(directory)
+ if "/" in filename:
+ return False
+ if filename.startswith(".."):
+ return False
+ if len(filename) > pc_name_max:
+ return False
+ if len(os.path.join(directory, filename)) > pc_path_max:
+ return False
+ return True
+
+ def _in_progress_path(self, path: Path) -> Path:
+ return path.with_name(path.name + ".downloading")
+
+ def _lookup_access_token(self, source: AnyHttpUrl) -> Optional[str]:
+ # Pull the token from config if it exists and matches the URL
+ token = None
+ for pair in self._app_config.remote_api_tokens or []:
+ if re.search(pair.url_regex, str(source)):
+ token = pair.token
+ break
+ return token
+
+ def _signal_job_started(self, job: DownloadJob) -> None:
+ job.status = DownloadJobStatus.RUNNING
+ self._execute_cb(job, "on_start")
+ if self._event_bus:
+ self._event_bus.emit_download_started(job)
+
+ def _signal_job_progress(self, job: DownloadJob) -> None:
+ self._execute_cb(job, "on_progress")
+ if self._event_bus:
+ self._event_bus.emit_download_progress(job)
+
+ def _signal_job_complete(self, job: DownloadJob) -> None:
+ job.status = DownloadJobStatus.COMPLETED
+ self._execute_cb(job, "on_complete")
+ if self._event_bus:
+ self._event_bus.emit_download_complete(job)
+
+ def _signal_job_cancelled(self, job: DownloadJob) -> None:
+ if job.status not in [DownloadJobStatus.RUNNING, DownloadJobStatus.WAITING]:
+ return
+ job.status = DownloadJobStatus.CANCELLED
+ self._execute_cb(job, "on_cancelled")
+ if self._event_bus:
+ self._event_bus.emit_download_cancelled(job)
+
+ # if multifile download, then signal the parent
+ if parent_job := self._download_part2parent.get(job.id, None):
+ if not parent_job.in_terminal_state:
+ parent_job.status = DownloadJobStatus.CANCELLED
+ self._execute_cb(parent_job, "on_cancelled")
+
+ def _signal_job_paused(self, job: DownloadJob) -> None:
+ if job.status not in [DownloadJobStatus.RUNNING, DownloadJobStatus.WAITING]:
+ return
+ if job.download_path:
+ in_progress_path = self._in_progress_path(job.download_path)
+ if in_progress_path.exists():
+ job.bytes = in_progress_path.stat().st_size
+ job.status = DownloadJobStatus.PAUSED
+ self._execute_cb(job, "on_cancelled")
+ if self._event_bus:
+ self._event_bus.emit_download_paused(job)
+
+ if parent_job := self._download_part2parent.get(job.id, None):
+ if not parent_job.in_terminal_state:
+ parent_job.status = DownloadJobStatus.PAUSED
+ self._execute_cb(parent_job, "on_cancelled")
+
+ def _signal_job_error(self, job: DownloadJob, excp: Optional[Exception] = None) -> None:
+ job.status = DownloadJobStatus.ERROR
+ self._logger.error(f"{str(job.source)}: {traceback.format_exception(excp)}")
+ self._execute_cb(job, "on_error", excp)
+
+ if self._event_bus:
+ self._event_bus.emit_download_error(job)
+
+ def _cleanup_cancelled_job(self, job: DownloadJob) -> None:
+ if job.paused:
+ return
+ self._logger.debug(f"Cleaning up leftover files from cancelled download job {job.download_path}")
+ try:
+ if job.download_path:
+ partial_file = self._in_progress_path(job.download_path)
+ partial_file.unlink()
+ except OSError as excp:
+ self._logger.warning(excp)
+
+ ########################################
+ # callbacks used for multifile downloads
+ ########################################
+ def _mfd_started(self, download_job: DownloadJob) -> None:
+ self._logger.info(f"File download started: {download_job.source}")
+ with self._lock:
+ mf_job = self._download_part2parent[download_job.id]
+ if mf_job.waiting:
+ mf_job.total_bytes = sum(x.total_bytes for x in mf_job.download_parts)
+ mf_job.status = DownloadJobStatus.RUNNING
+ assert download_job.download_path is not None
+ path_relative_to_destdir = download_job.download_path.relative_to(mf_job.dest)
+ mf_job.download_path = (
+ mf_job.dest / path_relative_to_destdir.parts[0]
+ ) # keep just the first component of the path
+ self._execute_cb(mf_job, "on_start")
+
+ def _mfd_progress(self, download_job: DownloadJob) -> None:
+ with self._lock:
+ mf_job = self._download_part2parent[download_job.id]
+ if mf_job.cancelled:
+ for part in mf_job.download_parts:
+ self.cancel_job(part)
+ elif mf_job.running:
+ mf_job.total_bytes = sum(x.total_bytes for x in mf_job.download_parts)
+ mf_job.bytes = sum(x.bytes for x in mf_job.download_parts)
+ self._execute_cb(mf_job, "on_progress")
+
+ def _mfd_complete(self, download_job: DownloadJob) -> None:
+ self._logger.info(f"Download complete: {download_job.source}")
+ submit_next = False
+ mf_job: Optional[MultiFileDownloadJob] = None
+ with self._lock:
+ mf_job = self._download_part2parent[download_job.id]
+ self._mfd_active.pop(mf_job.id, None)
+ mf_job.total_bytes = sum(x.total_bytes for x in mf_job.download_parts)
+ mf_job.bytes = sum(x.bytes for x in mf_job.download_parts)
+
+ # are there any more active jobs left in this task?
+ if all(x.complete for x in mf_job.download_parts):
+ mf_job.status = DownloadJobStatus.COMPLETED
+ self._execute_cb(mf_job, "on_complete")
+ elif not mf_job.in_terminal_state and not mf_job.paused:
+ submit_next = True
+
+ # we're done with this sub-job
+ self._job_terminated_event.set()
+ if submit_next and mf_job is not None:
+ self._submit_next_mfd_part(mf_job)
+
+ def _mfd_cancelled(self, download_job: DownloadJob) -> None:
+ with self._lock:
+ mf_job = self._download_part2parent[download_job.id]
+ assert mf_job is not None
+ self._mfd_active.pop(mf_job.id, None)
+
+ if not mf_job.in_terminal_state:
+ if download_job.paused:
+ self._logger.warning(f"Download paused: {download_job.source}")
+ mf_job.pause()
+ else:
+ self._logger.warning(f"Download cancelled: {download_job.source}")
+ mf_job.cancel()
+
+ if download_job.paused:
+ return
+ for s in mf_job.download_parts:
+ self.cancel_job(s)
+ self._mfd_pending.pop(mf_job.id, None)
+
+ def _mfd_error(self, download_job: DownloadJob, excp: Optional[Exception] = None) -> None:
+ with self._lock:
+ mf_job = self._download_part2parent[download_job.id]
+ assert mf_job is not None
+ self._mfd_active.pop(mf_job.id, None)
+ if not mf_job.in_terminal_state:
+ mf_job.status = download_job.status
+ mf_job.error = download_job.error
+ mf_job.error_type = download_job.error_type
+ self._execute_cb(mf_job, "on_error", excp)
+ self._logger.error(
+ f"Cancelling {mf_job.dest} due to an error while downloading {download_job.source}: {str(excp)}"
+ )
+ for s in [x for x in mf_job.download_parts if x.running]:
+ self.cancel_job(s)
+ self._mfd_pending.pop(mf_job.id, None)
+ self._job_terminated_event.set()
+
+ def _execute_cb(
+ self,
+ job: DownloadJob | MultiFileDownloadJob,
+ callback_name: Literal[
+ "on_start",
+ "on_progress",
+ "on_complete",
+ "on_cancelled",
+ "on_error",
+ ],
+ excp: Optional[Exception] = None,
+ ) -> None:
+ if callback := getattr(job, callback_name, None):
+ args = [job, excp] if excp else [job]
+ try:
+ callback(*args)
+ except Exception as e:
+ self._logger.error(
+ f"An error occurred while processing the {callback_name} callback: {traceback.format_exception(e)}"
+ )
+
+
+def get_pc_name_max(directory: str) -> int:
+ if hasattr(os, "pathconf"):
+ try:
+ return os.pathconf(directory, "PC_NAME_MAX")
+ except OSError:
+ # macOS w/ external drives raise OSError
+ pass
+ return 260 # hardcoded for windows
+
+
+def get_pc_path_max(directory: str) -> int:
+ if hasattr(os, "pathconf"):
+ try:
+ return os.pathconf(directory, "PC_PATH_MAX")
+ except OSError:
+ # some platforms may not have this value
+ pass
+ return 32767 # hardcoded for windows with long names enabled
+
+
+# Example on_progress event handler to display a TQDM status bar
+# Activate with:
+# download_service.download(DownloadJob('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().update))
+class TqdmProgress(object):
+ """TQDM-based progress bar object to use in on_progress handlers."""
+
+ _bars: Dict[int, tqdm] # type: ignore
+ _last: Dict[int, int] # last bytes downloaded
+
+ def __init__(self) -> None: # noqa D107
+ self._bars = {}
+ self._last = {}
+
+ def update(self, job: DownloadJob) -> None: # noqa D102
+ job_id = job.id
+ # new job
+ if job_id not in self._bars:
+ assert job.download_path
+ dest = Path(job.download_path).name
+ self._bars[job_id] = tqdm(
+ desc=dest,
+ initial=0,
+ total=job.total_bytes,
+ unit="iB",
+ unit_scale=True,
+ )
+ self._last[job_id] = 0
+ self._bars[job_id].update(job.bytes - self._last[job_id])
+ self._last[job_id] = job.bytes
diff --git a/invokeai/app/services/events/__init__.py b/invokeai/app/services/events/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/events/events_base.py b/invokeai/app/services/events/events_base.py
new file mode 100644
index 00000000000..935b422a732
--- /dev/null
+++ b/invokeai/app/services/events/events_base.py
@@ -0,0 +1,235 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+
+from typing import TYPE_CHECKING, Optional
+
+from invokeai.app.services.events.events_common import (
+ BatchEnqueuedEvent,
+ BulkDownloadCompleteEvent,
+ BulkDownloadErrorEvent,
+ BulkDownloadStartedEvent,
+ DownloadCancelledEvent,
+ DownloadCompleteEvent,
+ DownloadErrorEvent,
+ DownloadPausedEvent,
+ DownloadProgressEvent,
+ DownloadStartedEvent,
+ EventBase,
+ InvocationCompleteEvent,
+ InvocationErrorEvent,
+ InvocationProgressEvent,
+ InvocationStartedEvent,
+ ModelInstallCancelledEvent,
+ ModelInstallCompleteEvent,
+ ModelInstallDownloadProgressEvent,
+ ModelInstallDownloadsCompleteEvent,
+ ModelInstallDownloadStartedEvent,
+ ModelInstallErrorEvent,
+ ModelInstallStartedEvent,
+ ModelLoadCompleteEvent,
+ ModelLoadStartedEvent,
+ QueueClearedEvent,
+ QueueItemsRetriedEvent,
+ QueueItemStatusChangedEvent,
+ RecallParametersUpdatedEvent,
+)
+
+if TYPE_CHECKING:
+ from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput
+ from invokeai.app.services.download.download_base import DownloadJob
+ from invokeai.app.services.model_install.model_install_common import ModelInstallJob
+ from invokeai.app.services.session_processor.session_processor_common import ProgressImage
+ from invokeai.app.services.session_queue.session_queue_common import (
+ BatchStatus,
+ EnqueueBatchResult,
+ RetryItemsResult,
+ SessionQueueItem,
+ SessionQueueStatus,
+ )
+ from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+ from invokeai.backend.model_manager.taxonomy import SubModelType
+
+
+class EventServiceBase:
+ """Basic event bus, to have an empty stand-in when not needed"""
+
+ def dispatch(self, event: "EventBase") -> None:
+ pass
+
+ # region: Invocation
+
+ def emit_invocation_started(self, queue_item: "SessionQueueItem", invocation: "BaseInvocation") -> None:
+ """Emitted when an invocation is started"""
+ self.dispatch(InvocationStartedEvent.build(queue_item, invocation))
+
+ def emit_invocation_progress(
+ self,
+ queue_item: "SessionQueueItem",
+ invocation: "BaseInvocation",
+ message: str,
+ percentage: float | None = None,
+ image: "ProgressImage | None" = None,
+ ) -> None:
+ """Emitted at periodically during an invocation"""
+ self.dispatch(InvocationProgressEvent.build(queue_item, invocation, message, percentage, image))
+
+ def emit_invocation_complete(
+ self, queue_item: "SessionQueueItem", invocation: "BaseInvocation", output: "BaseInvocationOutput"
+ ) -> None:
+ """Emitted when an invocation is complete"""
+ self.dispatch(InvocationCompleteEvent.build(queue_item, invocation, output))
+
+ def emit_invocation_error(
+ self,
+ queue_item: "SessionQueueItem",
+ invocation: "BaseInvocation",
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> None:
+ """Emitted when an invocation encounters an error"""
+ self.dispatch(InvocationErrorEvent.build(queue_item, invocation, error_type, error_message, error_traceback))
+
+ # endregion
+
+ # region Queue
+
+ def emit_queue_item_status_changed(
+ self, queue_item: "SessionQueueItem", batch_status: "BatchStatus", queue_status: "SessionQueueStatus"
+ ) -> None:
+ """Emitted when a queue item's status changes"""
+ self.dispatch(QueueItemStatusChangedEvent.build(queue_item, batch_status, queue_status))
+
+ def emit_batch_enqueued(self, enqueue_result: "EnqueueBatchResult", user_id: str = "system") -> None:
+ """Emitted when a batch is enqueued"""
+ self.dispatch(BatchEnqueuedEvent.build(enqueue_result, user_id))
+
+ def emit_queue_items_retried(self, retry_result: "RetryItemsResult") -> None:
+ """Emitted when a list of queue items are retried"""
+ self.dispatch(QueueItemsRetriedEvent.build(retry_result))
+
+ def emit_queue_cleared(self, queue_id: str) -> None:
+ """Emitted when a queue is cleared"""
+ self.dispatch(QueueClearedEvent.build(queue_id))
+
+ def emit_recall_parameters_updated(self, queue_id: str, user_id: str, parameters: dict) -> None:
+ """Emitted when recall parameters are updated"""
+ self.dispatch(RecallParametersUpdatedEvent.build(queue_id, user_id, parameters))
+
+ # endregion
+
+ # region Download
+
+ def emit_download_started(self, job: "DownloadJob") -> None:
+ """Emitted when a download is started"""
+ self.dispatch(DownloadStartedEvent.build(job))
+
+ def emit_download_progress(self, job: "DownloadJob") -> None:
+ """Emitted at intervals during a download"""
+ self.dispatch(DownloadProgressEvent.build(job))
+
+ def emit_download_complete(self, job: "DownloadJob") -> None:
+ """Emitted when a download is completed"""
+ self.dispatch(DownloadCompleteEvent.build(job))
+
+ def emit_download_cancelled(self, job: "DownloadJob") -> None:
+ """Emitted when a download is cancelled"""
+ self.dispatch(DownloadCancelledEvent.build(job))
+
+ def emit_download_paused(self, job: "DownloadJob") -> None:
+ """Emitted when a download is paused"""
+ self.dispatch(DownloadPausedEvent.build(job))
+
+ def emit_download_error(self, job: "DownloadJob") -> None:
+ """Emitted when a download encounters an error"""
+ self.dispatch(DownloadErrorEvent.build(job))
+
+ # endregion
+
+ # region Model loading
+
+ def emit_model_load_started(self, config: "AnyModelConfig", submodel_type: Optional["SubModelType"] = None) -> None:
+ """Emitted when a model load is started."""
+ self.dispatch(ModelLoadStartedEvent.build(config, submodel_type))
+
+ def emit_model_load_complete(
+ self, config: "AnyModelConfig", submodel_type: Optional["SubModelType"] = None
+ ) -> None:
+ """Emitted when a model load is complete."""
+ self.dispatch(ModelLoadCompleteEvent.build(config, submodel_type))
+
+ # endregion
+
+ # region Model install
+
+ def emit_model_install_download_started(self, job: "ModelInstallJob") -> None:
+ """Emitted at intervals while the install job is started (remote models only)."""
+ self.dispatch(ModelInstallDownloadStartedEvent.build(job))
+
+ def emit_model_install_download_progress(self, job: "ModelInstallJob") -> None:
+ """Emitted at intervals while the install job is in progress (remote models only)."""
+ self.dispatch(ModelInstallDownloadProgressEvent.build(job))
+
+ def emit_model_install_downloads_complete(self, job: "ModelInstallJob") -> None:
+ self.dispatch(ModelInstallDownloadsCompleteEvent.build(job))
+
+ def emit_model_install_started(self, job: "ModelInstallJob") -> None:
+ """Emitted once when an install job is started (after any download)."""
+ self.dispatch(ModelInstallStartedEvent.build(job))
+
+ def emit_model_install_complete(self, job: "ModelInstallJob") -> None:
+ """Emitted when an install job is completed successfully."""
+ self.dispatch(ModelInstallCompleteEvent.build(job))
+
+ def emit_model_install_cancelled(self, job: "ModelInstallJob") -> None:
+ """Emitted when an install job is cancelled."""
+ self.dispatch(ModelInstallCancelledEvent.build(job))
+
+ def emit_model_install_error(self, job: "ModelInstallJob") -> None:
+ """Emitted when an install job encounters an exception."""
+ self.dispatch(ModelInstallErrorEvent.build(job))
+
+ # endregion
+
+ # region Bulk image download
+
+ def emit_bulk_download_started(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> None:
+ """Emitted when a bulk image download is started"""
+ self.dispatch(
+ BulkDownloadStartedEvent.build(bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id)
+ )
+
+ def emit_bulk_download_complete(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> None:
+ """Emitted when a bulk image download is complete"""
+ self.dispatch(
+ BulkDownloadCompleteEvent.build(bulk_download_id, bulk_download_item_id, bulk_download_item_name, user_id)
+ )
+
+ def emit_bulk_download_error(
+ self,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ error: str,
+ user_id: str = "system",
+ ) -> None:
+ """Emitted when a bulk image download has an error"""
+ self.dispatch(
+ BulkDownloadErrorEvent.build(
+ bulk_download_id, bulk_download_item_id, bulk_download_item_name, error, user_id
+ )
+ )
+
+ # endregion
diff --git a/invokeai/app/services/events/events_common.py b/invokeai/app/services/events/events_common.py
new file mode 100644
index 00000000000..0c530f9a2f7
--- /dev/null
+++ b/invokeai/app/services/events/events_common.py
@@ -0,0 +1,703 @@
+from typing import TYPE_CHECKING, Any, ClassVar, Coroutine, Generic, Optional, Protocol, TypeAlias, TypeVar
+
+from fastapi_events.handlers.local import local_handler
+from fastapi_events.registry.payload_schema import registry as payload_schema
+from pydantic import BaseModel, ConfigDict, Field
+
+from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource
+from invokeai.app.services.session_processor.session_processor_common import ProgressImage
+from invokeai.app.services.session_queue.session_queue_common import (
+ QUEUE_ITEM_STATUS,
+ BatchStatus,
+ EnqueueBatchResult,
+ RetryItemsResult,
+ SessionQueueItem,
+ SessionQueueStatus,
+)
+from invokeai.app.services.shared.graph import AnyInvocation, AnyInvocationOutput
+from invokeai.app.util.misc import get_timestamp
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.taxonomy import SubModelType
+
+if TYPE_CHECKING:
+ from invokeai.app.services.download.download_base import DownloadJob
+ from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource
+
+
+class EventBase(BaseModel):
+ """Base class for all events. All events must inherit from this class.
+
+ Events must define a class attribute `__event_name__` to identify the event.
+
+ All other attributes should be defined as normal for a pydantic model.
+
+ A timestamp is automatically added to the event when it is created.
+ """
+
+ __event_name__: ClassVar[str]
+ timestamp: int = Field(description="The timestamp of the event", default_factory=get_timestamp)
+
+ model_config = ConfigDict(json_schema_serialization_defaults_required=True)
+
+ @classmethod
+ def get_events(cls) -> set[type["EventBase"]]:
+ """Get a set of all event models."""
+
+ event_subclasses: set[type["EventBase"]] = set()
+ for subclass in cls.__subclasses__():
+ # We only want to include subclasses that are event models, not intermediary classes
+ if hasattr(subclass, "__event_name__"):
+ event_subclasses.add(subclass)
+ event_subclasses.update(subclass.get_events())
+
+ return event_subclasses
+
+
+TEvent = TypeVar("TEvent", bound=EventBase, contravariant=True)
+
+FastAPIEvent: TypeAlias = tuple[str, TEvent]
+"""
+A tuple representing a `fastapi-events` event, with the event name and payload.
+Provide a generic type to `TEvent` to specify the payload type.
+"""
+
+
+class FastAPIEventFunc(Protocol, Generic[TEvent]):
+ def __call__(self, event: FastAPIEvent[TEvent]) -> Optional[Coroutine[Any, Any, None]]: ...
+
+
+def register_events(events: set[type[TEvent]] | type[TEvent], func: FastAPIEventFunc[TEvent]) -> None:
+ """Register a function to handle specific events.
+
+ :param events: An event or set of events to handle
+ :param func: The function to handle the events
+ """
+ events = events if isinstance(events, set) else {events}
+ for event in events:
+ assert hasattr(event, "__event_name__")
+ local_handler.register(event_name=event.__event_name__, _func=func) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
+
+
+class QueueEventBase(EventBase):
+ """Base class for queue events"""
+
+ queue_id: str = Field(description="The ID of the queue")
+
+
+class QueueItemEventBase(QueueEventBase):
+ """Base class for queue item events"""
+
+ item_id: int = Field(description="The ID of the queue item")
+ batch_id: str = Field(description="The ID of the queue batch")
+ origin: str | None = Field(default=None, description="The origin of the queue item")
+ destination: str | None = Field(default=None, description="The destination of the queue item")
+ user_id: str = Field(default="system", description="The ID of the user who created the queue item")
+
+
+class InvocationEventBase(QueueItemEventBase):
+ """Base class for invocation events"""
+
+ session_id: str = Field(description="The ID of the session (aka graph execution state)")
+ queue_id: str = Field(description="The ID of the queue")
+ session_id: str = Field(description="The ID of the session (aka graph execution state)")
+ invocation: AnyInvocation = Field(description="The ID of the invocation")
+ invocation_source_id: str = Field(description="The ID of the prepared invocation's source node")
+
+
+@payload_schema.register
+class InvocationStartedEvent(InvocationEventBase):
+ """Event model for invocation_started"""
+
+ __event_name__ = "invocation_started"
+
+ @classmethod
+ def build(cls, queue_item: SessionQueueItem, invocation: AnyInvocation) -> "InvocationStartedEvent":
+ return cls(
+ queue_id=queue_item.queue_id,
+ item_id=queue_item.item_id,
+ batch_id=queue_item.batch_id,
+ origin=queue_item.origin,
+ destination=queue_item.destination,
+ user_id=queue_item.user_id,
+ session_id=queue_item.session_id,
+ invocation=invocation,
+ invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
+ )
+
+
+@payload_schema.register
+class InvocationProgressEvent(InvocationEventBase):
+ """Event model for invocation_progress"""
+
+ __event_name__ = "invocation_progress"
+
+ message: str = Field(description="A message to display")
+ percentage: float | None = Field(
+ default=None, ge=0, le=1, description="The percentage of the progress (omit to indicate indeterminate progress)"
+ )
+ image: ProgressImage | None = Field(
+ default=None, description="An image representing the current state of the progress"
+ )
+
+ @classmethod
+ def build(
+ cls,
+ queue_item: SessionQueueItem,
+ invocation: AnyInvocation,
+ message: str,
+ percentage: float | None = None,
+ image: ProgressImage | None = None,
+ ) -> "InvocationProgressEvent":
+ return cls(
+ queue_id=queue_item.queue_id,
+ item_id=queue_item.item_id,
+ batch_id=queue_item.batch_id,
+ origin=queue_item.origin,
+ destination=queue_item.destination,
+ user_id=queue_item.user_id,
+ session_id=queue_item.session_id,
+ invocation=invocation,
+ invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
+ percentage=percentage,
+ image=image,
+ message=message,
+ )
+
+
+@payload_schema.register
+class InvocationCompleteEvent(InvocationEventBase):
+ """Event model for invocation_complete"""
+
+ __event_name__ = "invocation_complete"
+
+ result: AnyInvocationOutput = Field(description="The result of the invocation")
+
+ @classmethod
+ def build(
+ cls, queue_item: SessionQueueItem, invocation: AnyInvocation, result: AnyInvocationOutput
+ ) -> "InvocationCompleteEvent":
+ return cls(
+ queue_id=queue_item.queue_id,
+ item_id=queue_item.item_id,
+ batch_id=queue_item.batch_id,
+ origin=queue_item.origin,
+ destination=queue_item.destination,
+ user_id=queue_item.user_id,
+ session_id=queue_item.session_id,
+ invocation=invocation,
+ invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
+ result=result,
+ )
+
+
+@payload_schema.register
+class InvocationErrorEvent(InvocationEventBase):
+ """Event model for invocation_error"""
+
+ __event_name__ = "invocation_error"
+
+ error_type: str = Field(description="The error type")
+ error_message: str = Field(description="The error message")
+ error_traceback: str = Field(description="The error traceback")
+
+ @classmethod
+ def build(
+ cls,
+ queue_item: SessionQueueItem,
+ invocation: AnyInvocation,
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> "InvocationErrorEvent":
+ return cls(
+ queue_id=queue_item.queue_id,
+ item_id=queue_item.item_id,
+ batch_id=queue_item.batch_id,
+ origin=queue_item.origin,
+ destination=queue_item.destination,
+ user_id=queue_item.user_id,
+ session_id=queue_item.session_id,
+ invocation=invocation,
+ invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+
+
+@payload_schema.register
+class QueueItemStatusChangedEvent(QueueItemEventBase):
+ """Event model for queue_item_status_changed"""
+
+ __event_name__ = "queue_item_status_changed"
+
+ status: QUEUE_ITEM_STATUS = Field(description="The new status of the queue item")
+ status_sequence: int | None = Field(
+ default=None,
+ description="A monotonically increasing version for this queue item's visible status lifecycle",
+ )
+ error_type: Optional[str] = Field(default=None, description="The error type, if any")
+ error_message: Optional[str] = Field(default=None, description="The error message, if any")
+ error_traceback: Optional[str] = Field(default=None, description="The error traceback, if any")
+ created_at: str = Field(description="The timestamp when the queue item was created")
+ updated_at: str = Field(description="The timestamp when the queue item was last updated")
+ started_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was started")
+ completed_at: Optional[str] = Field(default=None, description="The timestamp when the queue item was completed")
+ batch_status: BatchStatus = Field(description="The status of the batch")
+ queue_status: SessionQueueStatus = Field(description="The status of the queue")
+ session_id: str = Field(description="The ID of the session (aka graph execution state)")
+
+ @classmethod
+ def build(
+ cls, queue_item: SessionQueueItem, batch_status: BatchStatus, queue_status: SessionQueueStatus
+ ) -> "QueueItemStatusChangedEvent":
+ return cls(
+ queue_id=queue_item.queue_id,
+ item_id=queue_item.item_id,
+ batch_id=queue_item.batch_id,
+ origin=queue_item.origin,
+ destination=queue_item.destination,
+ user_id=queue_item.user_id,
+ session_id=queue_item.session_id,
+ status=queue_item.status,
+ status_sequence=queue_item.status_sequence,
+ error_type=queue_item.error_type,
+ error_message=queue_item.error_message,
+ error_traceback=queue_item.error_traceback,
+ created_at=str(queue_item.created_at),
+ updated_at=str(queue_item.updated_at),
+ started_at=str(queue_item.started_at) if queue_item.started_at else None,
+ completed_at=str(queue_item.completed_at) if queue_item.completed_at else None,
+ batch_status=batch_status,
+ queue_status=queue_status,
+ )
+
+
+@payload_schema.register
+class BatchEnqueuedEvent(QueueEventBase):
+ """Event model for batch_enqueued"""
+
+ __event_name__ = "batch_enqueued"
+
+ batch_id: str = Field(description="The ID of the batch")
+ enqueued: int = Field(description="The number of invocations enqueued")
+ requested: int = Field(
+ description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)"
+ )
+ priority: int = Field(description="The priority of the batch")
+ origin: str | None = Field(default=None, description="The origin of the batch")
+ user_id: str = Field(default="system", description="The ID of the user who enqueued the batch")
+
+ @classmethod
+ def build(cls, enqueue_result: EnqueueBatchResult, user_id: str = "system") -> "BatchEnqueuedEvent":
+ return cls(
+ queue_id=enqueue_result.queue_id,
+ batch_id=enqueue_result.batch.batch_id,
+ origin=enqueue_result.batch.origin,
+ enqueued=enqueue_result.enqueued,
+ requested=enqueue_result.requested,
+ priority=enqueue_result.priority,
+ user_id=user_id,
+ )
+
+
+@payload_schema.register
+class QueueItemsRetriedEvent(QueueEventBase):
+ """Event model for queue_items_retried"""
+
+ __event_name__ = "queue_items_retried"
+
+ retried_item_ids: list[int] = Field(description="The IDs of the queue items that were retried")
+
+ @classmethod
+ def build(cls, retry_result: RetryItemsResult) -> "QueueItemsRetriedEvent":
+ return cls(
+ queue_id=retry_result.queue_id,
+ retried_item_ids=retry_result.retried_item_ids,
+ )
+
+
+@payload_schema.register
+class QueueClearedEvent(QueueEventBase):
+ """Event model for queue_cleared"""
+
+ __event_name__ = "queue_cleared"
+
+ @classmethod
+ def build(cls, queue_id: str) -> "QueueClearedEvent":
+ return cls(queue_id=queue_id)
+
+
+class DownloadEventBase(EventBase):
+ """Base class for events associated with a download"""
+
+ source: str = Field(description="The source of the download")
+
+
+@payload_schema.register
+class DownloadStartedEvent(DownloadEventBase):
+ """Event model for download_started"""
+
+ __event_name__ = "download_started"
+
+ download_path: str = Field(description="The local path where the download is saved")
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadStartedEvent":
+ assert job.download_path
+ return cls(source=str(job.source), download_path=job.download_path.as_posix())
+
+
+@payload_schema.register
+class DownloadProgressEvent(DownloadEventBase):
+ """Event model for download_progress"""
+
+ __event_name__ = "download_progress"
+
+ download_path: str = Field(description="The local path where the download is saved")
+ current_bytes: int = Field(description="The number of bytes downloaded so far")
+ total_bytes: int = Field(description="The total number of bytes to be downloaded")
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadProgressEvent":
+ assert job.download_path
+ return cls(
+ source=str(job.source),
+ download_path=job.download_path.as_posix(),
+ current_bytes=job.bytes,
+ total_bytes=job.total_bytes,
+ )
+
+
+@payload_schema.register
+class DownloadCompleteEvent(DownloadEventBase):
+ """Event model for download_complete"""
+
+ __event_name__ = "download_complete"
+
+ download_path: str = Field(description="The local path where the download is saved")
+ total_bytes: int = Field(description="The total number of bytes downloaded")
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadCompleteEvent":
+ assert job.download_path
+ return cls(source=str(job.source), download_path=job.download_path.as_posix(), total_bytes=job.total_bytes)
+
+
+@payload_schema.register
+class DownloadCancelledEvent(DownloadEventBase):
+ """Event model for download_cancelled"""
+
+ __event_name__ = "download_cancelled"
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadCancelledEvent":
+ return cls(source=str(job.source))
+
+
+@payload_schema.register
+class DownloadPausedEvent(DownloadEventBase):
+ """Event model for download_paused"""
+
+ __event_name__ = "download_paused"
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadPausedEvent":
+ return cls(source=str(job.source))
+
+
+@payload_schema.register
+class DownloadErrorEvent(DownloadEventBase):
+ """Event model for download_error"""
+
+ __event_name__ = "download_error"
+
+ error_type: str = Field(description="The type of error")
+ error: str = Field(description="The error message")
+
+ @classmethod
+ def build(cls, job: "DownloadJob") -> "DownloadErrorEvent":
+ assert job.error_type
+ assert job.error
+ return cls(source=str(job.source), error_type=job.error_type, error=job.error)
+
+
+class ModelEventBase(EventBase):
+ """Base class for events associated with a model"""
+
+
+@payload_schema.register
+class ModelLoadStartedEvent(ModelEventBase):
+ """Event model for model_load_started"""
+
+ __event_name__ = "model_load_started"
+
+ config: AnyModelConfig = Field(description="The model's config")
+ submodel_type: Optional[SubModelType] = Field(default=None, description="The submodel type, if any")
+
+ @classmethod
+ def build(cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> "ModelLoadStartedEvent":
+ return cls(config=config, submodel_type=submodel_type)
+
+
+@payload_schema.register
+class ModelLoadCompleteEvent(ModelEventBase):
+ """Event model for model_load_complete"""
+
+ __event_name__ = "model_load_complete"
+
+ config: AnyModelConfig = Field(description="The model's config")
+ submodel_type: Optional[SubModelType] = Field(default=None, description="The submodel type, if any")
+
+ @classmethod
+ def build(cls, config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> "ModelLoadCompleteEvent":
+ return cls(config=config, submodel_type=submodel_type)
+
+
+@payload_schema.register
+class ModelInstallDownloadStartedEvent(ModelEventBase):
+ """Event model for model_install_download_started"""
+
+ __event_name__ = "model_install_download_started"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+ local_path: str = Field(description="Where model is downloading to")
+ bytes: int = Field(description="Number of bytes downloaded so far")
+ total_bytes: int = Field(description="Total size of download, including all files")
+ parts: list[dict[str, int | str]] = Field(
+ description="Progress of downloading URLs that comprise the model, if any"
+ )
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadStartedEvent":
+ parts: list[dict[str, str | int]] = [
+ {
+ "url": str(x.source),
+ "local_path": str(x.download_path),
+ "bytes": x.bytes,
+ "total_bytes": x.total_bytes,
+ }
+ for x in job.download_parts
+ ]
+ return cls(
+ id=job.id,
+ source=job.source,
+ local_path=job.local_path.as_posix(),
+ parts=parts,
+ bytes=job.bytes,
+ total_bytes=job.total_bytes,
+ )
+
+
+@payload_schema.register
+class ModelInstallDownloadProgressEvent(ModelEventBase):
+ """Event model for model_install_download_progress"""
+
+ __event_name__ = "model_install_download_progress"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+ local_path: str = Field(description="Where model is downloading to")
+ bytes: int = Field(description="Number of bytes downloaded so far")
+ total_bytes: int = Field(description="Total size of download, including all files")
+ parts: list[dict[str, int | str]] = Field(
+ description="Progress of downloading URLs that comprise the model, if any"
+ )
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadProgressEvent":
+ parts: list[dict[str, str | int]] = [
+ {
+ "url": str(x.source),
+ "local_path": str(x.download_path),
+ "bytes": x.bytes,
+ "total_bytes": x.total_bytes,
+ }
+ for x in job.download_parts
+ ]
+ return cls(
+ id=job.id,
+ source=job.source,
+ local_path=job.local_path.as_posix(),
+ parts=parts,
+ bytes=job.bytes,
+ total_bytes=job.total_bytes,
+ )
+
+
+@payload_schema.register
+class ModelInstallDownloadsCompleteEvent(ModelEventBase):
+ """Emitted once when an install job becomes active."""
+
+ __event_name__ = "model_install_downloads_complete"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallDownloadsCompleteEvent":
+ return cls(id=job.id, source=job.source)
+
+
+@payload_schema.register
+class ModelInstallStartedEvent(ModelEventBase):
+ """Event model for model_install_started"""
+
+ __event_name__ = "model_install_started"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallStartedEvent":
+ return cls(id=job.id, source=job.source)
+
+
+@payload_schema.register
+class ModelInstallCompleteEvent(ModelEventBase):
+ """Event model for model_install_complete"""
+
+ __event_name__ = "model_install_complete"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+ key: str = Field(description="Model config record key")
+ total_bytes: Optional[int] = Field(description="Size of the model (may be None for installation of a local path)")
+ config: AnyModelConfig = Field(description="The installed model's config")
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallCompleteEvent":
+ assert job.config_out is not None
+ return cls(
+ id=job.id,
+ source=job.source,
+ key=(job.config_out.key),
+ total_bytes=job.total_bytes,
+ config=job.config_out,
+ )
+
+
+@payload_schema.register
+class ModelInstallCancelledEvent(ModelEventBase):
+ """Event model for model_install_cancelled"""
+
+ __event_name__ = "model_install_cancelled"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallCancelledEvent":
+ return cls(id=job.id, source=job.source)
+
+
+@payload_schema.register
+class ModelInstallErrorEvent(ModelEventBase):
+ """Event model for model_install_error"""
+
+ __event_name__ = "model_install_error"
+
+ id: int = Field(description="The ID of the install job")
+ source: ModelSource = Field(description="Source of the model; local path, repo_id or url")
+ error_type: str = Field(description="The name of the exception")
+ error: str = Field(description="A text description of the exception")
+
+ @classmethod
+ def build(cls, job: "ModelInstallJob") -> "ModelInstallErrorEvent":
+ assert job.error_type is not None
+ assert job.error is not None
+ return cls(id=job.id, source=job.source, error_type=job.error_type, error=job.error)
+
+
+class BulkDownloadEventBase(EventBase):
+ """Base class for events associated with a bulk image download"""
+
+ bulk_download_id: str = Field(description="The ID of the bulk image download")
+ bulk_download_item_id: str = Field(description="The ID of the bulk image download item")
+ bulk_download_item_name: str = Field(description="The name of the bulk image download item")
+ user_id: str = Field(default="system", description="The ID of the user who initiated the download")
+
+
+@payload_schema.register
+class BulkDownloadStartedEvent(BulkDownloadEventBase):
+ """Event model for bulk_download_started"""
+
+ __event_name__ = "bulk_download_started"
+
+ @classmethod
+ def build(
+ cls,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> "BulkDownloadStartedEvent":
+ return cls(
+ bulk_download_id=bulk_download_id,
+ bulk_download_item_id=bulk_download_item_id,
+ bulk_download_item_name=bulk_download_item_name,
+ user_id=user_id,
+ )
+
+
+@payload_schema.register
+class BulkDownloadCompleteEvent(BulkDownloadEventBase):
+ """Event model for bulk_download_complete"""
+
+ __event_name__ = "bulk_download_complete"
+
+ @classmethod
+ def build(
+ cls,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ user_id: str = "system",
+ ) -> "BulkDownloadCompleteEvent":
+ return cls(
+ bulk_download_id=bulk_download_id,
+ bulk_download_item_id=bulk_download_item_id,
+ bulk_download_item_name=bulk_download_item_name,
+ user_id=user_id,
+ )
+
+
+@payload_schema.register
+class BulkDownloadErrorEvent(BulkDownloadEventBase):
+ """Event model for bulk_download_error"""
+
+ __event_name__ = "bulk_download_error"
+
+ error: str = Field(description="The error message")
+
+ @classmethod
+ def build(
+ cls,
+ bulk_download_id: str,
+ bulk_download_item_id: str,
+ bulk_download_item_name: str,
+ error: str,
+ user_id: str = "system",
+ ) -> "BulkDownloadErrorEvent":
+ return cls(
+ bulk_download_id=bulk_download_id,
+ bulk_download_item_id=bulk_download_item_id,
+ bulk_download_item_name=bulk_download_item_name,
+ error=error,
+ user_id=user_id,
+ )
+
+
+@payload_schema.register
+class RecallParametersUpdatedEvent(QueueEventBase):
+ """Event model for recall_parameters_updated"""
+
+ __event_name__ = "recall_parameters_updated"
+
+ user_id: str = Field(description="The ID of the user whose recall parameters were updated")
+ parameters: dict[str, Any] = Field(description="The recall parameters that were updated")
+
+ @classmethod
+ def build(cls, queue_id: str, user_id: str, parameters: dict[str, Any]) -> "RecallParametersUpdatedEvent":
+ return cls(queue_id=queue_id, user_id=user_id, parameters=parameters)
diff --git a/invokeai/app/services/events/events_fastapievents.py b/invokeai/app/services/events/events_fastapievents.py
new file mode 100644
index 00000000000..90e1402773d
--- /dev/null
+++ b/invokeai/app/services/events/events_fastapievents.py
@@ -0,0 +1,54 @@
+import asyncio
+import threading
+
+from fastapi_events.dispatcher import dispatch
+
+from invokeai.app.services.events.events_base import EventServiceBase
+from invokeai.app.services.events.events_common import EventBase
+
+
+class FastAPIEventService(EventServiceBase):
+ def __init__(self, event_handler_id: int, loop: asyncio.AbstractEventLoop) -> None:
+ self.event_handler_id = event_handler_id
+ self._queue = asyncio.Queue[EventBase | None]()
+ self._stop_event = threading.Event()
+ self._loop = loop
+
+ # We need to store a reference to the task so it doesn't get GC'd
+ # See: https://docs.python.org/3/library/asyncio-task.html#creating-tasks
+ self._background_tasks: set[asyncio.Task[None]] = set()
+ task = self._loop.create_task(self._dispatch_from_queue(stop_event=self._stop_event))
+ self._background_tasks.add(task)
+ task.add_done_callback(self._background_tasks.remove)
+
+ super().__init__()
+
+ def stop(self, *args, **kwargs):
+ self._stop_event.set()
+ self._loop.call_soon_threadsafe(self._queue.put_nowait, None)
+
+ def dispatch(self, event: EventBase) -> None:
+ if self._loop.is_closed():
+ # The event loop was closed during shutdown. Events can no longer be dispatched;
+ # silently drop this one so the generation thread can wind down cleanly.
+ return
+ self._loop.call_soon_threadsafe(self._queue.put_nowait, event)
+
+ async def _dispatch_from_queue(self, stop_event: threading.Event):
+ """Get events on from the queue and dispatch them, from the correct thread"""
+ while not stop_event.is_set():
+ try:
+ event = await self._queue.get()
+ if not event: # Probably stopping
+ continue
+ # Leave the payloads as live pydantic models
+ dispatch(event, middleware_id=self.event_handler_id, payload_schema_dump=False)
+
+ except asyncio.CancelledError as e:
+ raise e # Raise a proper error
+ except Exception:
+ import logging
+
+ logging.getLogger("InvokeAI").error(
+ f"Error dispatching event {getattr(event, '__event_name__', event)}", exc_info=True
+ )
diff --git a/invokeai/app/services/external_generation/__init__.py b/invokeai/app/services/external_generation/__init__.py
new file mode 100644
index 00000000000..b933811d293
--- /dev/null
+++ b/invokeai/app/services/external_generation/__init__.py
@@ -0,0 +1,23 @@
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+ ExternalReferenceImage,
+)
+from invokeai.app.services.external_generation.external_generation_default import ExternalGenerationService
+
+__all__ = [
+ "ExternalGenerationRequest",
+ "ExternalGenerationResult",
+ "ExternalGeneratedImage",
+ "ExternalGenerationService",
+ "ExternalGenerationServiceBase",
+ "ExternalProvider",
+ "ExternalProviderStatus",
+ "ExternalReferenceImage",
+]
diff --git a/invokeai/app/services/external_generation/errors.py b/invokeai/app/services/external_generation/errors.py
new file mode 100644
index 00000000000..f61a6a8c730
--- /dev/null
+++ b/invokeai/app/services/external_generation/errors.py
@@ -0,0 +1,28 @@
+class ExternalGenerationError(Exception):
+ """Base error for external generation."""
+
+
+class ExternalProviderNotFoundError(ExternalGenerationError):
+ """Raised when no provider is registered for a model."""
+
+
+class ExternalProviderNotConfiguredError(ExternalGenerationError):
+ """Raised when a provider is missing required credentials."""
+
+
+class ExternalProviderCapabilityError(ExternalGenerationError):
+ """Raised when a request is not supported by provider capabilities."""
+
+
+class ExternalProviderRequestError(ExternalGenerationError):
+ """Raised when a provider rejects the request or returns an error."""
+
+
+class ExternalProviderRateLimitError(ExternalProviderRequestError):
+ """Raised when a provider returns HTTP 429 (rate limit exceeded)."""
+
+ retry_after: float | None
+
+ def __init__(self, message: str, retry_after: float | None = None) -> None:
+ super().__init__(message)
+ self.retry_after = retry_after
diff --git a/invokeai/app/services/external_generation/external_generation_base.py b/invokeai/app/services/external_generation/external_generation_base.py
new file mode 100644
index 00000000000..2145ff5ca42
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_base.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+
+
+class ExternalProvider(ABC):
+ provider_id: str
+
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ @abstractmethod
+ def is_configured(self) -> bool:
+ raise NotImplementedError
+
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ def get_status(self) -> ExternalProviderStatus:
+ return ExternalProviderStatus(provider_id=self.provider_id, configured=self.is_configured())
+
+
+class ExternalGenerationServiceBase(ABC):
+ @abstractmethod
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ raise NotImplementedError
+
+ @abstractmethod
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ raise NotImplementedError
diff --git a/invokeai/app/services/external_generation/external_generation_common.py b/invokeai/app/services/external_generation/external_generation_common.py
new file mode 100644
index 00000000000..f14bff52dd2
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_common.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+from PIL.Image import Image as PILImageType
+
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalGenerationMode
+
+
+@dataclass(frozen=True)
+class ExternalReferenceImage:
+ image: PILImageType
+
+
+@dataclass(frozen=True)
+class ExternalGenerationRequest:
+ model: ExternalApiModelConfig
+ mode: ExternalGenerationMode
+ prompt: str
+ seed: int | None
+ num_images: int
+ width: int
+ height: int
+ image_size: str | None
+ init_image: PILImageType | None
+ mask_image: PILImageType | None
+ reference_images: list[ExternalReferenceImage]
+ metadata: dict[str, Any] | None
+ provider_options: dict[str, Any] | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGeneratedImage:
+ image: PILImageType
+ seed: int | None = None
+
+
+@dataclass(frozen=True)
+class ExternalGenerationResult:
+ images: list[ExternalGeneratedImage]
+ seed_used: int | None = None
+ provider_request_id: str | None = None
+ provider_metadata: dict[str, Any] | None = None
+ content_filters: dict[str, str] | None = None
+
+
+@dataclass(frozen=True)
+class ExternalProviderStatus:
+ provider_id: str
+ configured: bool
+ message: str | None = None
diff --git a/invokeai/app/services/external_generation/external_generation_default.py b/invokeai/app/services/external_generation/external_generation_default.py
new file mode 100644
index 00000000000..d6a266753b3
--- /dev/null
+++ b/invokeai/app/services/external_generation/external_generation_default.py
@@ -0,0 +1,369 @@
+from __future__ import annotations
+
+import dataclasses
+import time
+from logging import Logger
+from typing import TYPE_CHECKING
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderNotConfiguredError,
+ ExternalProviderNotFoundError,
+ ExternalProviderRateLimitError,
+)
+from invokeai.app.services.external_generation.external_generation_base import (
+ ExternalGenerationServiceBase,
+ ExternalProvider,
+)
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+ ExternalProviderStatus,
+)
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig, ExternalImageSize
+from invokeai.backend.model_manager.starter_models import STARTER_MODELS
+
+if TYPE_CHECKING:
+ from invokeai.app.services.model_records import ModelRecordServiceBase
+
+
+class ExternalGenerationService(ExternalGenerationServiceBase):
+ def __init__(
+ self,
+ providers: dict[str, ExternalProvider],
+ logger: Logger,
+ record_store: ModelRecordServiceBase | None = None,
+ ) -> None:
+ self._providers = providers
+ self._logger = logger
+ self._record_store = record_store
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ provider = self._providers.get(request.model.provider_id)
+ if provider is None:
+ raise ExternalProviderNotFoundError(f"No external provider registered for '{request.model.provider_id}'")
+
+ if not provider.is_configured():
+ raise ExternalProviderNotConfiguredError(f"Provider '{request.model.provider_id}' is missing credentials")
+
+ request = self._refresh_model_capabilities(request)
+ resize_to_original_inpaint_size = _get_resize_target_for_inpaint(request)
+ request = self._bucket_request(request)
+ request = self._drop_unsupported_capabilities(request)
+
+ self._validate_request(request)
+ result = self._generate_with_retry(provider, request)
+
+ if resize_to_original_inpaint_size is None:
+ return result
+
+ width, height = resize_to_original_inpaint_size
+ return _resize_result_images(result, width, height)
+
+ _MAX_RETRIES = 3
+ _DEFAULT_RETRY_DELAY = 10.0
+ _MAX_RETRY_DELAY = 60.0
+
+ def _generate_with_retry(
+ self, provider: ExternalProvider, request: ExternalGenerationRequest
+ ) -> ExternalGenerationResult:
+ for attempt in range(self._MAX_RETRIES):
+ try:
+ return provider.generate(request)
+ except ExternalProviderRateLimitError as exc:
+ if attempt == self._MAX_RETRIES - 1:
+ raise
+ delay = min(exc.retry_after or self._DEFAULT_RETRY_DELAY, self._MAX_RETRY_DELAY)
+ self._logger.warning(
+ "Rate limited by %s (attempt %d/%d), retrying in %.0fs",
+ request.model.provider_id,
+ attempt + 1,
+ self._MAX_RETRIES,
+ delay,
+ )
+ time.sleep(delay)
+ raise ExternalProviderRateLimitError("Rate limit exceeded after all retries")
+
+ def get_provider_statuses(self) -> dict[str, ExternalProviderStatus]:
+ return {provider_id: provider.get_status() for provider_id, provider in self._providers.items()}
+
+ def _validate_request(self, request: ExternalGenerationRequest) -> None:
+ capabilities = request.model.capabilities
+
+ self._logger.debug(
+ "Validating external request provider=%s model=%s mode=%s supported=%s",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.mode,
+ capabilities.modes,
+ )
+
+ if request.mode not in capabilities.modes:
+ raise ExternalProviderCapabilityError(f"Mode '{request.mode}' is not supported by {request.model.name}")
+
+ if request.reference_images and not capabilities.supports_reference_images:
+ raise ExternalProviderCapabilityError(f"Reference images are not supported by {request.model.name}")
+
+ if capabilities.max_reference_images is not None:
+ if len(request.reference_images) > capabilities.max_reference_images:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_reference_images} reference images"
+ )
+
+ if capabilities.max_images_per_request is not None and request.num_images > capabilities.max_images_per_request:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {capabilities.max_images_per_request} images per request"
+ )
+
+ if capabilities.max_image_size is not None:
+ if request.width > capabilities.max_image_size.width or request.height > capabilities.max_image_size.height:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports a maximum size of {capabilities.max_image_size.width}x{capabilities.max_image_size.height}"
+ )
+
+ if capabilities.allowed_aspect_ratios:
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ if aspect_ratio not in capabilities.allowed_aspect_ratios:
+ size_ratio = None
+ if capabilities.aspect_ratio_sizes:
+ size_ratio = _ratio_for_size(request.width, request.height, capabilities.aspect_ratio_sizes)
+ if size_ratio is None or size_ratio not in capabilities.allowed_aspect_ratios:
+ ratio_label = size_ratio or aspect_ratio
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} does not support aspect ratio {ratio_label}"
+ )
+
+ required_modes = capabilities.input_image_required_for or ["img2img", "inpaint"]
+ if request.mode in required_modes and request.init_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires an init image for {request.model.name}"
+ )
+
+ if request.mode == "inpaint" and request.mask_image is None:
+ raise ExternalProviderCapabilityError(
+ f"Mode '{request.mode}' requires a mask image for {request.model.name}"
+ )
+
+ def _drop_unsupported_capabilities(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ """Silently drop request fields the selected model does not support so workflow-editor runs don't fail
+ when users wire them in regardless."""
+ capabilities = request.model.capabilities
+ updates: dict[str, object] = {}
+
+ if request.seed is not None and not capabilities.supports_seed:
+ self._logger.debug(
+ "Dropping seed for %s: model does not support seed control",
+ request.model.name,
+ )
+ updates["seed"] = None
+
+ if updates:
+ return dataclasses.replace(request, **updates)
+ return request
+
+ def _refresh_model_capabilities(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ if self._record_store is None:
+ return request
+
+ try:
+ record = self._record_store.get_model(request.model.key)
+ except Exception:
+ record = None
+
+ if not isinstance(record, ExternalApiModelConfig):
+ return request
+
+ if record.key != request.model.key:
+ return request
+
+ if record.provider_id != request.model.provider_id:
+ return request
+
+ if record.provider_model_id != request.model.provider_model_id:
+ return request
+
+ record = _apply_starter_overrides(record)
+
+ if record == request.model:
+ return request
+
+ return ExternalGenerationRequest(
+ model=record,
+ mode=request.mode,
+ prompt=request.prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=request.width,
+ height=request.height,
+ image_size=request.image_size,
+ init_image=request.init_image,
+ mask_image=request.mask_image,
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ provider_options=request.provider_options,
+ )
+
+ def _bucket_request(self, request: ExternalGenerationRequest) -> ExternalGenerationRequest:
+ capabilities = request.model.capabilities
+ if not capabilities.allowed_aspect_ratios:
+ return request
+
+ aspect_ratio = _format_aspect_ratio(request.width, request.height)
+ size = None
+ if capabilities.aspect_ratio_sizes:
+ size = capabilities.aspect_ratio_sizes.get(aspect_ratio)
+
+ if size is not None:
+ if request.width == size.width and request.height == size.height:
+ return request
+ return self._bucket_to_size(request, size.width, size.height, aspect_ratio)
+
+ if aspect_ratio in capabilities.allowed_aspect_ratios:
+ return request
+
+ if not capabilities.aspect_ratio_sizes:
+ return request
+
+ closest = _select_closest_ratio(
+ request.width,
+ request.height,
+ capabilities.allowed_aspect_ratios,
+ )
+ if closest is None:
+ return request
+
+ size = capabilities.aspect_ratio_sizes.get(closest)
+ if size is None:
+ return request
+
+ return self._bucket_to_size(request, size.width, size.height, closest)
+
+ def _bucket_to_size(
+ self,
+ request: ExternalGenerationRequest,
+ width: int,
+ height: int,
+ ratio: str,
+ ) -> ExternalGenerationRequest:
+ self._logger.info(
+ "Bucketing external request provider=%s model=%s %sx%s -> %sx%s (ratio %s)",
+ request.model.provider_id,
+ request.model.provider_model_id,
+ request.width,
+ request.height,
+ width,
+ height,
+ ratio,
+ )
+
+ return ExternalGenerationRequest(
+ model=request.model,
+ mode=request.mode,
+ prompt=request.prompt,
+ seed=request.seed,
+ num_images=request.num_images,
+ width=width,
+ height=height,
+ image_size=request.image_size,
+ init_image=_resize_image(request.init_image, width, height, "RGB"),
+ mask_image=_resize_image(request.mask_image, width, height, "L"),
+ reference_images=request.reference_images,
+ metadata=request.metadata,
+ provider_options=request.provider_options,
+ )
+
+
+def _format_aspect_ratio(width: int, height: int) -> str:
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _select_closest_ratio(width: int, height: int, ratios: list[str]) -> str | None:
+ ratio = width / height
+ parsed: list[tuple[str, float]] = []
+ for value in ratios:
+ parsed_ratio = _parse_ratio(value)
+ if parsed_ratio is not None:
+ parsed.append((value, parsed_ratio))
+ if not parsed:
+ return None
+ return min(parsed, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _ratio_for_size(width: int, height: int, sizes: dict[str, ExternalImageSize]) -> str | None:
+ for ratio, size in sizes.items():
+ if size.width == width and size.height == height:
+ return ratio
+ return None
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
+
+
+def _resize_image(image: PILImageType | None, width: int, height: int, mode: str) -> PILImageType | None:
+ if image is None:
+ return None
+ if image.width == width and image.height == height:
+ return image
+ return image.convert(mode).resize((width, height), Image.Resampling.LANCZOS)
+
+
+def _get_resize_target_for_inpaint(request: ExternalGenerationRequest) -> tuple[int, int] | None:
+ if request.mode != "inpaint" or request.init_image is None:
+ return None
+ return request.init_image.width, request.init_image.height
+
+
+def _resize_result_images(result: ExternalGenerationResult, width: int, height: int) -> ExternalGenerationResult:
+ resized_images = [
+ ExternalGeneratedImage(
+ image=generated.image
+ if generated.image.width == width and generated.image.height == height
+ else generated.image.resize((width, height), Image.Resampling.LANCZOS),
+ seed=generated.seed,
+ )
+ for generated in result.images
+ ]
+ return ExternalGenerationResult(
+ images=resized_images,
+ seed_used=result.seed_used,
+ provider_request_id=result.provider_request_id,
+ provider_metadata=result.provider_metadata,
+ content_filters=result.content_filters,
+ )
+
+
+def _apply_starter_overrides(model: ExternalApiModelConfig) -> ExternalApiModelConfig:
+ source = model.source or f"external://{model.provider_id}/{model.provider_model_id}"
+ starter_match = next((starter for starter in STARTER_MODELS if starter.source == source), None)
+ if starter_match is None:
+ return model
+ updates: dict[str, object] = {}
+ if starter_match.capabilities is not None:
+ updates["capabilities"] = starter_match.capabilities
+ if starter_match.default_settings is not None:
+ updates["default_settings"] = starter_match.default_settings
+ if not updates:
+ return model
+ return model.model_copy(update=updates)
diff --git a/invokeai/app/services/external_generation/image_utils.py b/invokeai/app/services/external_generation/image_utils.py
new file mode 100644
index 00000000000..a23c1f11d66
--- /dev/null
+++ b/invokeai/app/services/external_generation/image_utils.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import base64
+import io
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+
+def encode_image_base64(image: PILImageType, format: str = "PNG") -> str:
+ buffer = io.BytesIO()
+ image.save(buffer, format=format)
+ return base64.b64encode(buffer.getvalue()).decode("ascii")
+
+
+def decode_image_base64(encoded: str) -> PILImageType:
+ data = base64.b64decode(encoded)
+ image = Image.open(io.BytesIO(data))
+ return image.convert("RGB")
diff --git a/invokeai/app/services/external_generation/providers/__init__.py b/invokeai/app/services/external_generation/providers/__init__.py
new file mode 100644
index 00000000000..9926302addf
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/__init__.py
@@ -0,0 +1,6 @@
+from invokeai.app.services.external_generation.providers.alibabacloud import AlibabaCloudProvider
+from invokeai.app.services.external_generation.providers.gemini import GeminiProvider
+from invokeai.app.services.external_generation.providers.openai import OpenAIProvider
+from invokeai.app.services.external_generation.providers.seedream import SeedreamProvider
+
+__all__ = ["AlibabaCloudProvider", "GeminiProvider", "OpenAIProvider", "SeedreamProvider"]
diff --git a/invokeai/app/services/external_generation/providers/alibabacloud.py b/invokeai/app/services/external_generation/providers/alibabacloud.py
new file mode 100644
index 00000000000..6a1d01baefa
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/alibabacloud.py
@@ -0,0 +1,410 @@
+from __future__ import annotations
+
+import io
+import time
+
+import requests
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import ExternalProviderRequestError
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+# Models that support the synchronous multimodal-generation endpoint with messages format
+_SYNC_MODELS = {
+ "qwen-image-2.0-pro",
+ "qwen-image-2.0",
+ "qwen-image-max",
+ "wan2.6-t2i",
+ "qwen-image-edit-max",
+}
+
+# Models that use the async image-generation endpoint with flat prompt format.
+# Currently no shipped starter model uses this path, but it is retained because
+# users may install custom external models via `external://alibabacloud/`.
+_ASYNC_MODELS: set[str] = set()
+
+_TASK_POLL_INTERVAL = 5 # seconds
+_TASK_POLL_TIMEOUT = 300 # seconds
+_DOWNLOAD_TIMEOUT = 60 # seconds
+_DOWNLOAD_MAX_BYTES = 32 * 1024 * 1024 # 32 MiB safety cap on image downloads
+_RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
+_MAX_RETRIES = 2 # total attempts = 1 + _MAX_RETRIES
+_RETRY_BACKOFF_BASE = 2.0 # seconds
+
+
+class AlibabaCloudProvider(ExternalProvider):
+ provider_id = "alibabacloud"
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_alibabacloud_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_alibabacloud_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Alibaba Cloud DashScope API key is not configured")
+
+ base_url = (self._app_config.external_alibabacloud_base_url or "https://dashscope-intl.aliyuncs.com").rstrip(
+ "/"
+ )
+ model_id = request.model.provider_model_id
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {api_key}",
+ }
+ size = f"{request.width}*{request.height}"
+
+ if model_id in _SYNC_MODELS:
+ return self._generate_sync(request, base_url, headers, model_id, size)
+ if model_id in _ASYNC_MODELS:
+ return self._generate_async(request, base_url, headers, model_id, size)
+ raise ExternalProviderRequestError(
+ f"Unknown DashScope model_id '{model_id}'. Add it to _SYNC_MODELS or _ASYNC_MODELS in alibabacloud.py."
+ )
+
+ def _generate_sync(
+ self,
+ request: ExternalGenerationRequest,
+ base_url: str,
+ headers: dict[str, str],
+ model_id: str,
+ size: str,
+ ) -> ExternalGenerationResult:
+ """Use the synchronous multimodal-generation endpoint (messages format)."""
+ endpoint = f"{base_url}/api/v1/services/aigc/multimodal-generation/generation"
+
+ content: list[dict[str, str]] = []
+
+ # Reference images: DashScope multimodal accepts up to 3 input images for the
+ # qwen-image-edit family; we let the API surface its own limit if exceeded.
+ for ref in request.reference_images:
+ content.append({"image": f"data:image/png;base64,{encode_image_base64(ref.image)}"})
+
+ content.append({"text": request.prompt})
+
+ parameters: dict[str, object] = {
+ "size": size,
+ "n": request.num_images,
+ "prompt_extend": False,
+ "watermark": False,
+ }
+ if request.seed is not None:
+ parameters["seed"] = request.seed
+
+ payload: dict[str, object] = {
+ "model": model_id,
+ "input": {
+ "messages": [
+ {
+ "role": "user",
+ "content": content,
+ }
+ ]
+ },
+ "parameters": parameters,
+ }
+
+ response = self._post_with_retry(endpoint, headers=headers, json=payload, timeout=120, label="DashScope sync")
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"DashScope request failed with status {response.status_code} for model '{model_id}': {response.text}"
+ )
+
+ data = response.json()
+ request_id = data.get("request_id")
+ return self._parse_sync_response(data, request, request_id)
+
+ def _generate_async(
+ self,
+ request: ExternalGenerationRequest,
+ base_url: str,
+ headers: dict[str, str],
+ model_id: str,
+ size: str,
+ ) -> ExternalGenerationResult:
+ """Use the async image-generation endpoint (flat prompt format) with task polling."""
+ endpoint = f"{base_url}/api/v1/services/aigc/image-generation/generation"
+ async_headers = {**headers, "X-DashScope-Async": "enable"}
+
+ parameters: dict[str, object] = {
+ "size": size,
+ "n": request.num_images,
+ "prompt_extend": False,
+ "watermark": False,
+ }
+ if request.seed is not None:
+ parameters["seed"] = request.seed
+
+ input_data: dict[str, object] = {"prompt": request.prompt}
+
+ payload: dict[str, object] = {
+ "model": model_id,
+ "input": input_data,
+ "parameters": parameters,
+ }
+
+ response = self._post_with_retry(
+ endpoint, headers=async_headers, json=payload, timeout=60, label="DashScope async submit"
+ )
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"DashScope async request failed with status {response.status_code} for model '{model_id}': {response.text}"
+ )
+
+ data = response.json()
+ request_id = data.get("request_id")
+ output = data.get("output", {})
+ task_id = output.get("task_id")
+
+ if not task_id:
+ raise ExternalProviderRequestError(f"DashScope async response missing task_id: {data}")
+
+ return self._poll_task(base_url, headers, task_id, request, request_id)
+
+ def _poll_task(
+ self,
+ base_url: str,
+ headers: dict[str, str],
+ task_id: str,
+ request: ExternalGenerationRequest,
+ request_id: str | None,
+ ) -> ExternalGenerationResult:
+ """Poll an async task until completion."""
+ task_url = f"{base_url}/api/v1/tasks/{task_id}"
+ start_time = time.monotonic()
+ poll_headers = {"Authorization": headers["Authorization"]}
+ first_poll = True
+
+ while True:
+ elapsed = time.monotonic() - start_time
+ if elapsed > _TASK_POLL_TIMEOUT:
+ raise ExternalProviderRequestError(f"DashScope task {task_id} timed out after {_TASK_POLL_TIMEOUT}s")
+
+ response = self._get_with_retry(task_url, headers=poll_headers, timeout=30, label="DashScope task poll")
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"DashScope task poll failed with status {response.status_code}: {response.text}"
+ )
+
+ data = response.json()
+ output = data.get("output", {})
+ status = output.get("task_status")
+
+ if first_poll:
+ self._logger.info("DashScope task %s submitted (status=%s)", task_id, status)
+ first_poll = False
+
+ if status == "SUCCEEDED":
+ return self._parse_async_response(output, request, request_id)
+ if status in ("FAILED", "UNKNOWN"):
+ message = output.get("message", "Unknown error")
+ raise ExternalProviderRequestError(f"DashScope task {task_id} failed: {message}")
+
+ self._logger.debug("DashScope task %s status: %s (%.0fs elapsed)", task_id, status, elapsed)
+ time.sleep(_TASK_POLL_INTERVAL)
+
+ def _parse_sync_response(
+ self,
+ data: dict[str, object],
+ request: ExternalGenerationRequest,
+ request_id: str | None,
+ ) -> ExternalGenerationResult:
+ """Parse the synchronous multimodal-generation response."""
+ output = data.get("output")
+ if not isinstance(output, dict):
+ raise ExternalProviderRequestError(f"DashScope response missing output: {data}")
+
+ choices = output.get("choices")
+ if not isinstance(choices, list):
+ raise ExternalProviderRequestError(f"DashScope response missing choices: {data}")
+
+ images: list[ExternalGeneratedImage] = []
+ for choice in choices:
+ if not isinstance(choice, dict):
+ continue
+ message = choice.get("message")
+ if not isinstance(message, dict):
+ continue
+ content = message.get("content")
+ if not isinstance(content, list):
+ continue
+ for part in content:
+ if not isinstance(part, dict):
+ continue
+ image_url = part.get("image")
+ if isinstance(image_url, str) and image_url:
+ pil_image = self._download_image(image_url)
+ images.append(ExternalGeneratedImage(image=pil_image, seed=request.seed))
+
+ if not images:
+ raise ExternalProviderRequestError(f"DashScope response contained no images: {data}")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_request_id=request_id,
+ provider_metadata={"model": request.model.provider_model_id},
+ )
+
+ def _parse_async_response(
+ self,
+ output: dict[str, object],
+ request: ExternalGenerationRequest,
+ request_id: str | None,
+ ) -> ExternalGenerationResult:
+ """Parse the async task completion response."""
+ results = output.get("results")
+ if not isinstance(results, list):
+ raise ExternalProviderRequestError(f"DashScope async response missing results: {output}")
+
+ images: list[ExternalGeneratedImage] = []
+ for result in results:
+ if not isinstance(result, dict):
+ continue
+ url = result.get("url")
+ if isinstance(url, str) and url:
+ pil_image = self._download_image(url)
+ images.append(ExternalGeneratedImage(image=pil_image, seed=request.seed))
+ continue
+ b64_image = result.get("b64_image")
+ if isinstance(b64_image, str) and b64_image:
+ pil_image = decode_image_base64(b64_image)
+ images.append(ExternalGeneratedImage(image=pil_image, seed=request.seed))
+
+ if not images:
+ raise ExternalProviderRequestError(f"DashScope async response contained no images: {output}")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_request_id=request_id,
+ provider_metadata={"model": request.model.provider_model_id},
+ )
+
+ def _download_image(self, url: str) -> PILImageType:
+ """Download an image from a URL and return it as a PIL Image, with a size cap."""
+ try:
+ response = requests.get(url, timeout=_DOWNLOAD_TIMEOUT, stream=True)
+ except requests.RequestException as exc:
+ raise ExternalProviderRequestError(f"Failed to download image from DashScope: {exc}") from exc
+
+ with response:
+ if not response.ok:
+ raise ExternalProviderRequestError(
+ f"Failed to download image from DashScope (status {response.status_code})"
+ )
+
+ content_length = response.headers.get("Content-Length")
+ if content_length is not None:
+ try:
+ if int(content_length) > _DOWNLOAD_MAX_BYTES:
+ raise ExternalProviderRequestError(
+ f"DashScope image exceeds {_DOWNLOAD_MAX_BYTES} byte cap (Content-Length={content_length})"
+ )
+ except ValueError:
+ pass
+
+ buffer = bytearray()
+ for chunk in response.iter_content(chunk_size=64 * 1024):
+ if not chunk:
+ continue
+ buffer.extend(chunk)
+ if len(buffer) > _DOWNLOAD_MAX_BYTES:
+ raise ExternalProviderRequestError(f"DashScope image exceeds {_DOWNLOAD_MAX_BYTES} byte cap")
+
+ return Image.open(io.BytesIO(bytes(buffer))).convert("RGB")
+
+ def _post_with_retry(
+ self,
+ url: str,
+ *,
+ headers: dict[str, str],
+ json: dict,
+ timeout: int,
+ label: str,
+ ) -> requests.Response:
+ return self._request_with_retry("POST", url, headers=headers, json=json, timeout=timeout, label=label)
+
+ def _get_with_retry(
+ self,
+ url: str,
+ *,
+ headers: dict[str, str],
+ timeout: int,
+ label: str,
+ ) -> requests.Response:
+ return self._request_with_retry("GET", url, headers=headers, timeout=timeout, label=label)
+
+ def _request_with_retry(
+ self,
+ method: str,
+ url: str,
+ *,
+ headers: dict[str, str],
+ timeout: int,
+ label: str,
+ json: dict | None = None,
+ ) -> requests.Response:
+ """Issue a request with limited retries on transient failures (429/5xx, network errors).
+
+ Honors `Retry-After` for 429 responses when present. Non-retryable errors
+ (4xx other than 429, parse failures) are returned to the caller, which is
+ responsible for raising a meaningful ExternalProviderRequestError.
+ """
+ last_exc: Exception | None = None
+ for attempt in range(_MAX_RETRIES + 1):
+ try:
+ if method == "POST":
+ response = requests.post(url, headers=headers, json=json, timeout=timeout)
+ else:
+ response = requests.get(url, headers=headers, timeout=timeout)
+ except requests.RequestException as exc:
+ last_exc = exc
+ if attempt >= _MAX_RETRIES:
+ raise ExternalProviderRequestError(f"{label} network error: {exc}") from exc
+ delay = _RETRY_BACKOFF_BASE * (2**attempt)
+ self._logger.warning(
+ "%s network error on attempt %d/%d: %s — retrying in %.1fs",
+ label,
+ attempt + 1,
+ _MAX_RETRIES + 1,
+ exc,
+ delay,
+ )
+ time.sleep(delay)
+ continue
+
+ if response.status_code in _RETRY_STATUS_CODES and attempt < _MAX_RETRIES:
+ delay = self._retry_delay(response, attempt)
+ self._logger.warning(
+ "%s got status %d on attempt %d/%d — retrying in %.1fs",
+ label,
+ response.status_code,
+ attempt + 1,
+ _MAX_RETRIES + 1,
+ delay,
+ )
+ time.sleep(delay)
+ continue
+
+ return response
+
+ # Unreachable: the loop either returns a response or raises.
+ assert last_exc is not None
+ raise ExternalProviderRequestError(f"{label} failed after retries: {last_exc}") from last_exc
+
+ @staticmethod
+ def _retry_delay(response: requests.Response, attempt: int) -> float:
+ retry_after = response.headers.get("Retry-After")
+ if retry_after:
+ try:
+ return max(0.0, float(retry_after))
+ except ValueError:
+ pass
+ return _RETRY_BACKOFF_BASE * (2**attempt)
diff --git a/invokeai/app/services/external_generation/providers/gemini.py b/invokeai/app/services/external_generation/providers/gemini.py
new file mode 100644
index 00000000000..de2cf0e85a7
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/gemini.py
@@ -0,0 +1,248 @@
+from __future__ import annotations
+
+import requests
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderRateLimitError,
+ ExternalProviderRequestError,
+)
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+
+class GeminiProvider(ExternalProvider):
+ provider_id = "gemini"
+ _SYSTEM_INSTRUCTION = (
+ "You are an image generation model. Always respond with an image based on the user's prompt. "
+ "Do not return text-only responses. If the user input is not an edit instruction, "
+ "interpret it as a request to create a new image."
+ )
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_gemini_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_gemini_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Gemini API key is not configured")
+
+ base_url = (self._app_config.external_gemini_base_url or "https://generativelanguage.googleapis.com").rstrip(
+ "/"
+ )
+ if not base_url.endswith("/v1") and not base_url.endswith("/v1beta"):
+ base_url = f"{base_url}/v1beta"
+ model_id = request.model.provider_model_id.removeprefix("models/")
+ endpoint = f"{base_url}/models/{model_id}:generateContent"
+
+ request_parts: list[dict[str, object]] = []
+
+ if request.init_image is not None:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(request.init_image),
+ }
+ }
+ )
+
+ request_parts.append({"text": request.prompt})
+
+ for reference in request.reference_images:
+ request_parts.append(
+ {
+ "inlineData": {
+ "mimeType": "image/png",
+ "data": encode_image_base64(reference.image),
+ }
+ }
+ )
+
+ opts = request.provider_options or {}
+
+ generation_config: dict[str, object] = {
+ "candidateCount": request.num_images,
+ "responseModalities": ["IMAGE"],
+ }
+ if "temperature" in opts:
+ generation_config["temperature"] = opts["temperature"]
+ aspect_ratio = _select_aspect_ratio(
+ request.width,
+ request.height,
+ request.model.capabilities.allowed_aspect_ratios,
+ )
+ uses_image_config = request.model.capabilities.resolution_presets is not None
+ if uses_image_config:
+ image_config: dict[str, str] = {}
+ if aspect_ratio is not None:
+ image_config["aspectRatio"] = aspect_ratio
+ if request.image_size is not None:
+ image_config["imageSize"] = request.image_size
+ if image_config:
+ generation_config["imageConfig"] = image_config
+ system_instruction = self._SYSTEM_INSTRUCTION
+ if request.init_image is not None:
+ system_instruction = (
+ f"{system_instruction} An input image is provided. "
+ "Treat the prompt as an edit instruction and modify the image accordingly. "
+ "Do not return the original image unchanged."
+ )
+ if not uses_image_config and aspect_ratio is not None:
+ system_instruction = f"{system_instruction} Use an aspect ratio of {aspect_ratio}."
+
+ payload: dict[str, object] = {
+ "systemInstruction": {"parts": [{"text": system_instruction}]},
+ "contents": [{"role": "user", "parts": request_parts}],
+ "generationConfig": generation_config,
+ }
+ if "thinking_level" in opts:
+ payload["thinkingConfig"] = {"thinkingLevel": opts["thinking_level"].upper()}
+
+ response = requests.post(
+ endpoint,
+ params={"key": api_key},
+ json=payload,
+ timeout=120,
+ )
+
+ if not response.ok:
+ if response.status_code == 429:
+ retry_after = _parse_retry_after(response.headers.get("retry-after"))
+ raise ExternalProviderRateLimitError(
+ f"Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}",
+ retry_after=retry_after,
+ )
+ raise ExternalProviderRequestError(
+ f"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}"
+ )
+
+ data = response.json()
+ if not isinstance(data, dict):
+ raise ExternalProviderRequestError("Gemini response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ text_parts: list[str] = []
+ finish_messages: list[str] = []
+ candidates = data.get("candidates")
+ if not isinstance(candidates, list):
+ raise ExternalProviderRequestError("Gemini response payload missing candidates")
+ for candidate in candidates:
+ if not isinstance(candidate, dict):
+ continue
+ finish_message = candidate.get("finishMessage")
+ finish_reason = candidate.get("finishReason")
+ if isinstance(finish_message, str):
+ finish_messages.append(finish_message)
+ elif isinstance(finish_reason, str):
+ finish_messages.append(f"Finish reason: {finish_reason}")
+ for part in _iter_response_parts(candidate):
+ inline_data = part.get("inline_data") or part.get("inlineData")
+ if isinstance(inline_data, dict):
+ encoded = inline_data.get("data")
+ if encoded:
+ image = decode_image_base64(encoded)
+ images.append(ExternalGeneratedImage(image=image, seed=request.seed))
+ continue
+ file_data = part.get("fileData") or part.get("file_data")
+ if isinstance(file_data, dict):
+ file_uri = file_data.get("fileUri") or file_data.get("file_uri")
+ if isinstance(file_uri, str) and file_uri:
+ raise ExternalProviderRequestError(
+ f"Gemini returned fileUri instead of inline image data: {file_uri}"
+ )
+ text = part.get("text")
+ if isinstance(text, str):
+ text_parts.append(text)
+
+ if not images:
+ self._logger.error("Gemini response contained no images: %s", data)
+ detail = ""
+ if finish_messages:
+ combined = " ".join(message.strip() for message in finish_messages if message.strip())
+ if combined:
+ detail = f" Response status: {combined[:500]}"
+ elif text_parts:
+ combined = " ".join(text_parts).strip()
+ if combined:
+ detail = f" Response text: {combined[:500]}"
+ raise ExternalProviderRequestError(f"Gemini response contained no images.{detail}")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_metadata={"model": request.model.provider_model_id},
+ )
+
+
+def _iter_response_parts(candidate: dict[str, object]) -> list[dict[str, object]]:
+ content = candidate.get("content")
+ if isinstance(content, dict):
+ content_parts = content.get("parts")
+ if isinstance(content_parts, list):
+ return [part for part in content_parts if isinstance(part, dict)]
+ contents = candidate.get("contents")
+ if isinstance(contents, list):
+ parts: list[dict[str, object]] = []
+ for item in contents:
+ if not isinstance(item, dict):
+ continue
+ item_parts = item.get("parts")
+ if isinstance(item_parts, list):
+ parts.extend([part for part in item_parts if isinstance(part, dict)])
+ if parts:
+ return parts
+ return []
+
+
+def _select_aspect_ratio(width: int, height: int, allowed: list[str] | None) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ ratio = width / height
+ default_ratio = _format_aspect_ratio(width, height)
+ if not allowed:
+ return default_ratio
+ parsed = [(value, _parse_ratio(value)) for value in allowed]
+ filtered = [(value, parsed_ratio) for value, parsed_ratio in parsed if parsed_ratio is not None]
+ if not filtered:
+ return default_ratio
+ return min(filtered, key=lambda item: abs(item[1] - ratio))[0]
+
+
+def _format_aspect_ratio(width: int, height: int) -> str | None:
+ if width <= 0 or height <= 0:
+ return None
+ divisor = _gcd(width, height)
+ return f"{width // divisor}:{height // divisor}"
+
+
+def _parse_ratio(value: str) -> float | None:
+ if ":" not in value:
+ return None
+ left, right = value.split(":", 1)
+ try:
+ numerator = float(left)
+ denominator = float(right)
+ except ValueError:
+ return None
+ if denominator == 0:
+ return None
+ return numerator / denominator
+
+
+def _parse_retry_after(value: str | None) -> float | None:
+ if not value:
+ return None
+ try:
+ return float(value)
+ except ValueError:
+ return None
+
+
+def _gcd(a: int, b: int) -> int:
+ while b:
+ a, b = b, a % b
+ return a
diff --git a/invokeai/app/services/external_generation/providers/openai.py b/invokeai/app/services/external_generation/providers/openai.py
new file mode 100644
index 00000000000..7e8252b43d3
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/openai.py
@@ -0,0 +1,162 @@
+from __future__ import annotations
+
+import io
+
+import requests
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderRateLimitError,
+ ExternalProviderRequestError,
+)
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64
+
+
+class OpenAIProvider(ExternalProvider):
+ provider_id = "openai"
+
+ _GPT_IMAGE_MODELS = {"gpt-image-1", "gpt-image-1.5", "gpt-image-1-mini", "gpt-image-2"}
+ _DEFAULT_TIMEOUT = 120
+ _MODEL_TIMEOUTS: dict[str, int] = {"gpt-image-2": 300}
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_openai_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_openai_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("OpenAI API key is not configured")
+
+ model_id = request.model.provider_model_id
+ is_gpt_image = model_id in self._GPT_IMAGE_MODELS
+ timeout = self._MODEL_TIMEOUTS.get(model_id, self._DEFAULT_TIMEOUT)
+ size = f"{request.width}x{request.height}"
+ base_url = (self._app_config.external_openai_base_url or "https://api.openai.com").rstrip("/")
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ use_edits_endpoint = request.mode != "txt2img" or bool(request.reference_images)
+
+ opts = request.provider_options or {}
+
+ if not use_edits_endpoint:
+ payload: dict[str, object] = {
+ "model": model_id,
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ }
+ # GPT Image models use output_format; DALL-E uses response_format
+ if is_gpt_image:
+ payload["output_format"] = "png"
+ else:
+ payload["response_format"] = "b64_json"
+ if is_gpt_image:
+ if opts.get("quality") and opts["quality"] != "auto":
+ payload["quality"] = opts["quality"]
+ if opts.get("background") and opts["background"] != "auto":
+ payload["background"] = opts["background"]
+ response = requests.post(
+ f"{base_url}/v1/images/generations",
+ headers=headers,
+ json=payload,
+ timeout=timeout,
+ )
+ else:
+ images: list[PILImageType] = []
+ if request.init_image is not None:
+ images.append(request.init_image)
+ images.extend(reference.image for reference in request.reference_images)
+ if not images:
+ raise ExternalProviderRequestError(
+ "OpenAI image edits require at least one image (init image or reference image)"
+ )
+
+ files: list[tuple[str, tuple[str, io.BytesIO, str]]] = []
+ image_field_name = "image" if len(images) == 1 else "image[]"
+ for index, image in enumerate(images):
+ image_buffer = io.BytesIO()
+ image.save(image_buffer, format="PNG")
+ image_buffer.seek(0)
+ files.append((image_field_name, (f"image_{index}.png", image_buffer, "image/png")))
+
+ if request.mask_image is not None:
+ mask_buffer = io.BytesIO()
+ request.mask_image.save(mask_buffer, format="PNG")
+ mask_buffer.seek(0)
+ files.append(("mask", ("mask.png", mask_buffer, "image/png")))
+
+ data: dict[str, object] = {
+ "model": model_id,
+ "prompt": request.prompt,
+ "n": request.num_images,
+ "size": size,
+ }
+ if is_gpt_image:
+ data["output_format"] = "png"
+ else:
+ data["response_format"] = "b64_json"
+ if is_gpt_image:
+ if opts.get("quality") and opts["quality"] != "auto":
+ data["quality"] = opts["quality"]
+ if opts.get("background") and opts["background"] != "auto":
+ data["background"] = opts["background"]
+ if opts.get("input_fidelity"):
+ data["input_fidelity"] = opts["input_fidelity"]
+ response = requests.post(
+ f"{base_url}/v1/images/edits",
+ headers=headers,
+ data=data,
+ files=files,
+ timeout=timeout,
+ )
+
+ if not response.ok:
+ if response.status_code == 429:
+ retry_after = _parse_retry_after(response.headers.get("retry-after"))
+ raise ExternalProviderRateLimitError(
+ f"OpenAI rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}",
+ retry_after=retry_after,
+ )
+ raise ExternalProviderRequestError(
+ f"OpenAI request failed with status {response.status_code}: {response.text}"
+ )
+
+ response_payload = response.json()
+ if not isinstance(response_payload, dict):
+ raise ExternalProviderRequestError("OpenAI response payload was not a JSON object")
+ images: list[ExternalGeneratedImage] = []
+ data_items = response_payload.get("data")
+ if not isinstance(data_items, list):
+ raise ExternalProviderRequestError("OpenAI response payload missing image data")
+ for item in data_items:
+ if not isinstance(item, dict):
+ continue
+ encoded = item.get("b64_json")
+ if not encoded:
+ continue
+ images.append(ExternalGeneratedImage(image=decode_image_base64(encoded), seed=request.seed))
+
+ if not images:
+ raise ExternalProviderRequestError("OpenAI response contained no images")
+
+ return ExternalGenerationResult(
+ images=images,
+ seed_used=request.seed,
+ provider_request_id=response.headers.get("x-request-id"),
+ provider_metadata={"model": model_id},
+ )
+
+
+def _parse_retry_after(value: str | None) -> float | None:
+ if not value:
+ return None
+ try:
+ return float(value)
+ except ValueError:
+ return None
diff --git a/invokeai/app/services/external_generation/providers/seedream.py b/invokeai/app/services/external_generation/providers/seedream.py
new file mode 100644
index 00000000000..13b05af7e38
--- /dev/null
+++ b/invokeai/app/services/external_generation/providers/seedream.py
@@ -0,0 +1,171 @@
+from __future__ import annotations
+
+import requests
+
+from invokeai.app.services.external_generation.errors import (
+ ExternalProviderCapabilityError,
+ ExternalProviderRateLimitError,
+ ExternalProviderRequestError,
+)
+from invokeai.app.services.external_generation.external_generation_base import ExternalProvider
+from invokeai.app.services.external_generation.external_generation_common import (
+ ExternalGeneratedImage,
+ ExternalGenerationRequest,
+ ExternalGenerationResult,
+)
+from invokeai.app.services.external_generation.image_utils import decode_image_base64, encode_image_base64
+
+_SEEDREAM_BATCH_PREFIXES = (
+ "seedream-5",
+ "seedream-4.5",
+ "seedream-4.0",
+ "seedream-4-5",
+ "seedream-4-0",
+ "seedream-5-0",
+)
+
+# Seedream batch endpoint accepts up to 15 total images counting both inputs (reference + init)
+# and outputs combined. Hitting this only after the API call wastes a request and produces a
+# confusing 400, so we enforce it locally for batch-capable models.
+_SEEDREAM_BATCH_MAX_TOTAL_IMAGES = 15
+
+
+class SeedreamProvider(ExternalProvider):
+ provider_id = "seedream"
+
+ def is_configured(self) -> bool:
+ return bool(self._app_config.external_seedream_api_key)
+
+ def generate(self, request: ExternalGenerationRequest) -> ExternalGenerationResult:
+ api_key = self._app_config.external_seedream_api_key
+ if not api_key:
+ raise ExternalProviderRequestError("Seedream API key is not configured")
+
+ base_url = (self._app_config.external_seedream_base_url or "https://ark.ap-southeast.bytepluses.com").rstrip(
+ "/"
+ )
+ endpoint = f"{base_url}/api/v3/images/generations"
+ headers = {"Authorization": f"Bearer {api_key}"}
+
+ model_id = request.model.provider_model_id
+ is_batch_model = any(model_id.startswith(prefix) for prefix in _SEEDREAM_BATCH_PREFIXES)
+
+ if is_batch_model:
+ input_image_count = len(request.reference_images) + (1 if request.init_image is not None else 0)
+ total_images = input_image_count + request.num_images
+ if total_images > _SEEDREAM_BATCH_MAX_TOTAL_IMAGES:
+ raise ExternalProviderCapabilityError(
+ f"{request.model.name} supports at most {_SEEDREAM_BATCH_MAX_TOTAL_IMAGES} images total "
+ f"(reference + init + output), got {total_images}"
+ )
+
+ opts = request.provider_options or {}
+
+ payload: dict[str, object] = {
+ "model": model_id,
+ "prompt": request.prompt,
+ "size": f"{request.width}x{request.height}",
+ "response_format": "b64_json",
+ "watermark": opts.get("watermark", False),
+ }
+
+ if opts.get("optimize_prompt"):
+ payload["optimize_prompt_options"] = {"optimize_prompt": True}
+
+ # Seed and guidance_scale are only supported on 3.0 models
+ if not is_batch_model and request.seed is not None and request.seed >= 0:
+ payload["seed"] = request.seed
+ if not is_batch_model and opts.get("guidance_scale") is not None:
+ payload["guidance_scale"] = opts["guidance_scale"]
+
+ # Batch generation for 4.x/5.x models
+ if is_batch_model:
+ if request.num_images > 1:
+ payload["sequential_image_generation"] = "auto"
+ payload["sequential_image_generation_options"] = {"max_images": request.num_images}
+ else:
+ payload["sequential_image_generation"] = "disabled"
+
+ # Image input: init_image for img2img, reference images for 4.x
+ images_b64: list[str] = []
+ if request.init_image is not None:
+ images_b64.append(f"data:image/png;base64,{encode_image_base64(request.init_image)}")
+ for reference in request.reference_images:
+ images_b64.append(f"data:image/png;base64,{encode_image_base64(reference.image)}")
+
+ if images_b64:
+ payload["image"] = images_b64 if len(images_b64) > 1 else images_b64[0]
+
+ response = requests.post(endpoint, headers=headers, json=payload, timeout=120)
+
+ if not response.ok:
+ if response.status_code == 429:
+ retry_after = _parse_retry_after(response.headers.get("retry-after"))
+ raise ExternalProviderRateLimitError(
+ f"Seedream rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}",
+ retry_after=retry_after,
+ )
+ raise ExternalProviderRequestError(
+ f"Seedream request failed with status {response.status_code}: {response.text}"
+ )
+
+ body = response.json()
+ if not isinstance(body, dict):
+ raise ExternalProviderRequestError("Seedream response payload was not a JSON object")
+
+ generated_images: list[ExternalGeneratedImage] = []
+ item_errors: list[dict[str, object]] = []
+ data_items = body.get("data")
+ if not isinstance(data_items, list):
+ raise ExternalProviderRequestError("Seedream response payload missing image data")
+
+ for item in data_items:
+ if not isinstance(item, dict):
+ continue
+ # Items may be error objects for failed images in batch — collect rather than discard
+ # so partial-failure causes (e.g., content filter) are visible to the caller.
+ if "error" in item:
+ error_payload = item["error"]
+ item_errors.append(
+ error_payload if isinstance(error_payload, dict) else {"message": str(error_payload)}
+ )
+ continue
+ encoded = item.get("b64_json")
+ if not encoded:
+ continue
+ image = decode_image_base64(encoded)
+ generated_images.append(ExternalGeneratedImage(image=image, seed=request.seed))
+
+ if not generated_images:
+ if item_errors:
+ first = item_errors[0]
+ message = first.get("message") if isinstance(first, dict) else None
+ raise ExternalProviderRequestError(
+ f"Seedream returned no images. Provider reported: {message or item_errors}"
+ )
+ raise ExternalProviderRequestError("Seedream response contained no images")
+
+ provider_metadata: dict[str, object] = {"model": model_id}
+ if item_errors:
+ provider_metadata["partial_failures"] = item_errors
+ self._logger.warning(
+ "Seedream returned %d image(s) with %d partial failure(s): %s",
+ len(generated_images),
+ len(item_errors),
+ item_errors,
+ )
+
+ return ExternalGenerationResult(
+ images=generated_images,
+ seed_used=request.seed,
+ provider_metadata=provider_metadata,
+ )
+
+
+def _parse_retry_after(value: str | None) -> float | None:
+ if not value:
+ return None
+ try:
+ return float(value)
+ except ValueError:
+ return None
diff --git a/invokeai/app/services/external_generation/startup.py b/invokeai/app/services/external_generation/startup.py
new file mode 100644
index 00000000000..a95e6c94180
--- /dev/null
+++ b/invokeai/app/services/external_generation/startup.py
@@ -0,0 +1,59 @@
+from logging import Logger
+from typing import TYPE_CHECKING
+
+from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
+from invokeai.backend.model_manager.configs.external_api import ExternalApiModelConfig
+from invokeai.backend.model_manager.starter_models import STARTER_MODELS
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+
+if TYPE_CHECKING:
+ from invokeai.app.services.model_manager.model_manager_base import ModelManagerServiceBase
+
+
+def sync_configured_external_starter_models(
+ configured_provider_ids: set[str],
+ model_manager: "ModelManagerServiceBase",
+ logger: Logger,
+) -> list[str]:
+ """Queue missing external starter models for configured providers."""
+
+ if not configured_provider_ids:
+ return []
+
+ installed_sources = {
+ model.source
+ for model in model_manager.store.search_by_attr(
+ base_model=BaseModelType.External,
+ model_type=ModelType.ExternalImageGenerator,
+ )
+ if isinstance(model, ExternalApiModelConfig) and model.source
+ }
+
+ queued_sources: list[str] = []
+ for starter_model in STARTER_MODELS:
+ if not starter_model.source.startswith("external://"):
+ continue
+
+ provider_id = starter_model.source.removeprefix("external://").split("/", 1)[0]
+ if provider_id not in configured_provider_ids:
+ continue
+
+ if starter_model.source in installed_sources:
+ continue
+
+ model_manager.install.heuristic_import(
+ starter_model.source,
+ config=ModelRecordChanges(
+ name=starter_model.name,
+ base=starter_model.base,
+ type=starter_model.type,
+ description=starter_model.description,
+ format=starter_model.format,
+ capabilities=starter_model.capabilities,
+ default_settings=starter_model.default_settings,
+ ),
+ )
+ queued_sources.append(starter_model.source)
+ logger.info("Queued external starter model sync for %s", starter_model.source)
+
+ return queued_sources
diff --git a/invokeai/app/services/image_files/__init__.py b/invokeai/app/services/image_files/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/image_files/image_files_base.py b/invokeai/app/services/image_files/image_files_base.py
new file mode 100644
index 00000000000..7464cd7941d
--- /dev/null
+++ b/invokeai/app/services/image_files/image_files_base.py
@@ -0,0 +1,55 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Optional
+
+from PIL.Image import Image as PILImageType
+
+
+class ImageFileStorageBase(ABC):
+ """Low-level service responsible for storing and retrieving image files."""
+
+ @abstractmethod
+ def get(self, image_name: str, image_subfolder: str = "") -> PILImageType:
+ """Retrieves an image as PIL Image."""
+ pass
+
+ @abstractmethod
+ def get_path(self, image_name: str, thumbnail: bool = False, image_subfolder: str = "") -> Path:
+ """Gets the internal path to an image or thumbnail."""
+ pass
+
+ # TODO: We need to validate paths before starlette makes the FileResponse, else we get a
+ # 500 internal server error. I don't like having this method on the service.
+ @abstractmethod
+ def validate_path(self, path: str) -> bool:
+ """Validates the path given for an image or thumbnail."""
+ pass
+
+ @abstractmethod
+ def save(
+ self,
+ image: PILImageType,
+ image_name: str,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ thumbnail_size: int = 256,
+ image_subfolder: str = "",
+ ) -> None:
+ """Saves an image and a 256x256 WEBP thumbnail. Returns a tuple of the image name, thumbnail name, and created timestamp."""
+ pass
+
+ @abstractmethod
+ def delete(self, image_name: str, image_subfolder: str = "") -> None:
+ """Deletes an image and its thumbnail (if one exists)."""
+ pass
+
+ @abstractmethod
+ def get_workflow(self, image_name: str, image_subfolder: str = "") -> Optional[str]:
+ """Gets the workflow of an image."""
+ pass
+
+ @abstractmethod
+ def get_graph(self, image_name: str, image_subfolder: str = "") -> Optional[str]:
+ """Gets the graph of an image."""
+ pass
diff --git a/invokeai/app/services/image_files/image_files_common.py b/invokeai/app/services/image_files/image_files_common.py
new file mode 100644
index 00000000000..e9cc2a3fa75
--- /dev/null
+++ b/invokeai/app/services/image_files/image_files_common.py
@@ -0,0 +1,20 @@
+# TODO: Should these excpetions subclass existing python exceptions?
+class ImageFileNotFoundException(Exception):
+ """Raised when an image file is not found in storage."""
+
+ def __init__(self, message="Image file not found"):
+ super().__init__(message)
+
+
+class ImageFileSaveException(Exception):
+ """Raised when an image cannot be saved."""
+
+ def __init__(self, message="Image file not saved"):
+ super().__init__(message)
+
+
+class ImageFileDeleteException(Exception):
+ """Raised when an image cannot be deleted."""
+
+ def __init__(self, message="Image file not deleted"):
+ super().__init__(message)
diff --git a/invokeai/app/services/image_files/image_files_disk.py b/invokeai/app/services/image_files/image_files_disk.py
new file mode 100644
index 00000000000..12b737a7cf1
--- /dev/null
+++ b/invokeai/app/services/image_files/image_files_disk.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
+from pathlib import Path
+from queue import Queue
+from typing import Optional, Union
+
+from PIL import Image, PngImagePlugin
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
+from invokeai.app.services.image_files.image_files_common import (
+ ImageFileDeleteException,
+ ImageFileNotFoundException,
+ ImageFileSaveException,
+)
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.util.thumbnails import get_thumbnail_name, make_thumbnail
+
+
+class DiskImageFileStorage(ImageFileStorageBase):
+ """Stores images on disk"""
+
+ def __init__(self, output_folder: Union[str, Path]):
+ self.__cache: dict[Path, PILImageType] = {}
+ self.__cache_ids = Queue[Path]()
+ self.__max_cache_size = 10 # TODO: get this from config
+
+ self.__output_folder = output_folder if isinstance(output_folder, Path) else Path(output_folder)
+ self.__thumbnails_folder = self.__output_folder / "thumbnails"
+ # Validate required output folders at launch
+ self.__validate_storage_folders()
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def get(self, image_name: str, image_subfolder: str = "") -> PILImageType:
+ try:
+ image_path = self.get_path(image_name, image_subfolder=image_subfolder)
+
+ cache_item = self.__get_cache(image_path)
+ if cache_item:
+ return cache_item
+
+ image = Image.open(image_path)
+ self.__set_cache(image_path, image)
+ return image
+ except FileNotFoundError as e:
+ raise ImageFileNotFoundException from e
+
+ def save(
+ self,
+ image: PILImageType,
+ image_name: str,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ thumbnail_size: int = 256,
+ image_subfolder: str = "",
+ ) -> None:
+ try:
+ self.__validate_storage_folders()
+ image_path = self.get_path(image_name, image_subfolder=image_subfolder)
+
+ # Ensure subfolder directories exist
+ image_path.parent.mkdir(parents=True, exist_ok=True)
+
+ pnginfo = PngImagePlugin.PngInfo()
+ info_dict = {}
+
+ if metadata is not None:
+ info_dict["invokeai_metadata"] = metadata
+ pnginfo.add_text("invokeai_metadata", metadata)
+ if workflow is not None:
+ info_dict["invokeai_workflow"] = workflow
+ pnginfo.add_text("invokeai_workflow", workflow)
+ if graph is not None:
+ info_dict["invokeai_graph"] = graph
+ pnginfo.add_text("invokeai_graph", graph)
+
+ # When saving the image, the image object's info field is not populated. We need to set it
+ image.info = info_dict
+ image.save(
+ image_path,
+ "PNG",
+ pnginfo=pnginfo,
+ compress_level=self.__invoker.services.configuration.pil_compress_level,
+ )
+
+ thumbnail_name = get_thumbnail_name(image_name)
+ thumbnail_path = self.get_path(thumbnail_name, thumbnail=True, image_subfolder=image_subfolder)
+
+ # Ensure thumbnail subfolder directories exist
+ thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
+
+ thumbnail_image = make_thumbnail(image, thumbnail_size)
+ thumbnail_image.save(thumbnail_path)
+
+ self.__set_cache(image_path, image)
+ self.__set_cache(thumbnail_path, thumbnail_image)
+ except Exception as e:
+ raise ImageFileSaveException from e
+
+ def delete(self, image_name: str, image_subfolder: str = "") -> None:
+ try:
+ image_path = self.get_path(image_name, image_subfolder=image_subfolder)
+
+ if image_path.exists():
+ image_path.unlink()
+ if image_path in self.__cache:
+ del self.__cache[image_path]
+
+ thumbnail_name = get_thumbnail_name(image_name)
+ thumbnail_path = self.get_path(thumbnail_name, True, image_subfolder=image_subfolder)
+
+ if thumbnail_path.exists():
+ thumbnail_path.unlink()
+ if thumbnail_path in self.__cache:
+ del self.__cache[thumbnail_path]
+ except Exception as e:
+ raise ImageFileDeleteException from e
+
+ def get_path(self, image_name: str, thumbnail: bool = False, image_subfolder: str = "") -> Path:
+ base_folder = self.__thumbnails_folder if thumbnail else self.__output_folder
+ filename = get_thumbnail_name(image_name) if thumbnail else image_name
+
+ # Validate the filename itself (no path separators allowed in the filename)
+ basename = Path(filename).name
+ if basename != filename:
+ raise ValueError("Invalid image name, potential directory traversal detected")
+
+ # Build the full path with optional subfolder
+ if image_subfolder:
+ self._validate_subfolder(image_subfolder)
+ image_path = base_folder / image_subfolder / basename
+ else:
+ image_path = base_folder / basename
+
+ # Ensure the image path is within the base folder to prevent directory traversal
+ resolved_base = base_folder.resolve()
+ resolved_image_path = image_path.resolve()
+
+ if not resolved_image_path.is_relative_to(resolved_base):
+ raise ValueError("Image path outside outputs folder, potential directory traversal detected")
+
+ return resolved_image_path
+
+ @staticmethod
+ def _validate_subfolder(subfolder: str) -> None:
+ """Validates a subfolder path to prevent directory traversal while allowing controlled subdirectories."""
+ if not subfolder:
+ return
+ if "\\" in subfolder:
+ raise ValueError("Backslashes not allowed in subfolder path")
+ if subfolder.startswith("/"):
+ raise ValueError("Absolute paths not allowed in subfolder path")
+ parts = subfolder.split("/")
+ for part in parts:
+ if part == "..":
+ raise ValueError("Parent directory references not allowed in subfolder path")
+ if part == "":
+ raise ValueError("Empty path segments not allowed in subfolder path")
+
+ def validate_path(self, path: Union[str, Path]) -> bool:
+ """Validates the path given for an image or thumbnail."""
+ path = path if isinstance(path, Path) else Path(path)
+ return path.exists()
+
+ def get_workflow(self, image_name: str, image_subfolder: str = "") -> str | None:
+ image = self.get(image_name, image_subfolder=image_subfolder)
+ workflow = image.info.get("invokeai_workflow", None)
+ if isinstance(workflow, str):
+ return workflow
+ return None
+
+ def get_graph(self, image_name: str, image_subfolder: str = "") -> str | None:
+ image = self.get(image_name, image_subfolder=image_subfolder)
+ graph = image.info.get("invokeai_graph", None)
+ if isinstance(graph, str):
+ return graph
+ return None
+
+ def __validate_storage_folders(self) -> None:
+ """Checks if the required output folders exist and create them if they don't"""
+ folders: list[Path] = [self.__output_folder, self.__thumbnails_folder]
+ for folder in folders:
+ folder.mkdir(parents=True, exist_ok=True)
+
+ def __get_cache(self, image_name: Path) -> Optional[PILImageType]:
+ return None if image_name not in self.__cache else self.__cache[image_name]
+
+ def __set_cache(self, image_name: Path, image: PILImageType):
+ if image_name not in self.__cache:
+ self.__cache[image_name] = image
+ self.__cache_ids.put(image_name) # TODO: this should refresh position for LRU cache
+ if len(self.__cache) > self.__max_cache_size:
+ cache_id = self.__cache_ids.get()
+ if cache_id in self.__cache:
+ del self.__cache[cache_id]
diff --git a/invokeai/app/services/image_files/image_subfolder_strategy.py b/invokeai/app/services/image_files/image_subfolder_strategy.py
new file mode 100644
index 00000000000..66c363c4f95
--- /dev/null
+++ b/invokeai/app/services/image_files/image_subfolder_strategy.py
@@ -0,0 +1,58 @@
+from abc import ABC, abstractmethod
+from datetime import datetime
+
+from invokeai.app.services.image_records.image_records_common import ImageCategory
+
+
+class ImageSubfolderStrategy(ABC):
+ """Base class for image subfolder strategies."""
+
+ @abstractmethod
+ def get_subfolder(self, image_name: str, image_category: ImageCategory, is_intermediate: bool) -> str:
+ """Returns relative subfolder prefix (e.g. '2026/03/17', 'general'), or empty string for flat."""
+ pass
+
+
+class FlatStrategy(ImageSubfolderStrategy):
+ """No subfolders - all images in one directory (default behavior)."""
+
+ def get_subfolder(self, image_name: str, image_category: ImageCategory, is_intermediate: bool) -> str:
+ return ""
+
+
+class DateStrategy(ImageSubfolderStrategy):
+ """Organize images by date: YYYY/MM/DD."""
+
+ def get_subfolder(self, image_name: str, image_category: ImageCategory, is_intermediate: bool) -> str:
+ now = datetime.now()
+ return f"{now.year}/{now.month:02d}/{now.day:02d}"
+
+
+class TypeStrategy(ImageSubfolderStrategy):
+ """Organize images by category/type: general, intermediate, mask, control, etc."""
+
+ def get_subfolder(self, image_name: str, image_category: ImageCategory, is_intermediate: bool) -> str:
+ if is_intermediate:
+ return "intermediate"
+ return image_category.value
+
+
+class HashStrategy(ImageSubfolderStrategy):
+ """Organize images by UUID prefix for filesystem performance (first 2 characters)."""
+
+ def get_subfolder(self, image_name: str, image_category: ImageCategory, is_intermediate: bool) -> str:
+ return image_name[:2]
+
+
+def create_subfolder_strategy(strategy_name: str) -> ImageSubfolderStrategy:
+ """Factory function to create a subfolder strategy by name."""
+ strategies: dict[str, type[ImageSubfolderStrategy]] = {
+ "flat": FlatStrategy,
+ "date": DateStrategy,
+ "type": TypeStrategy,
+ "hash": HashStrategy,
+ }
+ cls = strategies.get(strategy_name)
+ if cls is None:
+ raise ValueError(f"Unknown subfolder strategy: {strategy_name}. Valid options: {', '.join(strategies.keys())}")
+ return cls()
diff --git a/invokeai/app/services/image_records/__init__.py b/invokeai/app/services/image_records/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/image_records/image_records_base.py b/invokeai/app/services/image_records/image_records_base.py
new file mode 100644
index 00000000000..8c71dfba9e7
--- /dev/null
+++ b/invokeai/app/services/image_records/image_records_base.py
@@ -0,0 +1,149 @@
+from abc import ABC, abstractmethod
+from datetime import datetime
+from typing import Optional
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ ImageNamesResult,
+ ImageRecord,
+ ImageRecordChanges,
+ ResourceOrigin,
+)
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
+
+
+class ImageRecordStorageBase(ABC):
+ """Low-level service responsible for interfacing with the image record store."""
+
+ # TODO: Implement an `update()` method
+
+ @abstractmethod
+ def get(self, image_name: str) -> ImageRecord:
+ """Gets an image record."""
+ pass
+
+ @abstractmethod
+ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
+ """Gets an image's metadata'."""
+ pass
+
+ @abstractmethod
+ def update(
+ self,
+ image_name: str,
+ changes: ImageRecordChanges,
+ ) -> None:
+ """Updates an image record."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[ImageRecord]:
+ """Gets a page of image records. When board_id is 'none', filters by user_id for per-user uncategorized images unless is_admin is True."""
+ pass
+
+ # TODO: The database has a nullable `deleted_at` column, currently unused.
+ # Should we implement soft deletes? Would need coordination with ImageFileStorage.
+ @abstractmethod
+ def delete(self, image_name: str) -> None:
+ """Deletes an image record."""
+ pass
+
+ @abstractmethod
+ def delete_many(self, image_names: list[str]) -> None:
+ """Deletes many image records."""
+ pass
+
+ @abstractmethod
+ def delete_intermediates(self) -> list[tuple[str, str]]:
+ """Deletes all intermediate image records, returning a list of (image_name, image_subfolder) tuples."""
+ pass
+
+ @abstractmethod
+ def get_intermediates_count(self, user_id: Optional[str] = None) -> int:
+ """Gets a count of intermediate images. If user_id is provided, only counts that user's intermediates."""
+ pass
+
+ @abstractmethod
+ def save(
+ self,
+ image_name: str,
+ image_origin: ResourceOrigin,
+ image_category: ImageCategory,
+ width: int,
+ height: int,
+ has_workflow: bool,
+ is_intermediate: Optional[bool] = False,
+ starred: Optional[bool] = False,
+ session_id: Optional[str] = None,
+ node_id: Optional[str] = None,
+ metadata: Optional[str] = None,
+ user_id: Optional[str] = None,
+ image_subfolder: str = "",
+ ) -> datetime:
+ """Saves an image record."""
+ pass
+
+ @abstractmethod
+ def get_user_id(self, image_name: str) -> Optional[str]:
+ """Gets the user_id of the image owner. Returns None if image not found."""
+ pass
+
+ @abstractmethod
+ def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
+ """Gets the most recent image for a board."""
+ pass
+
+ @abstractmethod
+ def get_image_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ """Gets ordered list of image names with metadata for optimistic updates."""
+ pass
+
+ @abstractmethod
+ def get_image_dates(
+ self,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> list[VirtualSubBoardDTO]:
+ """Gets a list of dates with image counts, grouped by DATE(created_at)."""
+ pass
+
+ @abstractmethod
+ def get_image_names_by_date(
+ self,
+ date: str,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ categories: Optional[list[ImageCategory]] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ """Gets ordered list of image names for a specific date."""
+ pass
diff --git a/invokeai/app/services/image_records/image_records_common.py b/invokeai/app/services/image_records/image_records_common.py
new file mode 100644
index 00000000000..3d4650b77ae
--- /dev/null
+++ b/invokeai/app/services/image_records/image_records_common.py
@@ -0,0 +1,235 @@
+# TODO: Should these excpetions subclass existing python exceptions?
+import datetime
+from enum import Enum
+from typing import Optional, Union
+
+from pydantic import BaseModel, Field, StrictBool, StrictStr
+
+from invokeai.app.util.metaenum import MetaEnum
+from invokeai.app.util.misc import get_iso_timestamp
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class ResourceOrigin(str, Enum, metaclass=MetaEnum):
+ """The origin of a resource (eg image).
+
+ - INTERNAL: The resource was created by the application.
+ - EXTERNAL: The resource was not created by the application.
+ This may be a user-initiated upload, or an internal application upload (eg Canvas init image).
+ """
+
+ INTERNAL = "internal"
+ """The resource was created by the application."""
+ EXTERNAL = "external"
+ """The resource was not created by the application.
+ This may be a user-initiated upload, or an internal application upload (eg Canvas init image).
+ """
+
+
+class InvalidOriginException(ValueError):
+ """Raised when a provided value is not a valid ResourceOrigin.
+
+ Subclasses `ValueError`.
+ """
+
+ def __init__(self, message="Invalid resource origin."):
+ super().__init__(message)
+
+
+class ImageCategory(str, Enum, metaclass=MetaEnum):
+ """The category of an image.
+
+ - GENERAL: The image is an output, init image, or otherwise an image without a specialized purpose.
+ - MASK: The image is a mask image.
+ - CONTROL: The image is a ControlNet control image.
+ - USER: The image is a user-provide image.
+ - OTHER: The image is some other type of image with a specialized purpose. To be used by external nodes.
+ """
+
+ GENERAL = "general"
+ """GENERAL: The image is an output, init image, or otherwise an image without a specialized purpose."""
+ MASK = "mask"
+ """MASK: The image is a mask image."""
+ CONTROL = "control"
+ """CONTROL: The image is a ControlNet control image."""
+ USER = "user"
+ """USER: The image is a user-provide image."""
+ OTHER = "other"
+ """OTHER: The image is some other type of image with a specialized purpose. To be used by external nodes."""
+
+
+IMAGE_CATEGORIES: list[ImageCategory] = [ImageCategory.GENERAL]
+ASSETS_CATEGORIES: list[ImageCategory] = [
+ ImageCategory.CONTROL,
+ ImageCategory.MASK,
+ ImageCategory.USER,
+ ImageCategory.OTHER,
+]
+
+
+class InvalidImageCategoryException(ValueError):
+ """Raised when a provided value is not a valid ImageCategory.
+
+ Subclasses `ValueError`.
+ """
+
+ def __init__(self, message="Invalid image category."):
+ super().__init__(message)
+
+
+class ImageRecordNotFoundException(Exception):
+ """Raised when an image record is not found."""
+
+ def __init__(self, message="Image record not found"):
+ super().__init__(message)
+
+
+class ImageRecordSaveException(Exception):
+ """Raised when an image record cannot be saved."""
+
+ def __init__(self, message="Image record not saved"):
+ super().__init__(message)
+
+
+class ImageRecordDeleteException(Exception):
+ """Raised when an image record cannot be deleted."""
+
+ def __init__(self, message="Image record not deleted"):
+ super().__init__(message)
+
+
+IMAGE_DTO_COLS = ", ".join(
+ [
+ "images." + c
+ for c in [
+ "image_name",
+ "image_origin",
+ "image_category",
+ "width",
+ "height",
+ "session_id",
+ "node_id",
+ "has_workflow",
+ "is_intermediate",
+ "created_at",
+ "updated_at",
+ "deleted_at",
+ "starred",
+ "image_subfolder",
+ ]
+ ]
+)
+
+
+class ImageRecord(BaseModelExcludeNull):
+ """Deserialized image record without metadata."""
+
+ image_name: str = Field(description="The unique name of the image.")
+ """The unique name of the image."""
+ image_origin: ResourceOrigin = Field(description="The type of the image.")
+ """The origin of the image."""
+ image_category: ImageCategory = Field(description="The category of the image.")
+ """The category of the image."""
+ width: int = Field(description="The width of the image in px.")
+ """The actual width of the image in px. This may be different from the width in metadata."""
+ height: int = Field(description="The height of the image in px.")
+ """The actual height of the image in px. This may be different from the height in metadata."""
+ created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the image.")
+ """The created timestamp of the image."""
+ updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the image.")
+ """The updated timestamp of the image."""
+ deleted_at: Optional[Union[datetime.datetime, str]] = Field(
+ default=None, description="The deleted timestamp of the image."
+ )
+ """The deleted timestamp of the image."""
+ is_intermediate: bool = Field(description="Whether this is an intermediate image.")
+ """Whether this is an intermediate image."""
+ session_id: Optional[str] = Field(
+ default=None,
+ description="The session ID that generated this image, if it is a generated image.",
+ )
+ """The session ID that generated this image, if it is a generated image."""
+ node_id: Optional[str] = Field(
+ default=None,
+ description="The node ID that generated this image, if it is a generated image.",
+ )
+ """The node ID that generated this image, if it is a generated image."""
+ starred: bool = Field(description="Whether this image is starred.")
+ """Whether this image is starred."""
+ has_workflow: bool = Field(description="Whether this image has a workflow.")
+ image_subfolder: str = Field(default="", description="The subfolder where the image is stored on disk.")
+
+
+class ImageRecordChanges(BaseModelExcludeNull, extra="allow"):
+ """A set of changes to apply to an image record.
+
+ Only limited changes are valid:
+ - `image_category`: change the category of an image
+ - `session_id`: change the session associated with an image
+ - `is_intermediate`: change the image's `is_intermediate` flag
+ - `starred`: change whether the image is starred
+ """
+
+ image_category: Optional[ImageCategory] = Field(default=None, description="The image's new category.")
+ """The image's new category."""
+ session_id: Optional[StrictStr] = Field(
+ default=None,
+ description="The image's new session ID.",
+ )
+ """The image's new session ID."""
+ is_intermediate: Optional[StrictBool] = Field(default=None, description="The image's new `is_intermediate` flag.")
+ """The image's new `is_intermediate` flag."""
+ starred: Optional[StrictBool] = Field(default=None, description="The image's new `starred` state")
+ """The image's new `starred` state."""
+
+
+def deserialize_image_record(image_dict: dict) -> ImageRecord:
+ """Deserializes an image record."""
+
+ # Retrieve all the values, setting "reasonable" defaults if they are not present.
+
+ # TODO: do we really need to handle default values here? ideally the data is the correct shape...
+ image_name = image_dict.get("image_name", "unknown")
+ image_origin = ResourceOrigin(image_dict.get("image_origin", ResourceOrigin.INTERNAL.value))
+ image_category = ImageCategory(image_dict.get("image_category", ImageCategory.GENERAL.value))
+ width = image_dict.get("width", 0)
+ height = image_dict.get("height", 0)
+ session_id = image_dict.get("session_id", None)
+ node_id = image_dict.get("node_id", None)
+ created_at = image_dict.get("created_at", get_iso_timestamp())
+ updated_at = image_dict.get("updated_at", get_iso_timestamp())
+ deleted_at = image_dict.get("deleted_at", get_iso_timestamp())
+ is_intermediate = image_dict.get("is_intermediate", False)
+ starred = image_dict.get("starred", False)
+ has_workflow = image_dict.get("has_workflow", False)
+ image_subfolder = image_dict.get("image_subfolder", "")
+
+ return ImageRecord(
+ image_name=image_name,
+ image_origin=image_origin,
+ image_category=image_category,
+ width=width,
+ height=height,
+ session_id=session_id,
+ node_id=node_id,
+ created_at=created_at,
+ updated_at=updated_at,
+ deleted_at=deleted_at,
+ is_intermediate=is_intermediate,
+ starred=starred,
+ has_workflow=has_workflow,
+ image_subfolder=image_subfolder,
+ )
+
+
+class ImageCollectionCounts(BaseModel):
+ starred_count: int = Field(description="The number of starred images in the collection.")
+ unstarred_count: int = Field(description="The number of unstarred images in the collection.")
+
+
+class ImageNamesResult(BaseModel):
+ """Response containing ordered image names with metadata for optimistic updates."""
+
+ image_names: list[str] = Field(description="Ordered list of image names")
+ starred_count: int = Field(description="Number of starred images (when starred_first=True)")
+ total_count: int = Field(description="Total number of images matching the query")
diff --git a/invokeai/app/services/image_records/image_records_sqlite.py b/invokeai/app/services/image_records/image_records_sqlite.py
new file mode 100644
index 00000000000..1eb3857dba6
--- /dev/null
+++ b/invokeai/app/services/image_records/image_records_sqlite.py
@@ -0,0 +1,651 @@
+import sqlite3
+from datetime import datetime
+from typing import Optional, Union, cast
+
+from invokeai.app.invocations.fields import MetadataField, MetadataFieldValidator
+from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
+from invokeai.app.services.image_records.image_records_common import (
+ IMAGE_DTO_COLS,
+ ImageCategory,
+ ImageNamesResult,
+ ImageRecord,
+ ImageRecordChanges,
+ ImageRecordDeleteException,
+ ImageRecordNotFoundException,
+ ImageRecordSaveException,
+ ResourceOrigin,
+ deserialize_image_record,
+)
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.virtual_boards.virtual_boards_common import VirtualSubBoardDTO
+
+
+class SqliteImageRecordStorage(ImageRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def get(self, image_name: str) -> ImageRecord:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ f"""--sql
+ SELECT {IMAGE_DTO_COLS} FROM images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ except sqlite3.Error as e:
+ raise ImageRecordNotFoundException from e
+
+ if not result:
+ raise ImageRecordNotFoundException
+
+ return deserialize_image_record(dict(result))
+
+ def get_user_id(self, image_name: str) -> Optional[str]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT user_id FROM images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+ if not result:
+ return None
+ return cast(Optional[str], dict(result).get("user_id"))
+
+ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ SELECT metadata FROM images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+
+ except sqlite3.Error as e:
+ raise ImageRecordNotFoundException from e
+
+ if not result:
+ raise ImageRecordNotFoundException
+
+ as_dict = dict(result)
+ metadata_raw = cast(Optional[str], as_dict.get("metadata", None))
+ return MetadataFieldValidator.validate_json(metadata_raw) if metadata_raw is not None else None
+
+ def update(
+ self,
+ image_name: str,
+ changes: ImageRecordChanges,
+ ) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ # Change the category of the image
+ if changes.image_category is not None:
+ cursor.execute(
+ """--sql
+ UPDATE images
+ SET image_category = ?
+ WHERE image_name = ?;
+ """,
+ (changes.image_category, image_name),
+ )
+
+ # Change the session associated with the image
+ if changes.session_id is not None:
+ cursor.execute(
+ """--sql
+ UPDATE images
+ SET session_id = ?
+ WHERE image_name = ?;
+ """,
+ (changes.session_id, image_name),
+ )
+
+ # Change the image's `is_intermediate`` flag
+ if changes.is_intermediate is not None:
+ cursor.execute(
+ """--sql
+ UPDATE images
+ SET is_intermediate = ?
+ WHERE image_name = ?;
+ """,
+ (changes.is_intermediate, image_name),
+ )
+
+ # Change the image's `starred`` state
+ if changes.starred is not None:
+ cursor.execute(
+ """--sql
+ UPDATE images
+ SET starred = ?
+ WHERE image_name = ?;
+ """,
+ (changes.starred, image_name),
+ )
+
+ except sqlite3.Error as e:
+ raise ImageRecordSaveException from e
+
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[ImageRecord]:
+ with self._db.transaction() as cursor:
+ # Manually build two queries - one for the count, one for the records
+ count_query = """--sql
+ SELECT COUNT(*)
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE 1=1
+ """
+
+ images_query = f"""--sql
+ SELECT {IMAGE_DTO_COLS}
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE 1=1
+ """
+
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ if image_origin is not None:
+ query_conditions += """--sql
+ AND images.image_origin = ?
+ """
+ query_params.append(image_origin.value)
+
+ if categories is not None:
+ # Convert the enum values to unique list of strings
+ category_strings = [c.value for c in set(categories)]
+ # Create the correct length of placeholders
+ placeholders = ",".join("?" * len(category_strings))
+
+ query_conditions += f"""--sql
+ AND images.image_category IN ( {placeholders} )
+ """
+
+ # Unpack the included categories into the query params
+ for c in category_strings:
+ query_params.append(c)
+
+ if is_intermediate is not None:
+ query_conditions += """--sql
+ AND images.is_intermediate = ?
+ """
+
+ query_params.append(is_intermediate)
+
+ # board_id of "none" is reserved for images without a board
+ if board_id == "none":
+ query_conditions += """--sql
+ AND board_images.board_id IS NULL
+ """
+ # For uncategorized images, filter by user_id to ensure per-user isolation
+ # Admin users can see all uncategorized images from all users
+ if user_id is not None and not is_admin:
+ query_conditions += """--sql
+ AND images.user_id = ?
+ """
+ query_params.append(user_id)
+ elif board_id is not None:
+ query_conditions += """--sql
+ AND board_images.board_id = ?
+ """
+ query_params.append(board_id)
+
+ # Search term condition
+ if search_term:
+ query_conditions += """--sql
+ AND (
+ images.metadata LIKE ?
+ OR images.created_at LIKE ?
+ )
+ """
+ query_params.append(f"%{search_term.lower()}%")
+ query_params.append(f"%{search_term.lower()}%")
+
+ if starred_first:
+ query_pagination = f"""--sql
+ ORDER BY images.starred DESC, images.created_at {order_dir.value} LIMIT ? OFFSET ?
+ """
+ else:
+ query_pagination = f"""--sql
+ ORDER BY images.created_at {order_dir.value} LIMIT ? OFFSET ?
+ """
+
+ # Final images query with pagination
+ images_query += query_conditions + query_pagination + ";"
+ # Add all the parameters
+ images_params = query_params.copy()
+ # Add the pagination parameters
+ images_params.extend([limit, offset])
+
+ # Build the list of images, deserializing each row
+ cursor.execute(images_query, images_params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+
+ images = [deserialize_image_record(dict(r)) for r in result]
+
+ # Set up and execute the count query, without pagination
+ count_query += query_conditions + ";"
+ count_params = query_params.copy()
+ cursor.execute(count_query, count_params)
+ count = cast(int, cursor.fetchone()[0])
+
+ return OffsetPaginatedResults(items=images, offset=offset, limit=limit, total=count)
+
+ def delete(self, image_name: str) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ DELETE FROM images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+ except sqlite3.Error as e:
+ raise ImageRecordDeleteException from e
+
+ def delete_many(self, image_names: list[str]) -> None:
+ with self._db.transaction() as cursor:
+ try:
+ placeholders = ",".join("?" for _ in image_names)
+
+ # Construct the SQLite query with the placeholders
+ query = f"DELETE FROM images WHERE image_name IN ({placeholders})"
+
+ # Execute the query with the list of IDs as parameters
+ cursor.execute(query, image_names)
+
+ except sqlite3.Error as e:
+ raise ImageRecordDeleteException from e
+
+ def get_intermediates_count(self, user_id: Optional[str] = None) -> int:
+ with self._db.transaction() as cursor:
+ query = "SELECT COUNT(*) FROM images WHERE is_intermediate = TRUE"
+ params: list[str] = []
+ if user_id is not None:
+ query += " AND user_id = ?"
+ params.append(user_id)
+ cursor.execute(query, params)
+ count = cast(int, cursor.fetchone()[0])
+ return count
+
+ def delete_intermediates(self) -> list[tuple[str, str]]:
+ """Deletes all intermediate image records.
+
+ Returns a list of (image_name, image_subfolder) tuples for file cleanup.
+ """
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ SELECT image_name, image_subfolder FROM images
+ WHERE is_intermediate = TRUE;
+ """
+ )
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ image_name_subfolder_pairs = [(r[0], r[1]) for r in result]
+ cursor.execute(
+ """--sql
+ DELETE FROM images
+ WHERE is_intermediate = TRUE;
+ """
+ )
+ except sqlite3.Error as e:
+ raise ImageRecordDeleteException from e
+ return image_name_subfolder_pairs
+
+ def save(
+ self,
+ image_name: str,
+ image_origin: ResourceOrigin,
+ image_category: ImageCategory,
+ width: int,
+ height: int,
+ has_workflow: bool,
+ is_intermediate: Optional[bool] = False,
+ starred: Optional[bool] = False,
+ session_id: Optional[str] = None,
+ node_id: Optional[str] = None,
+ metadata: Optional[str] = None,
+ user_id: Optional[str] = None,
+ image_subfolder: str = "",
+ ) -> datetime:
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO images (
+ image_name,
+ image_origin,
+ image_category,
+ width,
+ height,
+ node_id,
+ session_id,
+ metadata,
+ is_intermediate,
+ starred,
+ has_workflow,
+ user_id,
+ image_subfolder
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
+ """,
+ (
+ image_name,
+ image_origin.value,
+ image_category.value,
+ width,
+ height,
+ node_id,
+ session_id,
+ metadata,
+ is_intermediate,
+ starred,
+ has_workflow,
+ user_id or "system",
+ image_subfolder,
+ ),
+ )
+
+ cursor.execute(
+ """--sql
+ SELECT created_at
+ FROM images
+ WHERE image_name = ?;
+ """,
+ (image_name,),
+ )
+
+ created_at = datetime.fromisoformat(cursor.fetchone()[0])
+
+ except sqlite3.Error as e:
+ raise ImageRecordSaveException from e
+ return created_at
+
+ def get_most_recent_image_for_board(self, board_id: str) -> Optional[ImageRecord]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT images.*
+ FROM images
+ JOIN board_images ON images.image_name = board_images.image_name
+ WHERE board_images.board_id = ?
+ AND images.is_intermediate = FALSE
+ ORDER BY images.starred DESC, images.created_at DESC
+ LIMIT 1;
+ """,
+ (board_id,),
+ )
+
+ result = cast(Optional[sqlite3.Row], cursor.fetchone())
+
+ if result is None:
+ return None
+
+ return deserialize_image_record(dict(result))
+
+ def get_image_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ with self._db.transaction() as cursor:
+ # Build query conditions (reused for both starred count and image names queries)
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ if image_origin is not None:
+ query_conditions += """--sql
+ AND images.image_origin = ?
+ """
+ query_params.append(image_origin.value)
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ query_conditions += f"""--sql
+ AND images.image_category IN ( {placeholders} )
+ """
+ for c in category_strings:
+ query_params.append(c)
+
+ if is_intermediate is not None:
+ query_conditions += """--sql
+ AND images.is_intermediate = ?
+ """
+ query_params.append(is_intermediate)
+
+ if board_id == "none":
+ query_conditions += """--sql
+ AND board_images.board_id IS NULL
+ """
+ # For uncategorized images, filter by user_id to ensure per-user isolation
+ # Admin users can see all uncategorized images from all users
+ if user_id is not None and not is_admin:
+ query_conditions += """--sql
+ AND images.user_id = ?
+ """
+ query_params.append(user_id)
+ elif board_id is not None:
+ query_conditions += """--sql
+ AND board_images.board_id = ?
+ """
+ query_params.append(board_id)
+
+ if search_term:
+ query_conditions += """--sql
+ AND (
+ images.metadata LIKE ?
+ OR images.created_at LIKE ?
+ )
+ """
+ query_params.append(f"%{search_term.lower()}%")
+ query_params.append(f"%{search_term.lower()}%")
+
+ # Get starred count if starred_first is enabled
+ starred_count = 0
+ if starred_first:
+ starred_count_query = f"""--sql
+ SELECT COUNT(*)
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE images.starred = TRUE AND (1=1{query_conditions})
+ """
+ cursor.execute(starred_count_query, query_params)
+ starred_count = cast(int, cursor.fetchone()[0])
+
+ # Get all image names with proper ordering
+ if starred_first:
+ names_query = f"""--sql
+ SELECT images.image_name
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE 1=1{query_conditions}
+ ORDER BY images.starred DESC, images.created_at {order_dir.value}
+ """
+ else:
+ names_query = f"""--sql
+ SELECT images.image_name
+ FROM images
+ LEFT JOIN board_images ON board_images.image_name = images.image_name
+ WHERE 1=1{query_conditions}
+ ORDER BY images.created_at {order_dir.value}
+ """
+
+ cursor.execute(names_query, query_params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ image_names = [row[0] for row in result]
+
+ return ImageNamesResult(image_names=image_names, starred_count=starred_count, total_count=len(image_names))
+
+ def get_image_dates(
+ self,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> list[VirtualSubBoardDTO]:
+ with self._db.transaction() as cursor:
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ # Only non-intermediate images
+ query_conditions += """--sql
+ AND images.is_intermediate = 0
+ """
+
+ # User isolation for non-admin users
+ if user_id is not None and not is_admin:
+ query_conditions += """--sql
+ AND images.user_id = ?
+ """
+ query_params.append(user_id)
+
+ query = f"""--sql
+ SELECT
+ DATE(images.created_at) as date,
+ SUM(CASE WHEN images.image_category = 'general' THEN 1 ELSE 0 END) as image_count,
+ SUM(CASE WHEN images.image_category != 'general' THEN 1 ELSE 0 END) as asset_count,
+ (
+ SELECT i2.image_name FROM images i2
+ WHERE DATE(i2.created_at) = DATE(images.created_at)
+ AND i2.is_intermediate = 0
+ ORDER BY i2.created_at DESC LIMIT 1
+ ) as cover_image_name
+ FROM images
+ WHERE 1=1
+ {query_conditions}
+ GROUP BY DATE(images.created_at)
+ ORDER BY date DESC;
+ """
+
+ cursor.execute(query, query_params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+
+ return [
+ VirtualSubBoardDTO(
+ virtual_board_id=f"by_date:{dict(row)['date']}",
+ board_name=dict(row)["date"],
+ date=dict(row)["date"],
+ image_count=dict(row)["image_count"],
+ asset_count=dict(row)["asset_count"],
+ cover_image_name=dict(row)["cover_image_name"],
+ )
+ for row in result
+ ]
+
+ def get_image_names_by_date(
+ self,
+ date: str,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ categories: Optional[list[ImageCategory]] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ with self._db.transaction() as cursor:
+ query_conditions = ""
+ query_params: list[Union[int, str, bool]] = []
+
+ # Filter by date
+ query_conditions += """--sql
+ AND DATE(images.created_at) = ?
+ """
+ query_params.append(date)
+
+ # Only non-intermediate images
+ query_conditions += """--sql
+ AND images.is_intermediate = 0
+ """
+
+ if categories is not None:
+ category_strings = [c.value for c in set(categories)]
+ placeholders = ",".join("?" * len(category_strings))
+ query_conditions += f"""--sql
+ AND images.image_category IN ( {placeholders} )
+ """
+ for c in category_strings:
+ query_params.append(c)
+
+ # User isolation for non-admin users
+ if user_id is not None and not is_admin:
+ query_conditions += """--sql
+ AND images.user_id = ?
+ """
+ query_params.append(user_id)
+
+ if search_term:
+ query_conditions += """--sql
+ AND (
+ images.metadata LIKE ?
+ OR images.created_at LIKE ?
+ )
+ """
+ query_params.append(f"%{search_term.lower()}%")
+ query_params.append(f"%{search_term.lower()}%")
+
+ # Get starred count if starred_first is enabled
+ starred_count = 0
+ if starred_first:
+ starred_count_query = f"""--sql
+ SELECT COUNT(*)
+ FROM images
+ WHERE images.starred = TRUE AND (1=1{query_conditions})
+ """
+ cursor.execute(starred_count_query, query_params)
+ starred_count = cast(int, cursor.fetchone()[0])
+
+ # Get all image names with proper ordering
+ if starred_first:
+ names_query = f"""--sql
+ SELECT images.image_name
+ FROM images
+ WHERE 1=1{query_conditions}
+ ORDER BY images.starred DESC, images.created_at {order_dir.value}
+ """
+ else:
+ names_query = f"""--sql
+ SELECT images.image_name
+ FROM images
+ WHERE 1=1{query_conditions}
+ ORDER BY images.created_at {order_dir.value}
+ """
+
+ cursor.execute(names_query, query_params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ image_names = [row[0] for row in result]
+
+ return ImageNamesResult(image_names=image_names, starred_count=starred_count, total_count=len(image_names))
diff --git a/invokeai/app/services/images/__init__.py b/invokeai/app/services/images/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/images/images_base.py b/invokeai/app/services/images/images_base.py
new file mode 100644
index 00000000000..aebbead2f35
--- /dev/null
+++ b/invokeai/app/services/images/images_base.py
@@ -0,0 +1,169 @@
+from abc import ABC, abstractmethod
+from typing import Callable, Optional
+
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ ImageNamesResult,
+ ImageRecord,
+ ImageRecordChanges,
+ ResourceOrigin,
+)
+from invokeai.app.services.images.images_common import ImageDTO
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class ImageServiceABC(ABC):
+ """High-level service for image management."""
+
+ _on_changed_callbacks: list[Callable[[ImageDTO], None]]
+ _on_deleted_callbacks: list[Callable[[str], None]]
+
+ def __init__(self) -> None:
+ self._on_changed_callbacks = []
+ self._on_deleted_callbacks = []
+
+ def on_changed(self, on_changed: Callable[[ImageDTO], None]) -> None:
+ """Register a callback for when an image is changed"""
+ self._on_changed_callbacks.append(on_changed)
+
+ def on_deleted(self, on_deleted: Callable[[str], None]) -> None:
+ """Register a callback for when an image is deleted"""
+ self._on_deleted_callbacks.append(on_deleted)
+
+ def _on_changed(self, item: ImageDTO) -> None:
+ for callback in self._on_changed_callbacks:
+ callback(item)
+
+ def _on_deleted(self, item_id: str) -> None:
+ for callback in self._on_deleted_callbacks:
+ callback(item_id)
+
+ @abstractmethod
+ def create(
+ self,
+ image: PILImageType,
+ image_origin: ResourceOrigin,
+ image_category: ImageCategory,
+ node_id: Optional[str] = None,
+ session_id: Optional[str] = None,
+ board_id: Optional[str] = None,
+ is_intermediate: Optional[bool] = False,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ user_id: Optional[str] = None,
+ ) -> ImageDTO:
+ """Creates an image, storing the file and its metadata."""
+ pass
+
+ @abstractmethod
+ def update(
+ self,
+ image_name: str,
+ changes: ImageRecordChanges,
+ ) -> ImageDTO:
+ """Updates an image."""
+ pass
+
+ @abstractmethod
+ def get_pil_image(self, image_name: str) -> PILImageType:
+ """Gets an image as a PIL image."""
+ pass
+
+ @abstractmethod
+ def get_record(self, image_name: str) -> ImageRecord:
+ """Gets an image record."""
+ pass
+
+ @abstractmethod
+ def get_dto(self, image_name: str) -> ImageDTO:
+ """Gets an image DTO."""
+ pass
+
+ @abstractmethod
+ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
+ """Gets an image's metadata."""
+ pass
+
+ @abstractmethod
+ def get_workflow(self, image_name: str) -> Optional[str]:
+ """Gets an image's workflow."""
+ pass
+
+ @abstractmethod
+ def get_graph(self, image_name: str) -> Optional[str]:
+ """Gets an image's workflow."""
+ pass
+
+ @abstractmethod
+ def get_path(self, image_name: str, thumbnail: bool = False) -> str:
+ """Gets an image's path."""
+ pass
+
+ @abstractmethod
+ def validate_path(self, path: str) -> bool:
+ """Validates an image's path."""
+ pass
+
+ @abstractmethod
+ def get_url(self, image_name: str, thumbnail: bool = False) -> str:
+ """Gets an image's or thumbnail's URL."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[ImageDTO]:
+ """Gets a paginated list of image DTOs with starred images first when starred_first=True."""
+ pass
+
+ @abstractmethod
+ def delete(self, image_name: str):
+ """Deletes an image."""
+ pass
+
+ @abstractmethod
+ def delete_intermediates(self) -> int:
+ """Deletes all intermediate images."""
+ pass
+
+ @abstractmethod
+ def get_intermediates_count(self, user_id: Optional[str] = None) -> int:
+ """Gets the number of intermediate images. If user_id is provided, only counts that user's intermediates."""
+ pass
+
+ @abstractmethod
+ def delete_images_on_board(self, board_id: str):
+ """Deletes all images on a board."""
+ pass
+
+ @abstractmethod
+ def get_image_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ """Gets ordered list of image names with metadata for optimistic updates."""
+ pass
diff --git a/invokeai/app/services/images/images_common.py b/invokeai/app/services/images/images_common.py
new file mode 100644
index 00000000000..311f6e556d2
--- /dev/null
+++ b/invokeai/app/services/images/images_common.py
@@ -0,0 +1,65 @@
+from typing import Optional
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.services.image_records.image_records_common import ImageRecord
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class ImageUrlsDTO(BaseModelExcludeNull):
+ """The URLs for an image and its thumbnail."""
+
+ image_name: str = Field(description="The unique name of the image.")
+ """The unique name of the image."""
+ image_url: str = Field(description="The URL of the image.")
+ """The URL of the image."""
+ thumbnail_url: str = Field(description="The URL of the image's thumbnail.")
+ """The URL of the image's thumbnail."""
+
+
+class ImageDTO(ImageRecord, ImageUrlsDTO):
+ """Deserialized image record, enriched for the frontend."""
+
+ board_id: Optional[str] = Field(
+ default=None, description="The id of the board the image belongs to, if one exists."
+ )
+ """The id of the board the image belongs to, if one exists."""
+
+
+def image_record_to_dto(
+ image_record: ImageRecord,
+ image_url: str,
+ thumbnail_url: str,
+ board_id: Optional[str],
+) -> ImageDTO:
+ """Converts an image record to an image DTO."""
+ return ImageDTO(
+ **image_record.model_dump(),
+ image_url=image_url,
+ thumbnail_url=thumbnail_url,
+ board_id=board_id,
+ )
+
+
+class ResultWithAffectedBoards(BaseModel):
+ affected_boards: list[str] = Field(description="The ids of boards affected by the delete operation")
+
+
+class DeleteImagesResult(ResultWithAffectedBoards):
+ deleted_images: list[str] = Field(description="The names of the images that were deleted")
+
+
+class StarredImagesResult(ResultWithAffectedBoards):
+ starred_images: list[str] = Field(description="The names of the images that were starred")
+
+
+class UnstarredImagesResult(ResultWithAffectedBoards):
+ unstarred_images: list[str] = Field(description="The names of the images that were unstarred")
+
+
+class AddImagesToBoardResult(ResultWithAffectedBoards):
+ added_images: list[str] = Field(description="The image names that were added to the board")
+
+
+class RemoveImagesFromBoardResult(ResultWithAffectedBoards):
+ removed_images: list[str] = Field(description="The image names that were removed from their board")
diff --git a/invokeai/app/services/images/images_default.py b/invokeai/app/services/images/images_default.py
new file mode 100644
index 00000000000..4a190f37edc
--- /dev/null
+++ b/invokeai/app/services/images/images_default.py
@@ -0,0 +1,371 @@
+from typing import Optional
+
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.invocations.fields import MetadataField
+from invokeai.app.services.image_files.image_files_common import (
+ ImageFileDeleteException,
+ ImageFileNotFoundException,
+ ImageFileSaveException,
+)
+from invokeai.app.services.image_files.image_subfolder_strategy import create_subfolder_strategy
+from invokeai.app.services.image_records.image_records_common import (
+ ImageCategory,
+ ImageNamesResult,
+ ImageRecord,
+ ImageRecordChanges,
+ ImageRecordDeleteException,
+ ImageRecordNotFoundException,
+ ImageRecordSaveException,
+ InvalidImageCategoryException,
+ InvalidOriginException,
+ ResourceOrigin,
+)
+from invokeai.app.services.images.images_base import ImageServiceABC
+from invokeai.app.services.images.images_common import ImageDTO, image_record_to_dto
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.pagination import OffsetPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class ImageService(ImageServiceABC):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def create(
+ self,
+ image: PILImageType,
+ image_origin: ResourceOrigin,
+ image_category: ImageCategory,
+ node_id: Optional[str] = None,
+ session_id: Optional[str] = None,
+ board_id: Optional[str] = None,
+ is_intermediate: Optional[bool] = False,
+ metadata: Optional[str] = None,
+ workflow: Optional[str] = None,
+ graph: Optional[str] = None,
+ user_id: Optional[str] = None,
+ ) -> ImageDTO:
+ if image_origin not in ResourceOrigin:
+ raise InvalidOriginException
+
+ if image_category not in ImageCategory:
+ raise InvalidImageCategoryException
+
+ image_name = self.__invoker.services.names.create_image_name()
+
+ # Compute subfolder based on configured strategy
+ strategy_name = self.__invoker.services.configuration.image_subfolder_strategy
+ strategy = create_subfolder_strategy(strategy_name)
+ image_subfolder = strategy.get_subfolder(image_name, image_category, is_intermediate or False)
+
+ (width, height) = image.size
+
+ try:
+ # TODO: Consider using a transaction here to ensure consistency between storage and database
+ self.__invoker.services.image_records.save(
+ # Non-nullable fields
+ image_name=image_name,
+ image_origin=image_origin,
+ image_category=image_category,
+ width=width,
+ height=height,
+ has_workflow=workflow is not None or graph is not None,
+ # Meta fields
+ is_intermediate=is_intermediate,
+ # Nullable fields
+ node_id=node_id,
+ metadata=metadata,
+ session_id=session_id,
+ user_id=user_id,
+ image_subfolder=image_subfolder,
+ )
+ if board_id is not None:
+ try:
+ self.__invoker.services.board_image_records.add_image_to_board(
+ board_id=board_id, image_name=image_name
+ )
+ except Exception as e:
+ self.__invoker.services.logger.warning(f"Failed to add image to board {board_id}: {str(e)}")
+ self.__invoker.services.image_files.save(
+ image_name=image_name,
+ image=image,
+ metadata=metadata,
+ workflow=workflow,
+ graph=graph,
+ image_subfolder=image_subfolder,
+ )
+ image_dto = self.get_dto(image_name)
+
+ self._on_changed(image_dto)
+ return image_dto
+ except ImageRecordSaveException:
+ self.__invoker.services.logger.error("Failed to save image record")
+ raise
+ except ImageFileSaveException:
+ self.__invoker.services.logger.error("Failed to save image file")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error(f"Problem saving image record and file: {str(e)}")
+ raise e
+
+ def update(
+ self,
+ image_name: str,
+ changes: ImageRecordChanges,
+ ) -> ImageDTO:
+ try:
+ self.__invoker.services.image_records.update(image_name, changes)
+ image_dto = self.get_dto(image_name)
+ self._on_changed(image_dto)
+ return image_dto
+ except ImageRecordSaveException:
+ self.__invoker.services.logger.error("Failed to update image record")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem updating image record")
+ raise e
+
+ def get_pil_image(self, image_name: str) -> PILImageType:
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ return self.__invoker.services.image_files.get(image_name, image_subfolder=record.image_subfolder)
+ except ImageFileNotFoundException:
+ self.__invoker.services.logger.error("Failed to get image file")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image file")
+ raise e
+
+ def get_record(self, image_name: str) -> ImageRecord:
+ try:
+ return self.__invoker.services.image_records.get(image_name)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.error("Image record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image record")
+ raise e
+
+ def get_dto(self, image_name: str) -> ImageDTO:
+ try:
+ image_record = self.__invoker.services.image_records.get(image_name)
+
+ image_dto = image_record_to_dto(
+ image_record=image_record,
+ image_url=self.__invoker.services.urls.get_image_url(image_name),
+ thumbnail_url=self.__invoker.services.urls.get_image_url(image_name, True),
+ board_id=self.__invoker.services.board_image_records.get_board_for_image(image_name),
+ )
+
+ return image_dto
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.error("Image record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image DTO")
+ raise e
+
+ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
+ try:
+ return self.__invoker.services.image_records.get_metadata(image_name)
+ except ImageRecordNotFoundException:
+ self.__invoker.services.logger.error("Image record not found")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image metadata")
+ raise e
+
+ def get_workflow(self, image_name: str) -> Optional[str]:
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ return self.__invoker.services.image_files.get_workflow(image_name, image_subfolder=record.image_subfolder)
+ except ImageFileNotFoundException:
+ self.__invoker.services.logger.error("Image file not found")
+ raise
+ except Exception:
+ self.__invoker.services.logger.error("Problem getting image workflow")
+ raise
+
+ def get_graph(self, image_name: str) -> Optional[str]:
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ return self.__invoker.services.image_files.get_graph(image_name, image_subfolder=record.image_subfolder)
+ except ImageFileNotFoundException:
+ self.__invoker.services.logger.error("Image file not found")
+ raise
+ except Exception:
+ self.__invoker.services.logger.error("Problem getting image graph")
+ raise
+
+ def get_path(self, image_name: str, thumbnail: bool = False) -> str:
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ return str(
+ self.__invoker.services.image_files.get_path(
+ image_name, thumbnail, image_subfolder=record.image_subfolder
+ )
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image path")
+ raise e
+
+ def validate_path(self, path: str) -> bool:
+ try:
+ return self.__invoker.services.image_files.validate_path(path)
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem validating image path")
+ raise e
+
+ def get_url(self, image_name: str, thumbnail: bool = False) -> str:
+ try:
+ return self.__invoker.services.urls.get_image_url(image_name, thumbnail)
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image path")
+ raise e
+
+ def get_many(
+ self,
+ offset: int = 0,
+ limit: int = 10,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> OffsetPaginatedResults[ImageDTO]:
+ try:
+ results = self.__invoker.services.image_records.get_many(
+ offset,
+ limit,
+ starred_first,
+ order_dir,
+ image_origin,
+ categories,
+ is_intermediate,
+ board_id,
+ search_term,
+ user_id,
+ is_admin,
+ )
+
+ image_dtos = [
+ image_record_to_dto(
+ image_record=r,
+ image_url=self.__invoker.services.urls.get_image_url(r.image_name),
+ thumbnail_url=self.__invoker.services.urls.get_image_url(r.image_name, True),
+ board_id=self.__invoker.services.board_image_records.get_board_for_image(r.image_name),
+ )
+ for r in results.items
+ ]
+
+ return OffsetPaginatedResults[ImageDTO](
+ items=image_dtos,
+ offset=results.offset,
+ limit=results.limit,
+ total=results.total,
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting paginated image DTOs")
+ raise e
+
+ def delete(self, image_name: str):
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ self.__invoker.services.image_files.delete(image_name, image_subfolder=record.image_subfolder)
+ self.__invoker.services.image_records.delete(image_name)
+ self._on_deleted(image_name)
+ except ImageRecordDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image record")
+ raise
+ except ImageFileDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image file")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem deleting image record and file")
+ raise e
+
+ def delete_images_on_board(self, board_id: str):
+ try:
+ image_names = self.__invoker.services.board_image_records.get_all_board_image_names_for_board(
+ board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+ for image_name in image_names:
+ try:
+ record = self.__invoker.services.image_records.get(image_name)
+ self.__invoker.services.image_files.delete(image_name, image_subfolder=record.image_subfolder)
+ except Exception:
+ pass
+ self.__invoker.services.image_records.delete_many(image_names)
+ for image_name in image_names:
+ self._on_deleted(image_name)
+ except ImageRecordDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image records")
+ raise
+ except ImageFileDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image files")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error(f"Problem deleting image records and files: {str(e)}")
+ raise e
+
+ def delete_intermediates(self) -> int:
+ try:
+ image_name_subfolder_pairs = self.__invoker.services.image_records.delete_intermediates()
+ count = len(image_name_subfolder_pairs)
+ for image_name, image_subfolder in image_name_subfolder_pairs:
+ self.__invoker.services.image_files.delete(image_name, image_subfolder=image_subfolder)
+ self._on_deleted(image_name)
+ return count
+ except ImageRecordDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image records")
+ raise
+ except ImageFileDeleteException:
+ self.__invoker.services.logger.error("Failed to delete image files")
+ raise
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem deleting image records and files")
+ raise e
+
+ def get_intermediates_count(self, user_id: Optional[str] = None) -> int:
+ try:
+ return self.__invoker.services.image_records.get_intermediates_count(user_id=user_id)
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting intermediates count")
+ raise e
+
+ def get_image_names(
+ self,
+ starred_first: bool = True,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ image_origin: Optional[ResourceOrigin] = None,
+ categories: Optional[list[ImageCategory]] = None,
+ is_intermediate: Optional[bool] = None,
+ board_id: Optional[str] = None,
+ search_term: Optional[str] = None,
+ user_id: Optional[str] = None,
+ is_admin: bool = False,
+ ) -> ImageNamesResult:
+ try:
+ return self.__invoker.services.image_records.get_image_names(
+ starred_first=starred_first,
+ order_dir=order_dir,
+ image_origin=image_origin,
+ categories=categories,
+ is_intermediate=is_intermediate,
+ board_id=board_id,
+ search_term=search_term,
+ user_id=user_id,
+ is_admin=is_admin,
+ )
+ except Exception as e:
+ self.__invoker.services.logger.error("Problem getting image names")
+ raise e
diff --git a/invokeai/app/services/invocation_cache/__init__.py b/invokeai/app/services/invocation_cache/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/invocation_cache/invocation_cache_base.py b/invokeai/app/services/invocation_cache/invocation_cache_base.py
new file mode 100644
index 00000000000..bde6a0f1146
--- /dev/null
+++ b/invokeai/app/services/invocation_cache/invocation_cache_base.py
@@ -0,0 +1,63 @@
+from abc import ABC, abstractmethod
+from typing import Optional, Union
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput
+from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
+
+
+class InvocationCacheBase(ABC):
+ """
+ Base class for invocation caches.
+ When an invocation is executed, it is hashed and its output stored in the cache.
+ When new invocations are executed, if they are flagged with `use_cache`, they
+ will attempt to pull their value from the cache before executing.
+
+ Implementations should register for the `on_deleted` event of the `images` and `latents`
+ services, and delete any cached outputs that reference the deleted image or latent.
+
+ See the memory implementation for an example.
+
+ Implementations should respect the `node_cache_size` configuration value, and skip all
+ cache logic if the value is set to 0.
+ """
+
+ @abstractmethod
+ def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]:
+ """Retrieves an invocation output from the cache"""
+ pass
+
+ @abstractmethod
+ def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None:
+ """Stores an invocation output in the cache"""
+ pass
+
+ @abstractmethod
+ def delete(self, key: Union[int, str]) -> None:
+ """Deletes an invocation output from the cache"""
+ pass
+
+ @abstractmethod
+ def clear(self) -> None:
+ """Clears the cache"""
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def create_key(invocation: BaseInvocation) -> int:
+ """Gets the key for the invocation's cache item"""
+ pass
+
+ @abstractmethod
+ def disable(self) -> None:
+ """Disables the cache, overriding the max cache size"""
+ pass
+
+ @abstractmethod
+ def enable(self) -> None:
+ """Enables the cache, letting the the max cache size take effect"""
+ pass
+
+ @abstractmethod
+ def get_status(self) -> InvocationCacheStatus:
+ """Returns the status of the cache"""
+ pass
diff --git a/invokeai/app/services/invocation_cache/invocation_cache_common.py b/invokeai/app/services/invocation_cache/invocation_cache_common.py
new file mode 100644
index 00000000000..6ce2d02f3b4
--- /dev/null
+++ b/invokeai/app/services/invocation_cache/invocation_cache_common.py
@@ -0,0 +1,9 @@
+from pydantic import BaseModel, Field
+
+
+class InvocationCacheStatus(BaseModel):
+ size: int = Field(description="The current size of the invocation cache")
+ hits: int = Field(description="The number of cache hits")
+ misses: int = Field(description="The number of cache misses")
+ enabled: bool = Field(description="Whether the invocation cache is enabled")
+ max_size: int = Field(description="The maximum size of the invocation cache")
diff --git a/invokeai/app/services/invocation_cache/invocation_cache_memory.py b/invokeai/app/services/invocation_cache/invocation_cache_memory.py
new file mode 100644
index 00000000000..d15269caf91
--- /dev/null
+++ b/invokeai/app/services/invocation_cache/invocation_cache_memory.py
@@ -0,0 +1,130 @@
+from collections import OrderedDict
+from dataclasses import dataclass, field
+from threading import Lock
+from typing import Optional, Union
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput
+from invokeai.app.services.invocation_cache.invocation_cache_base import InvocationCacheBase
+from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
+from invokeai.app.services.invoker import Invoker
+
+
+@dataclass(order=True)
+class CachedItem:
+ invocation_output: BaseInvocationOutput = field(compare=False)
+ invocation_output_json: str = field(compare=False)
+
+
+class MemoryInvocationCache(InvocationCacheBase):
+ _cache: OrderedDict[Union[int, str], CachedItem]
+ _max_cache_size: int
+ _disabled: bool
+ _hits: int
+ _misses: int
+ _invoker: Invoker
+ _lock: Lock
+
+ def __init__(self, max_cache_size: int = 0) -> None:
+ self._cache = OrderedDict()
+ self._max_cache_size = max_cache_size
+ self._disabled = False
+ self._hits = 0
+ self._misses = 0
+ self._lock = Lock()
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+ if self._max_cache_size == 0:
+ return
+ self._invoker.services.images.on_deleted(self._delete_by_match)
+ self._invoker.services.tensors.on_deleted(self._delete_by_match)
+ self._invoker.services.conditioning.on_deleted(self._delete_by_match)
+
+ def get(self, key: Union[int, str]) -> Optional[BaseInvocationOutput]:
+ with self._lock:
+ if self._max_cache_size == 0 or self._disabled:
+ return None
+ item = self._cache.get(key, None)
+ if item is not None:
+ self._hits += 1
+ self._cache.move_to_end(key)
+ return item.invocation_output
+ self._misses += 1
+ return None
+
+ def save(self, key: Union[int, str], invocation_output: BaseInvocationOutput) -> None:
+ with self._lock:
+ if self._max_cache_size == 0 or self._disabled or key in self._cache:
+ return
+ # If the cache is full, we need to remove the least used
+ number_to_delete = len(self._cache) + 1 - self._max_cache_size
+ self._delete_oldest_access(number_to_delete)
+ self._cache[key] = CachedItem(
+ invocation_output,
+ invocation_output.model_dump_json(warnings=False, exclude_defaults=True, exclude_unset=True),
+ )
+
+ def _delete_oldest_access(self, number_to_delete: int) -> None:
+ number_to_delete = min(number_to_delete, len(self._cache))
+ for _ in range(number_to_delete):
+ self._cache.popitem(last=False)
+
+ def _delete(self, key: Union[int, str]) -> None:
+ if self._max_cache_size == 0:
+ return
+ if key in self._cache:
+ del self._cache[key]
+
+ def delete(self, key: Union[int, str]) -> None:
+ with self._lock:
+ return self._delete(key)
+
+ def clear(self) -> None:
+ with self._lock:
+ if self._max_cache_size == 0:
+ return
+ self._cache.clear()
+ self._misses = 0
+ self._hits = 0
+
+ @staticmethod
+ def create_key(invocation: BaseInvocation) -> int:
+ return hash(invocation.model_dump_json(exclude={"id"}, warnings=False))
+
+ def disable(self) -> None:
+ with self._lock:
+ if self._max_cache_size == 0:
+ return
+ self._disabled = True
+
+ def enable(self) -> None:
+ with self._lock:
+ if self._max_cache_size == 0:
+ return
+ self._disabled = False
+
+ def get_status(self) -> InvocationCacheStatus:
+ with self._lock:
+ return InvocationCacheStatus(
+ hits=self._hits,
+ misses=self._misses,
+ enabled=not self._disabled and self._max_cache_size > 0,
+ size=len(self._cache),
+ max_size=self._max_cache_size,
+ )
+
+ def _delete_by_match(self, to_match: str) -> None:
+ with self._lock:
+ if self._max_cache_size == 0:
+ return
+ keys_to_delete = set()
+ for key, cached_item in self._cache.items():
+ if to_match in cached_item.invocation_output_json:
+ keys_to_delete.add(key)
+ if not keys_to_delete:
+ return
+ for key in keys_to_delete:
+ self._delete(key)
+ self._invoker.services.logger.debug(
+ f"Deleted {len(keys_to_delete)} cached invocation outputs for {to_match}"
+ )
diff --git a/invokeai/app/services/invocation_services.py b/invokeai/app/services/invocation_services.py
new file mode 100644
index 00000000000..2c95f87b41d
--- /dev/null
+++ b/invokeai/app/services/invocation_services.py
@@ -0,0 +1,113 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654) and the InvokeAI Team
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase
+from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase
+from invokeai.app.services.style_preset_records.style_preset_records_base import StylePresetRecordsStorageBase
+
+if TYPE_CHECKING:
+ from logging import Logger
+
+ import torch
+
+ from invokeai.app.services.board_image_records.board_image_records_base import BoardImageRecordStorageBase
+ from invokeai.app.services.board_images.board_images_base import BoardImagesServiceABC
+ from invokeai.app.services.board_records.board_records_base import BoardRecordStorageBase
+ from invokeai.app.services.boards.boards_base import BoardServiceABC
+ from invokeai.app.services.bulk_download.bulk_download_base import BulkDownloadBase
+ from invokeai.app.services.client_state_persistence.client_state_persistence_base import ClientStatePersistenceABC
+ from invokeai.app.services.config import InvokeAIAppConfig
+ from invokeai.app.services.download import DownloadQueueServiceBase
+ from invokeai.app.services.events.events_base import EventServiceBase
+ from invokeai.app.services.external_generation.external_generation_base import ExternalGenerationServiceBase
+ from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
+ from invokeai.app.services.image_records.image_records_base import ImageRecordStorageBase
+ from invokeai.app.services.images.images_base import ImageServiceABC
+ from invokeai.app.services.invocation_cache.invocation_cache_base import InvocationCacheBase
+ from invokeai.app.services.invocation_stats.invocation_stats_base import InvocationStatsServiceBase
+ from invokeai.app.services.model_images.model_images_base import ModelImageFileStorageBase
+ from invokeai.app.services.model_manager.model_manager_base import ModelManagerServiceBase
+ from invokeai.app.services.model_relationship_records.model_relationship_records_base import (
+ ModelRelationshipRecordStorageBase,
+ )
+ from invokeai.app.services.model_relationships.model_relationships_base import ModelRelationshipsServiceABC
+ from invokeai.app.services.names.names_base import NameServiceBase
+ from invokeai.app.services.session_processor.session_processor_base import SessionProcessorBase
+ from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
+ from invokeai.app.services.urls.urls_base import UrlServiceBase
+ from invokeai.app.services.users.users_base import UserServiceBase
+ from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
+ from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_base import WorkflowThumbnailServiceBase
+ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
+
+
+class InvocationServices:
+ """Services that can be used by invocations"""
+
+ def __init__(
+ self,
+ board_images: "BoardImagesServiceABC",
+ board_image_records: "BoardImageRecordStorageBase",
+ boards: "BoardServiceABC",
+ board_records: "BoardRecordStorageBase",
+ bulk_download: "BulkDownloadBase",
+ configuration: "InvokeAIAppConfig",
+ events: "EventServiceBase",
+ images: "ImageServiceABC",
+ image_files: "ImageFileStorageBase",
+ image_records: "ImageRecordStorageBase",
+ logger: "Logger",
+ model_images: "ModelImageFileStorageBase",
+ model_manager: "ModelManagerServiceBase",
+ model_relationships: "ModelRelationshipsServiceABC",
+ model_relationship_records: "ModelRelationshipRecordStorageBase",
+ download_queue: "DownloadQueueServiceBase",
+ external_generation: "ExternalGenerationServiceBase",
+ performance_statistics: "InvocationStatsServiceBase",
+ session_queue: "SessionQueueBase",
+ session_processor: "SessionProcessorBase",
+ invocation_cache: "InvocationCacheBase",
+ names: "NameServiceBase",
+ urls: "UrlServiceBase",
+ workflow_records: "WorkflowRecordsStorageBase",
+ tensors: "ObjectSerializerBase[torch.Tensor]",
+ conditioning: "ObjectSerializerBase[ConditioningFieldData]",
+ style_preset_records: "StylePresetRecordsStorageBase",
+ style_preset_image_files: "StylePresetImageFileStorageBase",
+ workflow_thumbnails: "WorkflowThumbnailServiceBase",
+ client_state_persistence: "ClientStatePersistenceABC",
+ users: "UserServiceBase",
+ ):
+ self.board_images = board_images
+ self.board_image_records = board_image_records
+ self.boards = boards
+ self.board_records = board_records
+ self.bulk_download = bulk_download
+ self.configuration = configuration
+ self.events = events
+ self.images = images
+ self.image_files = image_files
+ self.image_records = image_records
+ self.logger = logger
+ self.model_images = model_images
+ self.model_manager = model_manager
+ self.model_relationships = model_relationships
+ self.model_relationship_records = model_relationship_records
+ self.download_queue = download_queue
+ self.external_generation = external_generation
+ self.performance_statistics = performance_statistics
+ self.session_queue = session_queue
+ self.session_processor = session_processor
+ self.invocation_cache = invocation_cache
+ self.names = names
+ self.urls = urls
+ self.workflow_records = workflow_records
+ self.tensors = tensors
+ self.conditioning = conditioning
+ self.style_preset_records = style_preset_records
+ self.style_preset_image_files = style_preset_image_files
+ self.workflow_thumbnails = workflow_thumbnails
+ self.client_state_persistence = client_state_persistence
+ self.users = users
diff --git a/invokeai/app/services/invocation_stats/__init__.py b/invokeai/app/services/invocation_stats/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/invocation_stats/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py
new file mode 100644
index 00000000000..1ada23c79b3
--- /dev/null
+++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py
@@ -0,0 +1,93 @@
+# Copyright 2023 Lincoln D. Stein
+"""Utility to collect execution time and GPU usage stats on invocations in flight
+
+Usage:
+
+statistics = InvocationStatsService()
+with statistics.collect_stats(invocation, graph_execution_state.id):
+ ... execute graphs...
+statistics.log_stats()
+
+Typical output:
+[2023-08-02 18:03:04,507]::[InvokeAI]::INFO --> Graph stats: c7764585-9c68-4d9d-a199-55e8186790f3
+[2023-08-02 18:03:04,507]::[InvokeAI]::INFO --> Node Calls Seconds VRAM Used
+[2023-08-02 18:03:04,507]::[InvokeAI]::INFO --> main_model_loader 1 0.005s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> clip_skip 1 0.004s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> compel 2 0.512s 0.26G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> rand_int 1 0.001s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> range_of_size 1 0.001s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> iterate 1 0.001s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> metadata_accumulator 1 0.002s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> noise 1 0.002s 0.01G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> t2l 1 3.541s 1.93G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> l2i 1 0.679s 0.58G
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> TOTAL GRAPH EXECUTION TIME: 4.749s
+[2023-08-02 18:03:04,508]::[InvokeAI]::INFO --> Current VRAM utilization 0.01G
+
+The abstract base class for this class is InvocationStatsServiceBase. An implementing class which
+writes to the system log is stored in InvocationServices.performance_statistics.
+"""
+
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import ContextManager
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation
+from invokeai.app.services.invocation_stats.invocation_stats_common import InvocationStatsSummary
+
+
+class InvocationStatsServiceBase(ABC):
+ "Abstract base class for recording node memory/time performance statistics"
+
+ @abstractmethod
+ def __init__(self) -> None:
+ """
+ Initialize the InvocationStatsService and reset counters to zero
+ """
+
+ @abstractmethod
+ def collect_stats(
+ self,
+ invocation: BaseInvocation,
+ graph_execution_state_id: str,
+ ) -> ContextManager[None]:
+ """
+ Return a context object that will capture the statistics on the execution
+ of invocaation. Use with: to place around the part of the code that executes the invocation.
+ :param invocation: BaseInvocation object from the current graph.
+ :param graph_execution_state_id: The id of the current session.
+ """
+ pass
+
+ @abstractmethod
+ def reset_stats(self, graph_execution_state_id: str) -> None:
+ """Reset all stored statistics."""
+ pass
+
+ @abstractmethod
+ def log_stats(self, graph_execution_state_id: str) -> None:
+ """
+ Write out the accumulated statistics to the log or somewhere else.
+ :param graph_execution_state_id: The id of the session whose stats to log.
+ :raises GESStatsNotFoundError: if the graph isn't tracked in the stats.
+ """
+ pass
+
+ @abstractmethod
+ def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary:
+ """
+ Gets the accumulated statistics for the indicated graph.
+ :param graph_execution_state_id: The id of the session whose stats to get.
+ :raises GESStatsNotFoundError: if the graph isn't tracked in the stats.
+ """
+ pass
+
+ @abstractmethod
+ def dump_stats(self, graph_execution_state_id: str, output_path: Path) -> None:
+ """
+ Write out the accumulated statistics to the indicated path as JSON.
+ :param graph_execution_state_id: The id of the session whose stats to dump.
+ :param output_path: The file to write the stats to.
+ :raises GESStatsNotFoundError: if the graph isn't tracked in the stats.
+ """
+ pass
diff --git a/invokeai/app/services/invocation_stats/invocation_stats_common.py b/invokeai/app/services/invocation_stats/invocation_stats_common.py
new file mode 100644
index 00000000000..4fec6d7bcf0
--- /dev/null
+++ b/invokeai/app/services/invocation_stats/invocation_stats_common.py
@@ -0,0 +1,183 @@
+from collections import defaultdict
+from dataclasses import asdict, dataclass
+from typing import Any, Optional
+
+
+class GESStatsNotFoundError(Exception):
+ """Raised when execution stats are not found for a given Graph Execution State."""
+
+
+@dataclass
+class NodeExecutionStatsSummary:
+ """The stats for a specific type of node."""
+
+ node_type: str
+ num_calls: int
+ time_used_seconds: float
+ delta_vram_gb: float
+
+
+@dataclass
+class ModelCacheStatsSummary:
+ """The stats for the model cache."""
+
+ high_water_mark_gb: float
+ cache_size_gb: float
+ total_usage_gb: float
+ cache_hits: int
+ cache_misses: int
+ models_cached: int
+ models_cleared: int
+
+
+@dataclass
+class GraphExecutionStatsSummary:
+ """The stats for the graph execution state."""
+
+ graph_execution_state_id: str
+ execution_time_seconds: float
+ # `wall_time_seconds`, `ram_usage_gb` and `ram_change_gb` are derived from the node execution stats.
+ # In some situations, there are no node stats, so these values are optional.
+ wall_time_seconds: Optional[float]
+ ram_usage_gb: Optional[float]
+ ram_change_gb: Optional[float]
+
+
+@dataclass
+class InvocationStatsSummary:
+ """
+ The accumulated stats for a graph execution.
+ Its `__str__` method returns a human-readable stats summary.
+ """
+
+ vram_usage_gb: Optional[float]
+ graph_stats: GraphExecutionStatsSummary
+ model_cache_stats: ModelCacheStatsSummary
+ node_stats: list[NodeExecutionStatsSummary]
+
+ def __str__(self) -> str:
+ _str = ""
+ _str = f"Graph stats: {self.graph_stats.graph_execution_state_id}\n"
+ _str += f"{'Node':>30} {'Calls':>7} {'Seconds':>9} {'VRAM Change':+>10}\n"
+
+ for summary in self.node_stats:
+ _str += f"{summary.node_type:>30} {summary.num_calls:>7} {summary.time_used_seconds:>8.3f}s {summary.delta_vram_gb:+10.3f}G\n"
+
+ _str += f"TOTAL GRAPH EXECUTION TIME: {self.graph_stats.execution_time_seconds:7.3f}s\n"
+
+ if self.graph_stats.wall_time_seconds is not None:
+ _str += f"TOTAL GRAPH WALL TIME: {self.graph_stats.wall_time_seconds:7.3f}s\n"
+
+ if self.graph_stats.ram_usage_gb is not None and self.graph_stats.ram_change_gb is not None:
+ _str += f"RAM used by InvokeAI process: {self.graph_stats.ram_usage_gb:4.2f}G ({self.graph_stats.ram_change_gb:+5.3f}G)\n"
+
+ _str += f"RAM used to load models: {self.model_cache_stats.total_usage_gb:4.2f}G\n"
+ if self.vram_usage_gb:
+ _str += f"VRAM in use: {self.vram_usage_gb:4.3f}G\n"
+ _str += "RAM cache statistics:\n"
+ _str += f" Model cache hits: {self.model_cache_stats.cache_hits}\n"
+ _str += f" Model cache misses: {self.model_cache_stats.cache_misses}\n"
+ _str += f" Models cached: {self.model_cache_stats.models_cached}\n"
+ _str += f" Models cleared from cache: {self.model_cache_stats.models_cleared}\n"
+ _str += f" Cache high water mark: {self.model_cache_stats.high_water_mark_gb:4.2f}/{self.model_cache_stats.cache_size_gb:4.2f}G\n"
+
+ return _str
+
+ def as_dict(self) -> dict[str, Any]:
+ """Returns the stats as a dictionary."""
+ return asdict(self)
+
+
+@dataclass
+class NodeExecutionStats:
+ """Class for tracking execution stats of an invocation node."""
+
+ invocation_type: str
+
+ start_time: float # Seconds since the epoch.
+ end_time: float # Seconds since the epoch.
+
+ start_ram_gb: float # GB
+ end_ram_gb: float # GB
+
+ delta_vram_gb: float # GB
+
+ def total_time(self) -> float:
+ return self.end_time - self.start_time
+
+
+class GraphExecutionStats:
+ """Class for tracking execution stats of a graph."""
+
+ def __init__(self):
+ self._node_stats_list: list[NodeExecutionStats] = []
+
+ def add_node_execution_stats(self, node_stats: NodeExecutionStats):
+ self._node_stats_list.append(node_stats)
+
+ def get_total_run_time(self) -> float:
+ """Get the total time spent executing nodes in the graph."""
+ total = 0.0
+ for node_stats in self._node_stats_list:
+ total += node_stats.total_time()
+ return total
+
+ def get_first_node_stats(self) -> NodeExecutionStats | None:
+ """Get the stats of the first node in the graph (by start_time)."""
+ first_node = None
+ for node_stats in self._node_stats_list:
+ if first_node is None or node_stats.start_time < first_node.start_time:
+ first_node = node_stats
+
+ assert first_node is not None
+ return first_node
+
+ def get_last_node_stats(self) -> NodeExecutionStats | None:
+ """Get the stats of the last node in the graph (by end_time)."""
+ last_node = None
+ for node_stats in self._node_stats_list:
+ if last_node is None or node_stats.end_time > last_node.end_time:
+ last_node = node_stats
+
+ return last_node
+
+ def get_graph_stats_summary(self, graph_execution_state_id: str) -> GraphExecutionStatsSummary:
+ """Get a summary of the graph stats."""
+ first_node = self.get_first_node_stats()
+ last_node = self.get_last_node_stats()
+
+ wall_time_seconds: Optional[float] = None
+ ram_usage_gb: Optional[float] = None
+ ram_change_gb: Optional[float] = None
+
+ if last_node and first_node:
+ wall_time_seconds = last_node.end_time - first_node.start_time
+ ram_usage_gb = last_node.end_ram_gb
+ ram_change_gb = last_node.end_ram_gb - first_node.start_ram_gb
+
+ return GraphExecutionStatsSummary(
+ graph_execution_state_id=graph_execution_state_id,
+ execution_time_seconds=self.get_total_run_time(),
+ wall_time_seconds=wall_time_seconds,
+ ram_usage_gb=ram_usage_gb,
+ ram_change_gb=ram_change_gb,
+ )
+
+ def get_node_stats_summaries(self) -> list[NodeExecutionStatsSummary]:
+ """Get a summary of the node stats."""
+ summaries: list[NodeExecutionStatsSummary] = []
+ node_stats_by_type: dict[str, list[NodeExecutionStats]] = defaultdict(list)
+
+ for node_stats in self._node_stats_list:
+ node_stats_by_type[node_stats.invocation_type].append(node_stats)
+
+ for node_type, node_type_stats_list in node_stats_by_type.items():
+ num_calls = len(node_type_stats_list)
+ time_used = sum([n.total_time() for n in node_type_stats_list])
+ delta_vram = max([n.delta_vram_gb for n in node_type_stats_list])
+ summary = NodeExecutionStatsSummary(
+ node_type=node_type, num_calls=num_calls, time_used_seconds=time_used, delta_vram_gb=delta_vram
+ )
+ summaries.append(summary)
+
+ return summaries
diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py
new file mode 100644
index 00000000000..9245d372d2e
--- /dev/null
+++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py
@@ -0,0 +1,143 @@
+import json
+import time
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Generator
+
+import psutil
+import torch
+
+import invokeai.backend.util.logging as logger
+from invokeai.app.invocations.baseinvocation import BaseInvocation
+from invokeai.app.services.invocation_stats.invocation_stats_base import InvocationStatsServiceBase
+from invokeai.app.services.invocation_stats.invocation_stats_common import (
+ GESStatsNotFoundError,
+ GraphExecutionStats,
+ GraphExecutionStatsSummary,
+ InvocationStatsSummary,
+ ModelCacheStatsSummary,
+ NodeExecutionStats,
+ NodeExecutionStatsSummary,
+)
+from invokeai.app.services.invoker import Invoker
+from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
+
+# Size of 1GB in bytes.
+GB = 2**30
+
+
+class InvocationStatsService(InvocationStatsServiceBase):
+ """Accumulate performance information about a running graph. Collects time spent in each node,
+ as well as the maximum and current VRAM utilisation for CUDA systems"""
+
+ def __init__(self):
+ # Maps graph_execution_state_id to GraphExecutionStats.
+ self._stats: dict[str, GraphExecutionStats] = {}
+ # Maps graph_execution_state_id to model manager CacheStats.
+ self._cache_stats: dict[str, CacheStats] = {}
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ @contextmanager
+ def collect_stats(self, invocation: BaseInvocation, graph_execution_state_id: str) -> Generator[None, None, None]:
+ # This is to handle case of the model manager not being initialized, which happens
+ # during some tests.
+ services = self._invoker.services
+ if not self._stats.get(graph_execution_state_id):
+ # First time we're seeing this graph_execution_state_id.
+ self._stats[graph_execution_state_id] = GraphExecutionStats()
+ self._cache_stats[graph_execution_state_id] = CacheStats()
+
+ # Record state before the invocation.
+ start_time = time.time()
+ start_ram = psutil.Process().memory_info().rss
+
+ # Remember current VRAM usage
+ vram_in_use = torch.cuda.memory_allocated() if torch.cuda.is_available() else 0.0
+
+ assert services.model_manager.load is not None
+ services.model_manager.load.ram_cache.stats = self._cache_stats[graph_execution_state_id]
+
+ try:
+ # Let the invocation run.
+ yield None
+ finally:
+ # Record delta VRAM
+ delta_vram_gb = ((torch.cuda.memory_allocated() - vram_in_use) / GB) if torch.cuda.is_available() else 0.0
+
+ node_stats = NodeExecutionStats(
+ invocation_type=invocation.get_type(),
+ start_time=start_time,
+ end_time=time.time(),
+ start_ram_gb=start_ram / GB,
+ end_ram_gb=psutil.Process().memory_info().rss / GB,
+ delta_vram_gb=delta_vram_gb,
+ )
+ self._stats[graph_execution_state_id].add_node_execution_stats(node_stats)
+
+ def reset_stats(self, graph_execution_state_id: str) -> None:
+ self._stats.pop(graph_execution_state_id, None)
+ self._cache_stats.pop(graph_execution_state_id, None)
+
+ def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary:
+ graph_stats_summary = self._get_graph_summary(graph_execution_state_id)
+ node_stats_summaries = self._get_node_summaries(graph_execution_state_id)
+ model_cache_stats_summary = self._get_model_cache_summary(graph_execution_state_id)
+ # Note: We use memory_allocated() here (not memory_reserved()) because we want to show
+ # the current actively-used VRAM, not the total reserved memory including PyTorch's cache.
+ vram_usage_gb = torch.cuda.memory_allocated() / GB if torch.cuda.is_available() else None
+
+ return InvocationStatsSummary(
+ graph_stats=graph_stats_summary,
+ model_cache_stats=model_cache_stats_summary,
+ node_stats=node_stats_summaries,
+ vram_usage_gb=vram_usage_gb,
+ )
+
+ def log_stats(self, graph_execution_state_id: str) -> None:
+ stats = self.get_stats(graph_execution_state_id)
+ logger.info(str(stats))
+
+ def dump_stats(self, graph_execution_state_id: str, output_path: Path) -> None:
+ stats = self.get_stats(graph_execution_state_id)
+ with open(output_path, "w") as f:
+ f.write(json.dumps(stats.as_dict(), indent=2))
+
+ def _get_model_cache_summary(self, graph_execution_state_id: str) -> ModelCacheStatsSummary:
+ try:
+ cache_stats = self._cache_stats[graph_execution_state_id]
+ except KeyError as e:
+ raise GESStatsNotFoundError(
+ f"Attempted to get model cache statistics for unknown graph {graph_execution_state_id}: {e}."
+ ) from e
+
+ return ModelCacheStatsSummary(
+ cache_hits=cache_stats.hits,
+ cache_misses=cache_stats.misses,
+ high_water_mark_gb=cache_stats.high_watermark / GB,
+ cache_size_gb=cache_stats.cache_size / GB,
+ total_usage_gb=sum(list(cache_stats.loaded_model_sizes.values())) / GB,
+ models_cached=cache_stats.in_cache,
+ models_cleared=cache_stats.cleared,
+ )
+
+ def _get_graph_summary(self, graph_execution_state_id: str) -> GraphExecutionStatsSummary:
+ try:
+ graph_stats = self._stats[graph_execution_state_id]
+ except KeyError as e:
+ raise GESStatsNotFoundError(
+ f"Attempted to get graph statistics for unknown graph {graph_execution_state_id}: {e}."
+ ) from e
+
+ return graph_stats.get_graph_stats_summary(graph_execution_state_id)
+
+ def _get_node_summaries(self, graph_execution_state_id: str) -> list[NodeExecutionStatsSummary]:
+ try:
+ graph_stats = self._stats[graph_execution_state_id]
+ except KeyError as e:
+ raise GESStatsNotFoundError(
+ f"Attempted to get node statistics for unknown graph {graph_execution_state_id}: {e}."
+ ) from e
+
+ return graph_stats.get_node_stats_summaries()
diff --git a/invokeai/app/services/invoker.py b/invokeai/app/services/invoker.py
new file mode 100644
index 00000000000..64f83725a1d
--- /dev/null
+++ b/invokeai/app/services/invoker.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+
+from invokeai.app.services.invocation_services import InvocationServices
+
+
+class Invoker:
+ """The invoker, used to execute invocations"""
+
+ services: InvocationServices
+
+ def __init__(self, services: InvocationServices):
+ self.services = services
+ self._start()
+
+ def __start_service(self, service) -> None:
+ # Call start() method on any services that have it
+ start_op = getattr(service, "start", None)
+ if callable(start_op):
+ start_op(self)
+
+ def __stop_service(self, service) -> None:
+ # Call stop() method on any services that have it
+ stop_op = getattr(service, "stop", None)
+ if callable(stop_op):
+ stop_op(self)
+
+ def _start(self) -> None:
+ """Starts the invoker. This is called automatically when the invoker is created."""
+ for service in vars(self.services):
+ self.__start_service(getattr(self.services, service))
+
+ def stop(self) -> None:
+ """Stops the invoker. A new invoker will have to be created to execute further."""
+ # First stop all services
+ for service in vars(self.services):
+ self.__stop_service(getattr(self.services, service))
diff --git a/invokeai/app/services/item_storage/__init__.py b/invokeai/app/services/item_storage/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/item_storage/item_storage_base.py b/invokeai/app/services/item_storage/item_storage_base.py
new file mode 100644
index 00000000000..ef227ba241c
--- /dev/null
+++ b/invokeai/app/services/item_storage/item_storage_base.py
@@ -0,0 +1,59 @@
+from abc import ABC, abstractmethod
+from typing import Callable, Generic, TypeVar
+
+from pydantic import BaseModel
+
+T = TypeVar("T", bound=BaseModel)
+
+
+class ItemStorageABC(ABC, Generic[T]):
+ """Provides storage for a single type of item. The type must be a Pydantic model."""
+
+ _on_changed_callbacks: list[Callable[[T], None]]
+ _on_deleted_callbacks: list[Callable[[str], None]]
+
+ def __init__(self) -> None:
+ self._on_changed_callbacks = []
+ self._on_deleted_callbacks = []
+
+ """Base item storage class"""
+
+ @abstractmethod
+ def get(self, item_id: str) -> T:
+ """
+ Gets the item.
+ :param item_id: the id of the item to get
+ :raises ItemNotFoundError: if the item is not found
+ """
+ pass
+
+ @abstractmethod
+ def set(self, item: T) -> None:
+ """
+ Sets the item.
+ :param item: the item to set
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, item_id: str) -> None:
+ """
+ Deletes the item, if it exists.
+ """
+ pass
+
+ def on_changed(self, on_changed: Callable[[T], None]) -> None:
+ """Register a callback for when an item is changed"""
+ self._on_changed_callbacks.append(on_changed)
+
+ def on_deleted(self, on_deleted: Callable[[str], None]) -> None:
+ """Register a callback for when an item is deleted"""
+ self._on_deleted_callbacks.append(on_deleted)
+
+ def _on_changed(self, item: T) -> None:
+ for callback in self._on_changed_callbacks:
+ callback(item)
+
+ def _on_deleted(self, item_id: str) -> None:
+ for callback in self._on_deleted_callbacks:
+ callback(item_id)
diff --git a/invokeai/app/services/item_storage/item_storage_common.py b/invokeai/app/services/item_storage/item_storage_common.py
new file mode 100644
index 00000000000..8fd677c71b7
--- /dev/null
+++ b/invokeai/app/services/item_storage/item_storage_common.py
@@ -0,0 +1,5 @@
+class ItemNotFoundError(KeyError):
+ """Raised when an item is not found in storage"""
+
+ def __init__(self, item_id: str) -> None:
+ super().__init__(f"Item with id {item_id} not found")
diff --git a/invokeai/app/services/item_storage/item_storage_memory.py b/invokeai/app/services/item_storage/item_storage_memory.py
new file mode 100644
index 00000000000..d8dd0e06645
--- /dev/null
+++ b/invokeai/app/services/item_storage/item_storage_memory.py
@@ -0,0 +1,52 @@
+from collections import OrderedDict
+from contextlib import suppress
+from typing import Generic, TypeVar
+
+from pydantic import BaseModel
+
+from invokeai.app.services.item_storage.item_storage_base import ItemStorageABC
+from invokeai.app.services.item_storage.item_storage_common import ItemNotFoundError
+
+T = TypeVar("T", bound=BaseModel)
+
+
+class ItemStorageMemory(ItemStorageABC[T], Generic[T]):
+ """
+ Provides a simple in-memory storage for items, with a maximum number of items to store.
+ The storage uses the LRU strategy to evict items from storage when the max has been reached.
+ """
+
+ def __init__(self, id_field: str = "id", max_items: int = 10) -> None:
+ super().__init__()
+ if max_items < 1:
+ raise ValueError("max_items must be at least 1")
+ if not id_field:
+ raise ValueError("id_field must not be empty")
+ self._id_field = id_field
+ self._items: OrderedDict[str, T] = OrderedDict()
+ self._max_items = max_items
+
+ def get(self, item_id: str) -> T:
+ # If the item exists, move it to the end of the OrderedDict.
+ item = self._items.pop(item_id, None)
+ if item is None:
+ raise ItemNotFoundError(item_id)
+ self._items[item_id] = item
+ return item
+
+ def set(self, item: T) -> None:
+ item_id = getattr(item, self._id_field)
+ if item_id in self._items:
+ # If item already exists, remove it and add it to the end
+ self._items.pop(item_id)
+ elif len(self._items) >= self._max_items:
+ # If cache is full, evict the least recently used item
+ self._items.popitem(last=False)
+ self._items[item_id] = item
+ self._on_changed(item)
+
+ def delete(self, item_id: str) -> None:
+ # This is a no-op if the item doesn't exist.
+ with suppress(KeyError):
+ del self._items[item_id]
+ self._on_deleted(item_id)
diff --git a/invokeai/app/services/model_images/model_images_base.py b/invokeai/app/services/model_images/model_images_base.py
new file mode 100644
index 00000000000..e66137c4c5c
--- /dev/null
+++ b/invokeai/app/services/model_images/model_images_base.py
@@ -0,0 +1,33 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+
+from PIL.Image import Image as PILImageType
+
+
+class ModelImageFileStorageBase(ABC):
+ """Low-level service responsible for storing and retrieving image files."""
+
+ @abstractmethod
+ def get(self, model_key: str) -> PILImageType:
+ """Retrieves a model image as PIL Image."""
+ pass
+
+ @abstractmethod
+ def get_path(self, model_key: str) -> Path:
+ """Gets the internal path to a model image."""
+ pass
+
+ @abstractmethod
+ def get_url(self, model_key: str) -> str | None:
+ """Gets the URL to fetch a model image."""
+ pass
+
+ @abstractmethod
+ def save(self, image: PILImageType, model_key: str) -> None:
+ """Saves a model image."""
+ pass
+
+ @abstractmethod
+ def delete(self, model_key: str) -> None:
+ """Deletes a model image."""
+ pass
diff --git a/invokeai/app/services/model_images/model_images_common.py b/invokeai/app/services/model_images/model_images_common.py
new file mode 100644
index 00000000000..4853a06f6a0
--- /dev/null
+++ b/invokeai/app/services/model_images/model_images_common.py
@@ -0,0 +1,20 @@
+# TODO: Should these exceptions subclass existing python exceptions?
+class ModelImageFileNotFoundException(Exception):
+ """Raised when an image file is not found in storage."""
+
+ def __init__(self, message="Model image file not found"):
+ super().__init__(message)
+
+
+class ModelImageFileSaveException(Exception):
+ """Raised when an image cannot be saved."""
+
+ def __init__(self, message="Model image file not saved"):
+ super().__init__(message)
+
+
+class ModelImageFileDeleteException(Exception):
+ """Raised when an image cannot be deleted."""
+
+ def __init__(self, message="Model image file not deleted"):
+ super().__init__(message)
diff --git a/invokeai/app/services/model_images/model_images_default.py b/invokeai/app/services/model_images/model_images_default.py
new file mode 100644
index 00000000000..5fe8086c6a5
--- /dev/null
+++ b/invokeai/app/services/model_images/model_images_default.py
@@ -0,0 +1,83 @@
+from pathlib import Path
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_images.model_images_base import ModelImageFileStorageBase
+from invokeai.app.services.model_images.model_images_common import (
+ ModelImageFileDeleteException,
+ ModelImageFileNotFoundException,
+ ModelImageFileSaveException,
+)
+from invokeai.app.util.misc import uuid_string
+from invokeai.app.util.thumbnails import make_thumbnail
+
+
+class ModelImageFileStorageDisk(ModelImageFileStorageBase):
+ """Stores images on disk"""
+
+ def __init__(self, model_images_folder: Path):
+ self._model_images_folder = model_images_folder
+ self._validate_storage_folders()
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def get(self, model_key: str) -> PILImageType:
+ try:
+ path = self.get_path(model_key)
+
+ if not self._validate_path(path):
+ raise ModelImageFileNotFoundException
+
+ return Image.open(path)
+ except FileNotFoundError as e:
+ raise ModelImageFileNotFoundException from e
+
+ def save(self, image: PILImageType, model_key: str) -> None:
+ try:
+ self._validate_storage_folders()
+ image_path = self._model_images_folder / (model_key + ".webp")
+ thumbnail = make_thumbnail(image, 256)
+ thumbnail.save(image_path, format="webp")
+
+ except Exception as e:
+ raise ModelImageFileSaveException from e
+
+ def get_path(self, model_key: str) -> Path:
+ path = self._model_images_folder / (model_key + ".webp")
+
+ return path
+
+ def get_url(self, model_key: str) -> str | None:
+ path = self.get_path(model_key)
+ if not self._validate_path(path):
+ return
+
+ url = self._invoker.services.urls.get_model_image_url(model_key)
+
+ # The image URL never changes, so we must add random query string to it to prevent caching
+ url += f"?{uuid_string()}"
+
+ return url
+
+ def delete(self, model_key: str) -> None:
+ try:
+ path = self.get_path(model_key)
+
+ if not self._validate_path(path):
+ raise ModelImageFileNotFoundException
+
+ path.unlink()
+
+ except Exception as e:
+ raise ModelImageFileDeleteException from e
+
+ def _validate_path(self, path: Path) -> bool:
+ """Validates the path given for an image."""
+ return path.exists()
+
+ def _validate_storage_folders(self) -> None:
+ """Checks if the required folders exist and create them if they don't"""
+ self._model_images_folder.mkdir(parents=True, exist_ok=True)
diff --git a/invokeai/app/services/model_install/__init__.py b/invokeai/app/services/model_install/__init__.py
new file mode 100644
index 00000000000..d96e86cbfed
--- /dev/null
+++ b/invokeai/app/services/model_install/__init__.py
@@ -0,0 +1,25 @@
+"""Initialization file for model install service package."""
+
+from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
+from invokeai.app.services.model_install.model_install_common import (
+ HFModelSource,
+ InstallStatus,
+ LocalModelSource,
+ ModelInstallJob,
+ ModelSource,
+ UnknownInstallJobException,
+ URLModelSource,
+)
+from invokeai.app.services.model_install.model_install_default import ModelInstallService
+
+__all__ = [
+ "ModelInstallServiceBase",
+ "ModelInstallService",
+ "InstallStatus",
+ "ModelInstallJob",
+ "UnknownInstallJobException",
+ "ModelSource",
+ "LocalModelSource",
+ "HFModelSource",
+ "URLModelSource",
+]
diff --git a/invokeai/app/services/model_install/model_install_base.py b/invokeai/app/services/model_install/model_install_base.py
new file mode 100644
index 00000000000..96e1c351415
--- /dev/null
+++ b/invokeai/app/services/model_install/model_install_base.py
@@ -0,0 +1,265 @@
+# Copyright 2023 Lincoln D. Stein and the InvokeAI development team
+"""Baseclass definitions for the model installer."""
+
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import TYPE_CHECKING, List, Optional, Union
+
+from pydantic.networks import AnyHttpUrl
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.download import DownloadQueueServiceBase
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_install.model_install_common import ModelInstallJob, ModelSource
+from invokeai.app.services.model_records import ModelRecordChanges, ModelRecordServiceBase
+
+if TYPE_CHECKING:
+ from invokeai.app.services.events.events_base import EventServiceBase
+
+
+class ModelInstallServiceBase(ABC):
+ """Abstract base class for InvokeAI model installation."""
+
+ @abstractmethod
+ def __init__(
+ self,
+ app_config: InvokeAIAppConfig,
+ record_store: ModelRecordServiceBase,
+ download_queue: DownloadQueueServiceBase,
+ event_bus: Optional["EventServiceBase"] = None,
+ ):
+ """
+ Create ModelInstallService object.
+
+ :param config: Systemwide InvokeAIAppConfig.
+ :param store: Systemwide ModelConfigStore
+ :param event_bus: InvokeAI event bus for reporting events to.
+ """
+
+ # make the invoker optional here because we don't need it and it
+ # makes the installer harder to use outside the web app
+ @abstractmethod
+ def start(self, invoker: Optional[Invoker] = None) -> None:
+ """Start the installer service."""
+
+ @abstractmethod
+ def stop(self, invoker: Optional[Invoker] = None) -> None:
+ """Stop the model install service. After this the objection can be safely deleted."""
+
+ @property
+ @abstractmethod
+ def app_config(self) -> InvokeAIAppConfig:
+ """Return the appConfig object associated with the installer."""
+
+ @property
+ @abstractmethod
+ def record_store(self) -> ModelRecordServiceBase:
+ """Return the ModelRecoreService object associated with the installer."""
+
+ @property
+ @abstractmethod
+ def event_bus(self) -> Optional["EventServiceBase"]:
+ """Return the event service base object associated with the installer."""
+
+ @abstractmethod
+ def register_path(
+ self,
+ model_path: Union[Path, str],
+ config: Optional[ModelRecordChanges] = None,
+ ) -> str:
+ """
+ Probe and register the model at model_path.
+
+ This keeps the model in its current location.
+
+ :param model_path: Filesystem Path to the model.
+ :param config: ModelRecordChanges object that will override autoassigned model record values.
+ :returns id: The string ID of the registered model.
+ """
+
+ @abstractmethod
+ def unregister(self, key: str) -> None:
+ """Remove model with indicated key from the database."""
+
+ @abstractmethod
+ def delete(self, key: str) -> None:
+ """Remove model with indicated key from the database. Delete its files only if they are within our models directory."""
+
+ @abstractmethod
+ def unconditionally_delete(self, key: str) -> None:
+ """Remove model with indicated key from the database and unconditionally delete weight files from disk."""
+
+ @abstractmethod
+ def install_path(
+ self,
+ model_path: Union[Path, str],
+ config: Optional[ModelRecordChanges] = None,
+ ) -> str:
+ """
+ Probe, register and install the model in the models directory.
+
+ This moves the model from its current location into
+ the models directory handled by InvokeAI.
+
+ :param model_path: Filesystem Path to the model.
+ :param config: ModelRecordChanges object that will override autoassigned model record values.
+ :returns id: The string ID of the registered model.
+ """
+
+ @abstractmethod
+ def heuristic_import(
+ self,
+ source: str,
+ config: Optional[ModelRecordChanges] = None,
+ access_token: Optional[str] = None,
+ inplace: Optional[bool] = False,
+ ) -> ModelInstallJob:
+ r"""Install the indicated model using heuristics to interpret user intentions.
+
+ :param source: String source
+ :param config: Optional ModelRecordChanges object. Any fields in this object
+ will override corresponding autoassigned probe fields in the
+ model's config record as described in `import_model()`.
+ :param access_token: Optional access token for remote sources.
+
+ The source can be:
+ 1. A local file path in posix() format (`/foo/bar` or `C:\foo\bar`)
+ 2. An http or https URL (`https://foo.bar/foo`)
+ 3. A HuggingFace repo_id (`foo/bar`, `foo/bar:fp16`, `foo/bar:fp16:vae`)
+
+ We extend the HuggingFace repo_id syntax to include the variant and the
+ subfolder or path. The following are acceptable alternatives:
+ stabilityai/stable-diffusion-v4
+ stabilityai/stable-diffusion-v4:fp16
+ stabilityai/stable-diffusion-v4:fp16:vae
+ stabilityai/stable-diffusion-v4::/checkpoints/sd4.safetensors
+ stabilityai/stable-diffusion-v4:onnx:vae
+
+ Because a local file path can look like a huggingface repo_id, the logic
+ first checks whether the path exists on disk, and if not, it is treated as
+ a parseable huggingface repo.
+
+ The previous support for recursing into a local folder and loading all model-like files
+ has been removed.
+ """
+ pass
+
+ @abstractmethod
+ def import_model(
+ self,
+ source: ModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ """Install the indicated model.
+
+ :param source: ModelSource object
+
+ :param config: Optional dict. Any fields in this dict
+ will override corresponding autoassigned probe fields in the
+ model's config record. Use it to override
+ `name`, `description`, `base_type`, `model_type`, `format`,
+ `prediction_type`, and/or `image_size`.
+
+ This will download the model located at `source`,
+ probe it, and install it into the models directory.
+ This call is executed asynchronously in a separate
+ thread and will issue the following events on the event bus:
+
+ - model_install_started
+ - model_install_error
+ - model_install_completed
+
+ The `inplace` flag does not affect the behavior of downloaded
+ models, which are always moved into the `models` directory.
+
+ The call returns a ModelInstallJob object which can be
+ polled to learn the current status and/or error message.
+
+ Variants recognized by HuggingFace currently are:
+ 1. onnx
+ 2. openvino
+ 3. fp16
+ 4. None (usually returns fp32 model)
+
+ """
+
+ @abstractmethod
+ def get_job_by_source(self, source: ModelSource) -> List[ModelInstallJob]:
+ """Return the ModelInstallJob(s) corresponding to the provided source."""
+
+ @abstractmethod
+ def get_job_by_id(self, id: int) -> ModelInstallJob:
+ """Return the ModelInstallJob corresponding to the provided id. Raises ValueError if no job has that ID."""
+
+ @abstractmethod
+ def list_jobs(self) -> List[ModelInstallJob]: # noqa D102
+ """
+ List active and complete install jobs.
+ """
+
+ @abstractmethod
+ def prune_jobs(self) -> None:
+ """Prune all completed and errored jobs."""
+
+ @abstractmethod
+ def cancel_job(self, job: ModelInstallJob) -> None:
+ """Cancel the indicated job."""
+
+ @abstractmethod
+ def pause_job(self, job: ModelInstallJob) -> None:
+ """Pause the indicated job, preserving partial downloads."""
+
+ @abstractmethod
+ def resume_job(self, job: ModelInstallJob) -> None:
+ """Resume a previously paused job."""
+
+ @abstractmethod
+ def restart_failed(self, job: ModelInstallJob) -> None:
+ """Restart failed or non-resumable downloads for a job."""
+
+ @abstractmethod
+ def restart_file(self, job: ModelInstallJob, file_source: str) -> None:
+ """Restart a specific file download for a job."""
+
+ @abstractmethod
+ def wait_for_job(self, job: ModelInstallJob, timeout: int = 0) -> ModelInstallJob:
+ """Wait for the indicated job to reach a terminal state.
+
+ This will block until the indicated install job has completed,
+ been cancelled, or errored out.
+
+ :param job: The job to wait on.
+ :param timeout: Wait up to indicated number of seconds. Raise a TimeoutError if
+ the job hasn't completed within the indicated time.
+ """
+
+ @abstractmethod
+ def wait_for_installs(self, timeout: int = 0) -> List[ModelInstallJob]:
+ """
+ Wait for all pending installs to complete.
+
+ This will block until all pending installs have
+ completed, been cancelled, or errored out.
+
+ :param timeout: Wait up to indicated number of seconds. Raise an Exception('timeout') if
+ installs do not complete within the indicated time. A timeout of zero (the default)
+ will block indefinitely until the installs complete.
+ """
+
+ @abstractmethod
+ def download_and_cache_model(self, source: str | AnyHttpUrl) -> Path:
+ """
+ Download the model file located at source to the models cache and return its Path.
+
+ :param source: A string representing a URL or repo_id.
+
+ The model file will be downloaded into the system-wide model cache
+ (`models/.cache`) if it isn't already there. Note that the model cache
+ is periodically cleared of infrequently-used entries when the model
+ converter runs.
+
+ Note that this doesn't automatically install or register the model, but is
+ intended for use by nodes that need access to models that aren't directly
+ supported by InvokeAI. The downloading process takes advantage of the download queue
+ to avoid interrupting other operations.
+ """
diff --git a/invokeai/app/services/model_install/model_install_common.py b/invokeai/app/services/model_install/model_install_common.py
new file mode 100644
index 00000000000..f223c4698c2
--- /dev/null
+++ b/invokeai/app/services/model_install/model_install_common.py
@@ -0,0 +1,270 @@
+import re
+import traceback
+from enum import Enum
+from pathlib import Path
+from typing import Literal, Optional, Set, Union
+
+from pydantic import BaseModel, Field, PrivateAttr, field_validator
+from pydantic.networks import AnyHttpUrl
+from typing_extensions import Annotated
+
+from invokeai.app.services.download import DownloadJob, MultiFileDownloadJob
+from invokeai.app.services.model_records import ModelRecordChanges
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata
+from invokeai.backend.model_manager.taxonomy import ModelRepoVariant, ModelSourceType
+
+
+class InvalidModelConfigException(Exception):
+ """Raised when a model configuration is invalid."""
+
+ pass
+
+
+class InstallStatus(str, Enum):
+ """State of an install job running in the background."""
+
+ WAITING = "waiting" # waiting to be dequeued
+ DOWNLOADING = "downloading" # downloading of model files in process
+ DOWNLOADS_DONE = "downloads_done" # downloading done, waiting to run
+ RUNNING = "running" # being processed
+ PAUSED = "paused" # paused, can be resumed
+ COMPLETED = "completed" # finished running
+ ERROR = "error" # terminated with an error message
+ CANCELLED = "cancelled" # terminated with an error message
+
+
+class UnknownInstallJobException(Exception):
+ """Raised when the status of an unknown job is requested."""
+
+
+class StringLikeSource(BaseModel):
+ """
+ Base class for model sources, implements functions that lets the source be sorted and indexed.
+
+ These shenanigans let this stuff work:
+
+ source1 = LocalModelSource(path='C:/users/mort/foo.safetensors')
+ mydict = {source1: 'model 1'}
+ assert mydict['C:/users/mort/foo.safetensors'] == 'model 1'
+ assert mydict[LocalModelSource(path='C:/users/mort/foo.safetensors')] == 'model 1'
+
+ source2 = LocalModelSource(path=Path('C:/users/mort/foo.safetensors'))
+ assert source1 == source2
+ assert source1 == 'C:/users/mort/foo.safetensors'
+ """
+
+ def __hash__(self) -> int:
+ """Return hash of the path field, for indexing."""
+ return hash(str(self))
+
+ def __lt__(self, other: object) -> int:
+ """Return comparison of the stringified version, for sorting."""
+ return str(self) < str(other)
+
+ def __eq__(self, other: object) -> bool:
+ """Return equality on the stringified version."""
+ if isinstance(other, Path):
+ return str(self) == other.as_posix()
+ else:
+ return str(self) == str(other)
+
+
+class LocalModelSource(StringLikeSource):
+ """A local file or directory path."""
+
+ path: str | Path
+ inplace: Optional[bool] = False
+ type: Literal["local"] = "local"
+
+ # these methods allow the source to be used in a string-like way,
+ # for example as an index into a dict
+ def __str__(self) -> str:
+ """Return string version of path when string rep needed."""
+ return Path(self.path).as_posix()
+
+
+class HFModelSource(StringLikeSource):
+ """
+ A HuggingFace repo_id with optional variant, sub-folder(s) and access token.
+ Note that the variant option, if not provided to the constructor, will default to fp16, which is
+ what people (almost) always want.
+
+ The subfolder can be a single path or multiple paths joined by '+' (e.g., "text_encoder+tokenizer").
+ When multiple subfolders are specified, all of them will be downloaded and combined into the model directory.
+ """
+
+ repo_id: str
+ variant: Optional[ModelRepoVariant] = ModelRepoVariant.FP16
+ subfolder: Optional[Path] = None
+ access_token: Optional[str] = None
+ type: Literal["hf"] = "hf"
+
+ @field_validator("repo_id")
+ @classmethod
+ def proper_repo_id(cls, v: str) -> str: # noqa D102
+ if not re.match(r"^([.\w-]+/[.\w-]+)$", v):
+ raise ValueError(f"{v}: invalid repo_id format")
+ return v
+
+ @property
+ def subfolders(self) -> list[Path]:
+ """Return list of subfolders (supports '+' separated multiple subfolders)."""
+ if self.subfolder is None:
+ return []
+ subfolder_str = self.subfolder.as_posix()
+ if "+" in subfolder_str:
+ return [Path(s.strip()) for s in subfolder_str.split("+")]
+ return [self.subfolder]
+
+ def __str__(self) -> str:
+ """Return string version of repoid when string rep needed."""
+ base: str = self.repo_id
+ if self.variant:
+ base += f":{self.variant or ''}"
+ if self.subfolder:
+ base += f"::{self.subfolder.as_posix()}"
+ return base
+
+
+class URLModelSource(StringLikeSource):
+ """A generic URL point to a checkpoint file."""
+
+ url: AnyHttpUrl
+ access_token: Optional[str] = None
+ type: Literal["url"] = "url"
+
+ def __str__(self) -> str:
+ """Return string version of the url when string rep needed."""
+ return str(self.url)
+
+
+class ExternalModelSource(StringLikeSource):
+ """An external provider model identifier."""
+
+ provider_id: str
+ provider_model_id: str
+ type: Literal["external"] = "external"
+
+ def __str__(self) -> str:
+ return f"external://{self.provider_id}/{self.provider_model_id}"
+
+
+ModelSource = Annotated[
+ Union[LocalModelSource, HFModelSource, URLModelSource, ExternalModelSource],
+ Field(discriminator="type"),
+]
+
+MODEL_SOURCE_TO_TYPE_MAP = {
+ URLModelSource: ModelSourceType.Url,
+ HFModelSource: ModelSourceType.HFRepoID,
+ LocalModelSource: ModelSourceType.Path,
+ ExternalModelSource: ModelSourceType.External,
+}
+
+
+class ModelInstallJob(BaseModel):
+ """Object that tracks the current status of an install request."""
+
+ id: int = Field(description="Unique ID for this job")
+ status: InstallStatus = Field(default=InstallStatus.WAITING, description="Current status of install process")
+ error_reason: Optional[str] = Field(default=None, description="Information about why the job failed")
+ config_in: ModelRecordChanges = Field(
+ default_factory=ModelRecordChanges,
+ description="Configuration information (e.g. 'description') to apply to model.",
+ )
+ config_out: Optional[AnyModelConfig] = Field(
+ default=None, description="After successful installation, this will hold the configuration object."
+ )
+ inplace: bool = Field(
+ default=False, description="Leave model in its current location; otherwise install under models directory"
+ )
+ source: ModelSource = Field(description="Source (URL, repo_id, or local path) of model")
+ local_path: Path = Field(description="Path to locally-downloaded model; may be the same as the source")
+ bytes: int = Field(
+ default=0, description="For a remote model, the number of bytes downloaded so far (may not be available)"
+ )
+ total_bytes: int = Field(default=0, description="Total size of the model to be installed")
+ source_metadata: Optional[AnyModelRepoMetadata] = Field(
+ default=None, description="Metadata provided by the model source"
+ )
+ download_parts: Set[DownloadJob] = Field(
+ default_factory=set, description="Download jobs contributing to this install"
+ )
+ error: Optional[str] = Field(
+ default=None, description="On an error condition, this field will contain the text of the exception"
+ )
+ error_traceback: Optional[str] = Field(
+ default=None, description="On an error condition, this field will contain the exception traceback"
+ )
+ # internal flags and transitory settings
+ _install_tmpdir: Optional[Path] = PrivateAttr(default=None)
+ _multifile_job: Optional[MultiFileDownloadJob] = PrivateAttr(default=None)
+ _exception: Optional[Exception] = PrivateAttr(default=None)
+ _resume_metadata: Optional[dict] = PrivateAttr(default=None)
+
+ def set_error(self, e: Exception) -> None:
+ """Record the error and traceback from an exception."""
+ self._exception = e
+ self.error = str(e)
+ self.error_traceback = self._format_error(e)
+ self.status = InstallStatus.ERROR
+ self.error_reason = self._exception.__class__.__name__ if self._exception else None
+
+ def cancel(self) -> None:
+ """Call to cancel the job."""
+ self.status = InstallStatus.CANCELLED
+
+ @property
+ def error_type(self) -> Optional[str]:
+ """Class name of the exception that led to status==ERROR."""
+ return self._exception.__class__.__name__ if self._exception else None
+
+ def _format_error(self, exception: Exception) -> str:
+ """Error traceback."""
+ return "".join(traceback.format_exception(exception))
+
+ @property
+ def cancelled(self) -> bool:
+ """Set status to CANCELLED."""
+ return self.status == InstallStatus.CANCELLED
+
+ @property
+ def errored(self) -> bool:
+ """Return true if job has errored."""
+ return self.status == InstallStatus.ERROR
+
+ @property
+ def waiting(self) -> bool:
+ """Return true if job is waiting to run."""
+ return self.status == InstallStatus.WAITING
+
+ @property
+ def downloading(self) -> bool:
+ """Return true if job is downloading."""
+ return self.status == InstallStatus.DOWNLOADING
+
+ @property
+ def downloads_done(self) -> bool:
+ """Return true if job's downloads ae done."""
+ return self.status == InstallStatus.DOWNLOADS_DONE
+
+ @property
+ def paused(self) -> bool:
+ """Return true if job is paused."""
+ return self.status == InstallStatus.PAUSED
+
+ @property
+ def running(self) -> bool:
+ """Return true if job is running."""
+ return self.status == InstallStatus.RUNNING
+
+ @property
+ def complete(self) -> bool:
+ """Return true if job completed without errors."""
+ return self.status == InstallStatus.COMPLETED
+
+ @property
+ def in_terminal_state(self) -> bool:
+ """Return true if job is in a terminal state."""
+ return self.status in [InstallStatus.COMPLETED, InstallStatus.ERROR, InstallStatus.CANCELLED]
diff --git a/invokeai/app/services/model_install/model_install_default.py b/invokeai/app/services/model_install/model_install_default.py
new file mode 100644
index 00000000000..3baf11029ff
--- /dev/null
+++ b/invokeai/app/services/model_install/model_install_default.py
@@ -0,0 +1,1515 @@
+"""Model installation class."""
+
+import gc
+import json
+import locale
+import os
+import re
+import sys
+import threading
+import time
+from copy import deepcopy
+from pathlib import Path
+from queue import Empty, Queue
+from shutil import move, rmtree
+from tempfile import mkdtemp
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union
+
+import torch
+import yaml
+from huggingface_hub import get_token as hf_get_token
+from pydantic.networks import AnyHttpUrl
+from pydantic_core import Url
+from requests import Session
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.download import DownloadQueueServiceBase, MultiFileDownloadJob
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
+from invokeai.app.services.model_install.model_install_common import (
+ MODEL_SOURCE_TO_TYPE_MAP,
+ ExternalModelSource,
+ HFModelSource,
+ InstallStatus,
+ InvalidModelConfigException,
+ LocalModelSource,
+ ModelInstallJob,
+ ModelSource,
+ StringLikeSource,
+ URLModelSource,
+)
+from invokeai.app.services.model_records import DuplicateModelException, ModelRecordServiceBase, UnknownModelException
+from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
+from invokeai.app.util.misc import get_iso_timestamp
+from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelConfig,
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
+from invokeai.backend.model_manager.configs.factory import (
+ AnyModelConfig,
+ ModelConfigFactory,
+)
+from invokeai.backend.model_manager.configs.unknown import Unknown_Config
+from invokeai.backend.model_manager.metadata import (
+ AnyModelRepoMetadata,
+ HuggingFaceMetadataFetch,
+ ModelMetadataFetchBase,
+ ModelMetadataWithFiles,
+ RemoteModelFile,
+)
+from invokeai.backend.model_manager.metadata.metadata_base import HuggingFaceMetadata
+from invokeai.backend.model_manager.search import ModelSearch
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ModelFormat,
+ ModelRepoVariant,
+ ModelSourceType,
+ ModelType,
+)
+from invokeai.backend.model_manager.util.lora_metadata_extractor import apply_lora_metadata
+from invokeai.backend.util import InvokeAILogger
+from invokeai.backend.util.catch_sigint import catch_sigint
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.util import slugify
+
+if TYPE_CHECKING:
+ from invokeai.app.services.events.events_base import EventServiceBase
+
+
+TMPDIR_PREFIX = "tmpinstall_"
+# Marker file used to resume or pause remote model installs across restarts.
+INSTALL_MARKER_FILENAME = ".invokeai_install.json"
+INSTALL_MARKER_VERSION = 1
+
+
+class ModelInstallService(ModelInstallServiceBase):
+ """class for InvokeAI model installation."""
+
+ def __init__(
+ self,
+ app_config: InvokeAIAppConfig,
+ record_store: ModelRecordServiceBase,
+ download_queue: DownloadQueueServiceBase,
+ event_bus: Optional["EventServiceBase"] = None,
+ session: Optional[Session] = None,
+ ):
+ """
+ Initialize the installer object.
+
+ :param app_config: InvokeAIAppConfig object
+ :param record_store: Previously-opened ModelRecordService database
+ :param event_bus: Optional EventService object
+ """
+ self._app_config = app_config
+ self._record_store = record_store
+ self._event_bus = event_bus
+ self._logger = InvokeAILogger.get_logger(name=self.__class__.__name__)
+ self._install_jobs: List[ModelInstallJob] = []
+ self._install_queue: Queue[ModelInstallJob] = Queue()
+ self._lock = threading.Lock()
+ self._stop_event = threading.Event()
+ self._downloads_changed_event = threading.Event()
+ self._install_completed_event = threading.Event()
+ self._restore_completed_event = threading.Event()
+ self._restore_completed_event.set()
+ self._download_queue = download_queue
+ self._download_cache: Dict[int, ModelInstallJob] = {}
+ self._running = False
+ self._session = session
+ self._install_thread: Optional[threading.Thread] = None
+ self._next_job_id = 0
+
+ def _marker_path(self, tmpdir: Path) -> Path:
+ return tmpdir / INSTALL_MARKER_FILENAME
+
+ def _write_install_marker(self, job: ModelInstallJob, status: Optional[InstallStatus] = None) -> None:
+ if job._install_tmpdir is None:
+ return
+ files: list[dict] = []
+ if job.download_parts:
+ for part in job.download_parts:
+ files.append(
+ {
+ "url": str(part.source),
+ "canonical_url": part.canonical_url,
+ "etag": part.etag,
+ "last_modified": part.last_modified,
+ "expected_total_bytes": part.expected_total_bytes,
+ "final_url": part.final_url,
+ "download_path": part.download_path.as_posix() if part.download_path else None,
+ "resume_required": part.resume_required,
+ "resume_message": part.resume_message,
+ }
+ )
+ marker = {
+ "version": INSTALL_MARKER_VERSION,
+ "source": str(job.source),
+ "access_token": (
+ job.source.access_token if isinstance(job.source, (HFModelSource, URLModelSource)) else None
+ ),
+ "config_in": job.config_in.model_dump(),
+ "status": (status or job.status).value,
+ "updated_at": get_iso_timestamp(),
+ "files": files,
+ }
+ path = self._marker_path(job._install_tmpdir)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with open(path, "wt", encoding="utf-8") as f:
+ json.dump(marker, f)
+
+ def _read_install_marker(self, tmpdir: Path) -> Optional[dict]:
+ path = self._marker_path(tmpdir)
+ if not path.exists():
+ return None
+ try:
+ with open(path, "rt", encoding="utf-8") as f:
+ marker = json.load(f)
+ if marker.get("version") != INSTALL_MARKER_VERSION:
+ return None
+ return marker
+ except Exception as e:
+ self._logger.warning(f"Invalid install marker in {tmpdir}: {e}")
+ return None
+
+ def _delete_install_marker(self, tmpdir: Path) -> None:
+ path = self._marker_path(tmpdir)
+ if path.exists():
+ try:
+ path.unlink()
+ except Exception as e:
+ self._logger.warning(f"Failed to remove install marker {path}: {e}")
+
+ def _find_reusable_tmpdir(self, source: ModelSource) -> Optional[Path]:
+ path = self._app_config.models_path
+ source_str = str(source)
+ candidates: list[tuple[str, Path]] = []
+ for tmpdir in path.glob(f"{TMPDIR_PREFIX}*"):
+ marker = self._read_install_marker(tmpdir)
+ if not marker:
+ continue
+ if marker.get("source") != source_str:
+ continue
+ status = marker.get("status")
+ if status in {InstallStatus.COMPLETED.value, InstallStatus.ERROR.value, InstallStatus.CANCELLED.value}:
+ continue
+ candidates.append((marker.get("updated_at", ""), tmpdir))
+ if not candidates:
+ return None
+ candidates.sort(key=lambda item: item[0], reverse=True)
+ return candidates[0][1]
+
+ def _restore_incomplete_installs(self) -> None:
+ path = self._app_config.models_path
+ seen_sources: set[str] = set()
+ # Collect sources already tracked by active jobs (including those being downloaded right now).
+ # We must not re-queue these or delete their tmpdirs.
+ with self._lock:
+ active_sources = {str(j.source) for j in self._install_jobs if not j.in_terminal_state}
+ active_sources.update(str(j.source) for j in self._download_cache.values() if not j.in_terminal_state)
+ for tmpdir in path.glob(f"{TMPDIR_PREFIX}*"):
+ marker = self._read_install_marker(tmpdir)
+ if not marker:
+ continue
+ status = marker.get("status")
+ if status in {InstallStatus.COMPLETED.value, InstallStatus.ERROR.value, InstallStatus.CANCELLED.value}:
+ continue
+
+ try:
+ source_str = marker.get("source")
+ if not isinstance(source_str, str):
+ raise ValueError("Missing source in install marker")
+ source = self._guess_source(source_str)
+ access_token = marker.get("access_token")
+ if isinstance(source, (HFModelSource, URLModelSource)) and isinstance(access_token, str):
+ source.access_token = access_token
+ if source_str in active_sources:
+ # This tmpdir belongs to an install already in progress; leave it alone.
+ self._logger.debug(f"Skipping restore for {source_str} - already being tracked")
+ continue
+ if source_str in seen_sources:
+ self._logger.info(f"Removing duplicate temporary directory {tmpdir}")
+ self._safe_rmtree(tmpdir, self._logger)
+ continue
+ seen_sources.add(source_str)
+ except Exception as e:
+ self._logger.warning(f"Skipping install marker in {tmpdir}: {e}")
+ continue
+
+ config_in = ModelRecordChanges(**(marker.get("config_in") or {}))
+ job = ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config_in,
+ local_path=tmpdir,
+ )
+ job._install_tmpdir = tmpdir
+ files_meta = marker.get("files") or []
+ if files_meta:
+ job._resume_metadata = {f.get("url"): f for f in files_meta if f.get("url")}
+ job.status = InstallStatus(status) if status else InstallStatus.WAITING
+ self._install_jobs.append(job)
+
+ if job.paused:
+ continue
+
+ if job.status in [InstallStatus.DOWNLOADS_DONE, InstallStatus.RUNNING]:
+ job.status = InstallStatus.DOWNLOADS_DONE
+ self._put_in_queue(job)
+ else:
+ try:
+ self._resume_remote_download(job)
+ except Exception as e:
+ self._set_error(job, e)
+ if job._install_tmpdir is not None:
+ self._safe_rmtree(job._install_tmpdir, self._logger)
+
+ def _restore_incomplete_installs_async(self) -> None:
+ self._restore_completed_event.clear()
+
+ def _run() -> None:
+ try:
+ self._logger.info("Restoring incomplete installs")
+ self._restore_incomplete_installs()
+ self._logger.info("Finished restoring incomplete installs")
+ except Exception as e:
+ self._logger.error(f"Failed to restore incomplete installs: {e}")
+ finally:
+ self._restore_completed_event.set()
+
+ threading.Thread(target=_run, daemon=True).start()
+
+ def _wait_for_restore_complete(self) -> None:
+ self._restore_completed_event.wait()
+
+ def _resume_remote_download(self, job: ModelInstallJob) -> None:
+ job.status = InstallStatus.WAITING
+ if job.download_parts:
+ for part in job.download_parts:
+ if part.complete or part.bytes <= 0:
+ continue
+ if not part.download_path:
+ continue
+ in_progress_path = part.download_path.with_name(part.download_path.name + ".downloading")
+ if not in_progress_path.exists():
+ part.bytes = 0
+ part.resume_from_scratch = True
+ part.resume_message = "Partial file missing. Restarted download from the beginning."
+ job.bytes = sum(p.bytes for p in job.download_parts)
+ remote_files, metadata = self._remote_files_from_source(job.source)
+ subfolders = job.source.subfolders if isinstance(job.source, HFModelSource) else []
+ self._enqueue_remote_download(
+ job=job,
+ source=job.source,
+ remote_files=remote_files,
+ metadata=metadata,
+ destdir=job._install_tmpdir or job.local_path,
+ subfolder=job.source.subfolder if isinstance(job.source, HFModelSource) and len(subfolders) <= 1 else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ resume_metadata=job._resume_metadata,
+ )
+
+ @property
+ def app_config(self) -> InvokeAIAppConfig: # noqa D102
+ return self._app_config
+
+ @property
+ def record_store(self) -> ModelRecordServiceBase: # noqa D102
+ return self._record_store
+
+ @property
+ def event_bus(self) -> Optional["EventServiceBase"]: # noqa D102
+ return self._event_bus
+
+ # make the invoker optional here because we don't need it and it
+ # makes the installer harder to use outside the web app
+ def start(self, invoker: Optional[Invoker] = None) -> None:
+ """Start the installer thread."""
+
+ with self._lock:
+ if self._running:
+ raise Exception("Attempt to start the installer service twice")
+ self._start_installer_thread()
+ self._remove_dangling_install_dirs()
+ self._migrate_yaml()
+ # In normal use, we do not want to scan the models directory - it should never have orphaned models.
+ # We should only do the scan when the flag is set (which should only be set when testing).
+ if self.app_config.scan_models_on_startup:
+ with catch_sigint():
+ self._register_orphaned_models()
+
+ # Check all models' paths and confirm they exist. A model could be missing if it was installed on a volume
+ # that isn't currently mounted. In this case, we don't want to delete the model from the database, but we do
+ # want to alert the user.
+ for model in self._scan_for_missing_models():
+ self._logger.warning(f"Missing model file: {model.name} at {model.path}")
+
+ self._write_invoke_managed_models_dir_readme()
+ self._restore_incomplete_installs_async()
+
+ def stop(self, invoker: Optional[Invoker] = None) -> None:
+ """Stop the installer thread; after this the object can be deleted and garbage collected."""
+ if not self._running:
+ return
+ self._logger.debug("calling stop_event.set()")
+ self._stop_event.set()
+ self._clear_pending_jobs()
+ self._download_cache.clear()
+ assert self._install_thread is not None
+ self._install_thread.join()
+ self._running = False
+
+ def _write_invoke_managed_models_dir_readme(self) -> None:
+ """Write a README file to the Invoke-managed models directory warning users to not fiddle with it."""
+ readme_path = self.app_config.models_path / "README.txt"
+ with open(readme_path, "wt", encoding=locale.getpreferredencoding()) as f:
+ f.write(
+ "This directory is managed by Invoke. Do not add, delete or move files in this directory.\n\nTo manage models, use the web interface.\n"
+ )
+
+ def _clear_pending_jobs(self) -> None:
+ for job in self.list_jobs():
+ if not job.in_terminal_state:
+ if job._multifile_job is not None:
+ self._logger.warning(f"Pausing job {job.id}")
+ self.pause_job(job)
+ else:
+ self._logger.warning(f"Cancelling job {job.id}")
+ self.cancel_job(job)
+ while True:
+ try:
+ job = self._install_queue.get(block=False)
+ self._install_queue.task_done()
+ except Empty:
+ break
+
+ def _put_in_queue(self, job: ModelInstallJob) -> None:
+ if self._stop_event.is_set():
+ self.cancel_job(job)
+ else:
+ self._install_queue.put(job)
+
+ def register_path(
+ self,
+ model_path: Union[Path, str],
+ config: Optional[ModelRecordChanges] = None,
+ ) -> str: # noqa D102
+ model_path = Path(model_path)
+ config = config or ModelRecordChanges()
+ if not config.source:
+ config.source = model_path.resolve().as_posix()
+ config.source_type = ModelSourceType.Path
+ return self._register(model_path, config)
+
+ # TODO: Replace this with a proper fix for underlying problem of Windows holding open
+ # the file when it needs to be moved.
+ @staticmethod
+ def _move_with_retries(src: Path, dst: Path, attempts: int = 5, delay: float = 0.5) -> None:
+ """Workaround for Windows file-handle issues when moving files."""
+ for tries_left in range(attempts, 0, -1):
+ try:
+ move(src, dst)
+ return
+ except PermissionError:
+ gc.collect()
+ if tries_left == 1:
+ raise
+ time.sleep(delay)
+ delay *= 2 # Exponential backoff
+
+ def install_path(
+ self,
+ model_path: Union[Path, str],
+ config: Optional[ModelRecordChanges] = None,
+ ) -> str:
+ model_path = Path(model_path)
+ config = config or ModelRecordChanges()
+ info: AnyModelConfig = self._probe(Path(model_path), config) # type: ignore
+
+ dest_dir = self.app_config.models_path / info.key
+ try:
+ if dest_dir.exists():
+ raise FileExistsError(
+ f"Cannot install model {model_path.name} to {dest_dir}: destination already exists"
+ )
+ dest_dir.mkdir(parents=True)
+ dest_path = dest_dir / model_path.name if model_path.is_file() else dest_dir
+ if model_path.is_file():
+ self._move_with_retries(model_path, dest_path) # Windows workaround TODO: fix root cause
+ elif model_path.is_dir():
+ # Move the contents of the directory, not the directory itself
+ for item in model_path.iterdir():
+ move(item, dest_dir / item.name)
+ except FileExistsError as e:
+ raise DuplicateModelException(
+ f"A model named {model_path.name} is already installed at {dest_dir.as_posix()}"
+ ) from e
+
+ return self._register(
+ dest_path,
+ config,
+ info,
+ )
+
+ def heuristic_import(
+ self,
+ source: str,
+ config: Optional[ModelRecordChanges] = None,
+ access_token: Optional[str] = None,
+ inplace: Optional[bool] = False,
+ ) -> ModelInstallJob:
+ """Install a model using pattern matching to infer the type of source."""
+ source_obj = self._guess_source(source)
+ if isinstance(source_obj, LocalModelSource):
+ source_obj.inplace = inplace
+ elif isinstance(source_obj, HFModelSource) or isinstance(source_obj, URLModelSource):
+ source_obj.access_token = access_token
+ return self.import_model(source_obj, config)
+
+ def import_model(self, source: ModelSource, config: Optional[ModelRecordChanges] = None) -> ModelInstallJob: # noqa D102
+ self._wait_for_restore_complete()
+
+ similar_jobs = [x for x in self.list_jobs() if x.source == source and not x.in_terminal_state]
+ if similar_jobs:
+ self._logger.warning(f"There is already an active install job for {source}. Not enqueuing.")
+ return similar_jobs[0]
+
+ if isinstance(source, LocalModelSource):
+ install_job = self._import_local_model(source, config)
+ self._put_in_queue(install_job) # synchronously install
+ elif isinstance(source, HFModelSource):
+ install_job = self._import_from_hf(source, config)
+ elif isinstance(source, URLModelSource):
+ install_job = self._import_from_url(source, config)
+ elif isinstance(source, ExternalModelSource):
+ install_job = self._import_external_model(source, config)
+ self._put_in_queue(install_job)
+ else:
+ raise ValueError(f"Unsupported model source: '{type(source)}'")
+
+ self._install_jobs.append(install_job)
+ return install_job
+
+ def list_jobs(self) -> List[ModelInstallJob]: # noqa D102
+ return self._install_jobs
+
+ def get_job_by_source(self, source: ModelSource) -> List[ModelInstallJob]: # noqa D102
+ return [x for x in self._install_jobs if x.source == source]
+
+ def get_job_by_id(self, id: int) -> ModelInstallJob: # noqa D102
+ jobs = [x for x in self._install_jobs if x.id == id]
+ if not jobs:
+ raise ValueError(f"No job with id {id} known")
+ assert len(jobs) == 1
+ assert isinstance(jobs[0], ModelInstallJob)
+ return jobs[0]
+
+ def wait_for_job(self, job: ModelInstallJob, timeout: int = 0) -> ModelInstallJob:
+ """Block until the indicated job has reached terminal state, or when timeout limit reached."""
+ start = time.time()
+ while not job.in_terminal_state:
+ if self._install_completed_event.wait(timeout=5): # in case we miss an event
+ self._install_completed_event.clear()
+ if timeout > 0 and time.time() - start > timeout:
+ raise TimeoutError("Timeout exceeded")
+ return job
+
+ def wait_for_installs(self, timeout: int = 0) -> List[ModelInstallJob]: # noqa D102
+ """Block until all installation jobs are done."""
+ self._wait_for_restore_complete()
+
+ start = time.time()
+ while len(self._download_cache) > 0:
+ if self._downloads_changed_event.wait(timeout=0.25): # in case we miss an event
+ self._downloads_changed_event.clear()
+ if timeout > 0 and time.time() - start > timeout:
+ raise TimeoutError("Timeout exceeded")
+ self._install_queue.join()
+
+ return self._install_jobs
+
+ def cancel_job(self, job: ModelInstallJob) -> None:
+ """Cancel the indicated job."""
+ job.cancel()
+ self._logger.warning(f"Cancelling {job.source}")
+ if dj := job._multifile_job:
+ self._download_queue.cancel_job(dj)
+ if job._install_tmpdir is not None:
+ # Mark cancelled before cleanup so we don't reuse the folder if deletion fails.
+ self._write_install_marker(job, status=InstallStatus.CANCELLED)
+ self._delete_install_marker(job._install_tmpdir)
+ self._safe_rmtree(job._install_tmpdir, self._logger)
+
+ def pause_job(self, job: ModelInstallJob) -> None:
+ """Pause the indicated job, preserving partial downloads."""
+ if job.in_terminal_state:
+ return
+ job.status = InstallStatus.PAUSED
+ self._logger.warning(f"Pausing {job.source}")
+ if dj := job._multifile_job:
+ for part in dj.download_parts:
+ self._download_queue.pause_job(part)
+ self._write_install_marker(job, status=InstallStatus.PAUSED)
+
+ def resume_job(self, job: ModelInstallJob) -> None:
+ """Resume a previously paused job."""
+ if not job.paused:
+ return
+ self._logger.info(f"Resuming {job.source}")
+ self._resume_remote_download(job)
+
+ def restart_failed(self, job: ModelInstallJob) -> None:
+ """Restart failed or non-resumable downloads for a job."""
+ if not isinstance(job.source, (HFModelSource, URLModelSource)):
+ return
+ if not job.download_parts:
+ return
+ if not any(part.resume_required or part.errored for part in job.download_parts):
+ return
+ sources_to_restart = {str(part.source) for part in job.download_parts if not part.complete}
+ if not sources_to_restart:
+ return
+ job.status = InstallStatus.WAITING
+ remote_files, metadata = self._remote_files_from_source(job.source)
+ remote_files = [rf for rf in remote_files if str(rf.url) in sources_to_restart]
+ subfolders = job.source.subfolders if isinstance(job.source, HFModelSource) else []
+ self._enqueue_remote_download(
+ job=job,
+ source=job.source,
+ remote_files=remote_files,
+ metadata=metadata,
+ destdir=job._install_tmpdir or job.local_path,
+ subfolder=job.source.subfolder if isinstance(job.source, HFModelSource) and len(subfolders) <= 1 else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ clear_partials=True,
+ )
+
+ def restart_file(self, job: ModelInstallJob, file_source: str) -> None:
+ """Restart a specific file download for a job."""
+ if not isinstance(job.source, (HFModelSource, URLModelSource)):
+ return
+ job.status = InstallStatus.WAITING
+ remote_files, metadata = self._remote_files_from_source(job.source)
+ remote_files = [rf for rf in remote_files if str(rf.url) == file_source]
+ if not remote_files:
+ return
+ subfolders = job.source.subfolders if isinstance(job.source, HFModelSource) else []
+ self._enqueue_remote_download(
+ job=job,
+ source=job.source,
+ remote_files=remote_files,
+ metadata=metadata,
+ destdir=job._install_tmpdir or job.local_path,
+ subfolder=job.source.subfolder if isinstance(job.source, HFModelSource) and len(subfolders) <= 1 else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ clear_partials=True,
+ )
+
+ def prune_jobs(self) -> None:
+ """Prune all completed and errored jobs."""
+ unfinished_jobs = [x for x in self._install_jobs if not x.in_terminal_state]
+ self._install_jobs = unfinished_jobs
+
+ def _migrate_yaml(self) -> None:
+ db_models = self.record_store.all_models()
+
+ legacy_models_yaml_path = (
+ self._app_config.legacy_models_yaml_path or self._app_config.root_path / "configs" / "models.yaml"
+ )
+
+ # The old path may be relative to the root path
+ if not legacy_models_yaml_path.exists():
+ legacy_models_yaml_path = Path(self._app_config.root_path, legacy_models_yaml_path)
+
+ if legacy_models_yaml_path.exists():
+ with open(legacy_models_yaml_path, "rt", encoding=locale.getpreferredencoding()) as file:
+ legacy_models_yaml = yaml.safe_load(file)
+
+ yaml_metadata = legacy_models_yaml.pop("__metadata__")
+ yaml_version = yaml_metadata.get("version")
+
+ if yaml_version != "3.0.0":
+ raise ValueError(
+ f"Attempted migration of unsupported `models.yaml` v{yaml_version}. Only v3.0.0 is supported. Exiting."
+ )
+
+ self._logger.info(
+ f"Starting one-time migration of {len(legacy_models_yaml.items())} models from {str(legacy_models_yaml_path)}. This may take a few minutes."
+ )
+
+ if len(db_models) == 0 and len(legacy_models_yaml.items()) != 0:
+ for model_key, stanza in legacy_models_yaml.items():
+ _, _, model_name = str(model_key).split("/")
+ model_path = Path(stanza["path"])
+ if not model_path.is_absolute():
+ model_path = self._app_config.models_path / model_path
+ model_path = model_path.resolve()
+
+ config = ModelRecordChanges(
+ name=model_name,
+ description=stanza.get("description"),
+ )
+ legacy_config_path = stanza.get("config")
+ if legacy_config_path:
+ # In v3, these paths were relative to the root. Migrate them to be relative to the legacy_conf_dir.
+ legacy_config_path = self._app_config.root_path / legacy_config_path
+ if legacy_config_path.is_relative_to(self._app_config.legacy_conf_path):
+ legacy_config_path = legacy_config_path.relative_to(self._app_config.legacy_conf_path)
+ config.config_path = str(legacy_config_path)
+ try:
+ id = self.register_path(model_path=model_path, config=config)
+ self._logger.info(f"Migrated {model_name} with id {id}")
+ except Exception as e:
+ self._logger.warning(f"Model at {model_path} could not be migrated: {e}")
+
+ # Rename `models.yaml` to `models.yaml.bak` to prevent re-migration
+ legacy_models_yaml_path.rename(legacy_models_yaml_path.with_suffix(".yaml.bak"))
+
+ # Unset the path - we are done with it either way
+ self._app_config.legacy_models_yaml_path = None
+
+ def unregister(self, key: str) -> None: # noqa D102
+ self.record_store.del_model(key)
+
+ def delete(self, key: str) -> None: # noqa D102
+ """Unregister the model. Delete its files only if they are within our models directory."""
+ model = self.record_store.get_model(key)
+ model_path = self.app_config.models_path / model.path
+
+ if model_path.is_relative_to(self.app_config.models_path):
+ # If the models is in the Invoke-managed models dir, we delete it
+ self.unconditionally_delete(key)
+ else:
+ # Else we only unregister it, leaving the file in place
+ self.unregister(key)
+
+ def unconditionally_delete(self, key: str) -> None: # noqa D102
+ model = self.record_store.get_model(key)
+ model_path = self.app_config.models_path / model.path
+ # Models are stored in a directory named by their key. To delete the model on disk, we delete the entire
+ # directory. However, the path we store in the model record may be either a file within the key directory,
+ # or the directory itself. So we have to handle both cases.
+ if model_path.is_file() or model_path.is_symlink():
+ # Delete the individual model file, not the entire parent directory.
+ # Other unrelated files may exist in the same directory.
+ model_path.unlink()
+ # Clean up the parent directory only if it is now empty
+ if model_path.parent != self.app_config.models_path and not any(model_path.parent.iterdir()):
+ model_path.parent.rmdir()
+ elif model_path.is_dir():
+ # Sanity check - folder models should be in their own directory under the models dir. The path should
+ # not be the Invoke models dir itself!
+ assert model_path != self.app_config.models_path
+ rmtree(model_path)
+ self.unregister(key)
+
+ @classmethod
+ def _download_cache_path(cls, source: Union[str, AnyHttpUrl], app_config: InvokeAIAppConfig) -> Path:
+ escaped_source = slugify(str(source))
+ return app_config.download_cache_path / escaped_source
+
+ def download_and_cache_model(
+ self,
+ source: str | AnyHttpUrl,
+ ) -> Path:
+ """Download the model file located at source to the models cache and return its Path."""
+ model_path = self._download_cache_path(str(source), self._app_config)
+
+ # We expect the cache directory to contain one and only one downloaded file or directory.
+ # We don't know the file's name in advance, as it is set by the download
+ # content-disposition header.
+ if model_path.exists():
+ contents: List[Path] = list(model_path.iterdir())
+ if len(contents) > 0:
+ return contents[0]
+
+ model_path.mkdir(parents=True, exist_ok=True)
+ model_source = self._guess_source(str(source))
+ remote_files, _ = self._remote_files_from_source(model_source)
+ # Handle multiple subfolders for HFModelSource
+ subfolders = model_source.subfolders if isinstance(model_source, HFModelSource) else []
+ job = self._multifile_download(
+ dest=model_path,
+ remote_files=remote_files,
+ subfolder=model_source.subfolder
+ if isinstance(model_source, HFModelSource) and len(subfolders) <= 1
+ else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ )
+ files_string = "file" if len(remote_files) == 1 else "files"
+ self._logger.info(f"Queuing model download: {source} ({len(remote_files)} {files_string})")
+ self._download_queue.wait_for_job(job)
+ if job.complete:
+ assert job.download_path is not None
+ return job.download_path
+ else:
+ raise Exception(job.error)
+
+ def _remote_files_from_source(
+ self, source: ModelSource
+ ) -> Tuple[List[RemoteModelFile], Optional[AnyModelRepoMetadata]]:
+ metadata = None
+ if isinstance(source, HFModelSource):
+ metadata = HuggingFaceMetadataFetch(self._session).from_id(source.repo_id, source.variant)
+ assert isinstance(metadata, ModelMetadataWithFiles)
+ # Use subfolders property which handles '+' separated multiple subfolders
+ subfolders = source.subfolders
+ return (
+ metadata.download_urls(
+ variant=source.variant or self._guess_variant(),
+ subfolder=source.subfolder if len(subfolders) <= 1 else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ session=self._session,
+ ),
+ metadata,
+ )
+
+ if isinstance(source, URLModelSource):
+ try:
+ fetcher = self.get_fetcher_from_url(str(source.url))
+ kwargs: dict[str, Any] = {"session": self._session}
+ metadata = fetcher(**kwargs).from_url(source.url)
+ assert isinstance(metadata, ModelMetadataWithFiles)
+ return metadata.download_urls(session=self._session), metadata
+ except ValueError:
+ pass
+
+ return [RemoteModelFile(url=self._normalize_huggingface_blob_url(source.url), path=Path("."), size=0)], None
+
+ raise Exception(f"No files associated with {source}")
+
+ def _guess_source(self, source: str) -> ModelSource:
+ """Turn a source string into a ModelSource object."""
+ variants = "|".join(ModelRepoVariant.__members__.values())
+ hf_repoid_re = f"^([^/:]+/[^/:]+)(?::({variants})?(?::/?([^:]+))?)?$"
+ source_obj: Optional[StringLikeSource] = None
+ source_stripped = source.strip('"')
+
+ if source_stripped.startswith("external://"):
+ external_id = source_stripped.removeprefix("external://")
+ provider_id, _, provider_model_id = external_id.partition("/")
+ if not provider_id or not provider_model_id:
+ raise ValueError(f"Invalid external model source: '{source_stripped}'")
+ source_obj = ExternalModelSource(provider_id=provider_id, provider_model_id=provider_model_id)
+ elif Path(source_stripped).exists(): # A local file or directory
+ source_obj = LocalModelSource(path=Path(source_stripped))
+ elif match := re.match(hf_repoid_re, source):
+ source_obj = HFModelSource(
+ repo_id=match.group(1),
+ variant=ModelRepoVariant(match.group(2)) if match.group(2) else None, # pass None rather than ''
+ subfolder=Path(match.group(3)) if match.group(3) else None,
+ )
+ elif re.match(r"^https?://[^/]+", source):
+ source_obj = URLModelSource(
+ url=Url(source),
+ )
+ else:
+ raise ValueError(f"Unsupported model source: '{source}'")
+ return source_obj
+
+ # --------------------------------------------------------------------------------------------
+ # Internal functions that manage the installer threads
+ # --------------------------------------------------------------------------------------------
+ def _start_installer_thread(self) -> None:
+ self._install_thread = threading.Thread(target=self._install_next_item, daemon=True)
+ self._install_thread.start()
+ self._running = True
+
+ @staticmethod
+ def _safe_rmtree(path: Path, logger: Any) -> None:
+ """Remove a directory tree with retry logic for Windows file locking issues.
+
+ On Windows, memory-mapped files may not be immediately released even after
+ the file handle is closed. This function retries the removal with garbage
+ collection to help release any lingering references.
+ """
+ max_retries = 3
+ retry_delay = 0.5 # seconds
+
+ for attempt in range(max_retries):
+ try:
+ # Force garbage collection to release any lingering file references
+ gc.collect()
+ rmtree(path)
+ return
+ except PermissionError as e:
+ if attempt < max_retries - 1 and sys.platform == "win32":
+ logger.warning(
+ f"Failed to remove {path} (attempt {attempt + 1}/{max_retries}): {e}. "
+ f"Retrying in {retry_delay}s..."
+ )
+ time.sleep(retry_delay)
+ retry_delay *= 2 # Exponential backoff
+ else:
+ logger.error(f"Failed to remove temporary directory {path}: {e}")
+ # On final failure, don't raise - the temp dir will be cleaned up on next startup
+ return
+ except Exception as e:
+ logger.error(f"Unexpected error removing {path}: {e}")
+ return
+
+ def _install_next_item(self) -> None:
+ self._logger.debug(f"Installer thread {threading.get_ident()} starting")
+ while True:
+ if self._stop_event.is_set():
+ break
+ self._logger.debug(f"Installer thread {threading.get_ident()} polling")
+ try:
+ job = self._install_queue.get(timeout=1)
+ except Empty:
+ continue
+ assert job.local_path is not None
+ try:
+ if job.cancelled:
+ self._signal_job_cancelled(job)
+
+ elif job.errored:
+ self._signal_job_errored(job)
+
+ elif job.waiting or job.downloads_done:
+ self._register_or_install(job)
+
+ except Exception as e:
+ # Expected errors include InvalidModelConfigException, DuplicateModelException, OSError, but we must
+ # gracefully handle _any_ error here.
+ self._set_error(job, e)
+
+ finally:
+ # if this is an install of a remote file, then clean up the temporary directory
+ if job._install_tmpdir is not None:
+ self._safe_rmtree(job._install_tmpdir, self._logger)
+ self._install_completed_event.set()
+ self._install_queue.task_done()
+ self._logger.info(f"Installer thread {threading.get_ident()} exiting")
+
+ def _register_or_install(self, job: ModelInstallJob) -> None:
+ if isinstance(job.source, ExternalModelSource):
+ self._register_external_model(job)
+ return
+ # local jobs will be in waiting state, remote jobs will be downloading state
+ job.total_bytes = self._stat_size(job.local_path)
+ job.bytes = job.total_bytes
+ self._signal_job_running(job)
+ job.config_in.source = str(job.source)
+ job.config_in.source_type = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
+ # enter the metadata, if there is any
+ if isinstance(job.source_metadata, (HuggingFaceMetadata)):
+ job.config_in.source_api_response = job.source_metadata.api_response
+
+ if job._install_tmpdir is not None:
+ self._delete_install_marker(job._install_tmpdir)
+
+ if job.inplace:
+ key = self.register_path(job.local_path, job.config_in)
+ else:
+ key = self.install_path(job.local_path, job.config_in)
+ job.config_out = self.record_store.get_model(key)
+ self._signal_job_completed(job)
+
+ def _register_external_model(self, job: ModelInstallJob) -> None:
+ job.total_bytes = 0
+ job.bytes = 0
+ self._signal_job_running(job)
+ job.config_in.source = str(job.source)
+ job.config_in.source_type = MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__]
+
+ provider_id = job.source.provider_id
+ provider_model_id = job.source.provider_model_id
+ capabilities = job.config_in.capabilities or ExternalModelCapabilities()
+ default_settings = (
+ job.config_in.default_settings
+ if isinstance(job.config_in.default_settings, ExternalApiModelDefaultSettings)
+ else None
+ )
+ name = job.config_in.name or f"{provider_id} {provider_model_id}"
+ key = job.config_in.key or slugify(f"{provider_id}-{provider_model_id}")
+
+ existing_external = next(
+ (
+ model
+ for model in self.record_store.search_by_attr(
+ base_model=BaseModelType.External, model_type=ModelType.ExternalImageGenerator
+ )
+ if isinstance(model, ExternalApiModelConfig)
+ and model.provider_id == provider_id
+ and model.provider_model_id == provider_model_id
+ ),
+ None,
+ )
+
+ if existing_external is not None:
+ key = existing_external.key
+ else:
+ try:
+ self.record_store.get_model(key)
+ raise DuplicateModelException(
+ f"Model key '{key}' already exists. Provide a different key to install this external model."
+ )
+ except UnknownModelException:
+ pass
+
+ config = ExternalApiModelConfig(
+ key=key,
+ name=name,
+ description=job.config_in.description,
+ provider_id=provider_id,
+ provider_model_id=provider_model_id,
+ capabilities=capabilities,
+ default_settings=default_settings,
+ source=str(job.source),
+ source_type=MODEL_SOURCE_TO_TYPE_MAP[job.source.__class__],
+ path="",
+ hash="",
+ file_size=0,
+ )
+
+ if existing_external is not None:
+ self.record_store.replace_model(existing_external.key, config)
+ else:
+ self.record_store.add_model(config)
+
+ job.config_out = self.record_store.get_model(config.key)
+ self._signal_job_completed(job)
+
+ def _set_error(self, install_job: ModelInstallJob, excp: Exception) -> None:
+ multifile_download_job = install_job._multifile_job
+ if multifile_download_job and any(
+ x.content_type is not None and "text/html" in x.content_type for x in multifile_download_job.download_parts
+ ):
+ install_job.set_error(
+ ValueError(
+ f"At least one file in {install_job.local_path} is an HTML page, not a model. This can happen when an access token is required to download."
+ )
+ )
+ else:
+ install_job.set_error(excp)
+ self._signal_job_errored(install_job)
+
+ # --------------------------------------------------------------------------------------------
+ # Internal functions that manage the models directory
+ # --------------------------------------------------------------------------------------------
+ def _remove_dangling_install_dirs(self) -> None:
+ """Remove leftover tmpdirs from aborted installs."""
+ path = self._app_config.models_path
+ for tmpdir in path.glob(f"{TMPDIR_PREFIX}*"):
+ marker = self._read_install_marker(tmpdir)
+ if marker is None:
+ self._logger.info(f"Removing dangling temporary directory {tmpdir}")
+ self._safe_rmtree(tmpdir, self._logger)
+ continue
+ status = marker.get("status")
+ if status in {InstallStatus.COMPLETED.value, InstallStatus.ERROR.value, InstallStatus.CANCELLED.value}:
+ self._logger.info(f"Removing completed/errored temporary directory {tmpdir}")
+ self._safe_rmtree(tmpdir, self._logger)
+
+ def _scan_for_missing_models(self) -> list[AnyModelConfig]:
+ """Scan the models directory for missing models and return a list of them."""
+ missing_models: list[AnyModelConfig] = []
+ for model_config in self.record_store.all_models():
+ if model_config.base == BaseModelType.External or model_config.format == ModelFormat.ExternalApi:
+ continue
+ if not (self.app_config.models_path / model_config.path).resolve().exists():
+ missing_models.append(model_config)
+ return missing_models
+
+ def _register_orphaned_models(self) -> None:
+ """Scan the invoke-managed models directory for orphaned models and registers them.
+
+ This is typically only used during testing with a new DB or when using the memory DB, because those are the
+ only situations in which we may have orphaned models in the models directory.
+ """
+ installed_model_paths = {
+ (self._app_config.models_path / x.path).resolve() for x in self.record_store.all_models()
+ }
+
+ # The bool returned by this callback determines if the model is added to the list of models found by the search
+ def on_model_found(model_path: Path) -> bool:
+ resolved_path = model_path.resolve()
+ # Already registered models should be in the list of found models, but not re-registered.
+ if resolved_path in installed_model_paths:
+ return True
+ # Skip core models entirely - these aren't registered with the model manager.
+ for special_directory in [
+ self.app_config.models_path / "core",
+ self.app_config.convert_cache_dir,
+ self.app_config.download_cache_dir,
+ ]:
+ if resolved_path.is_relative_to(special_directory):
+ return False
+ try:
+ model_id = self.register_path(model_path)
+ self._logger.info(f"Registered {model_path.name} with id {model_id}")
+ except DuplicateModelException:
+ # In case a duplicate models sneaks by, we will ignore this error - we "found" the model
+ pass
+ return True
+
+ self._logger.info(f"Scanning {self._app_config.models_path} for orphaned models")
+ search = ModelSearch(on_model_found=on_model_found)
+ found_models = search.search(self._app_config.models_path)
+ self._logger.info(f"{len(found_models)} new models registered")
+
+ def _probe(self, model_path: Path, config: Optional[ModelRecordChanges] = None):
+ config = config or ModelRecordChanges()
+ hash_algo = self._app_config.hashing_algorithm
+ fields = config.model_dump()
+
+ result = ModelConfigFactory.from_model_on_disk(
+ mod=model_path,
+ override_fields=deepcopy(fields),
+ hash_algo=hash_algo,
+ allow_unknown=self.app_config.allow_unknown_models,
+ )
+
+ if result.config is None:
+ self._logger.error(f"Could not identify model for {model_path}, detailed results: {result.details}")
+ raise InvalidModelConfigException(f"Could not identify model for {model_path}")
+ elif isinstance(result.config, Unknown_Config):
+ self._logger.error(f"Could not identify model for {model_path}, detailed results: {result.details}")
+
+ return result.config
+
+ def _register(
+ self, model_path: Path, config: Optional[ModelRecordChanges] = None, info: Optional[AnyModelConfig] = None
+ ) -> str:
+ config = config or ModelRecordChanges()
+
+ info = info or self._probe(model_path, config)
+
+ # Apply LoRA metadata if applicable
+ model_images_path = self.app_config.models_path / "model_images"
+ apply_lora_metadata(info, model_path.resolve(), model_images_path)
+
+ model_path = model_path.resolve()
+
+ # Models in the Invoke-managed models dir should use relative paths.
+ if model_path.is_relative_to(self.app_config.models_path):
+ model_path = model_path.relative_to(self.app_config.models_path)
+
+ info.path = model_path.as_posix()
+
+ if isinstance(info, Checkpoint_Config_Base) and info.config_path is not None:
+ # Checkpoints have a config file needed for conversion. Same handling as the model weights - if it's in the
+ # invoke-managed legacy config dir, we use a relative path.
+ legacy_config_path = self.app_config.legacy_conf_path / info.config_path
+ if legacy_config_path.is_relative_to(self.app_config.legacy_conf_path):
+ legacy_config_path = legacy_config_path.relative_to(self.app_config.legacy_conf_path)
+ info.config_path = legacy_config_path.as_posix()
+ self.record_store.add_model(info)
+ return info.key
+
+ def _next_id(self) -> int:
+ with self._lock:
+ id = self._next_job_id
+ self._next_job_id += 1
+ return id
+
+ def _guess_variant(self) -> Optional[ModelRepoVariant]:
+ """Guess the best HuggingFace variant type to download."""
+ precision = TorchDevice.choose_torch_dtype()
+ return ModelRepoVariant.FP16 if precision == torch.float16 else None
+
+ def _import_local_model(
+ self, source: LocalModelSource, config: Optional[ModelRecordChanges] = None
+ ) -> ModelInstallJob:
+ return ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config or ModelRecordChanges(),
+ local_path=Path(source.path),
+ inplace=source.inplace or False,
+ )
+
+ def _import_from_hf(
+ self,
+ source: HFModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ # Add user's cached access token to HuggingFace requests
+ if source.access_token is None:
+ source.access_token = hf_get_token()
+ remote_files, metadata = self._remote_files_from_source(source)
+ return self._import_remote_model(
+ source=source,
+ config=config,
+ remote_files=remote_files,
+ metadata=metadata,
+ )
+
+ def _import_from_url(
+ self,
+ source: URLModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ remote_files, metadata = self._remote_files_from_source(source)
+ return self._import_remote_model(
+ source=source,
+ config=config,
+ metadata=metadata,
+ remote_files=remote_files,
+ )
+
+ def _import_external_model(
+ self,
+ source: ExternalModelSource,
+ config: Optional[ModelRecordChanges] = None,
+ ) -> ModelInstallJob:
+ return ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config or ModelRecordChanges(),
+ local_path=self._app_config.models_path,
+ inplace=True,
+ )
+
+ def _import_remote_model(
+ self,
+ source: HFModelSource | URLModelSource,
+ remote_files: List[RemoteModelFile],
+ metadata: Optional[AnyModelRepoMetadata],
+ config: Optional[ModelRecordChanges],
+ ) -> ModelInstallJob:
+ if len(remote_files) == 0:
+ raise ValueError(f"{source}: No downloadable files found")
+ destdir = self._find_reusable_tmpdir(source)
+ if destdir is None:
+ destdir = Path(
+ mkdtemp(
+ dir=self._app_config.models_path,
+ prefix=TMPDIR_PREFIX,
+ )
+ )
+ install_job = ModelInstallJob(
+ id=self._next_id(),
+ source=source,
+ config_in=config or ModelRecordChanges(),
+ source_metadata=metadata,
+ local_path=destdir, # local path may change once the download has started due to content-disposition handling
+ bytes=0,
+ total_bytes=0,
+ )
+ # remember the temporary directory for later removal
+ install_job._install_tmpdir = destdir
+
+ # Handle multiple subfolders for HFModelSource
+ subfolders = source.subfolders if isinstance(source, HFModelSource) else []
+ return self._enqueue_remote_download(
+ job=install_job,
+ source=source,
+ remote_files=remote_files,
+ metadata=metadata,
+ destdir=destdir,
+ subfolder=source.subfolder if isinstance(source, HFModelSource) and len(subfolders) <= 1 else None,
+ subfolders=subfolders if len(subfolders) > 1 else None,
+ )
+
+ def _enqueue_remote_download(
+ self,
+ job: ModelInstallJob,
+ source: HFModelSource | URLModelSource,
+ remote_files: List[RemoteModelFile],
+ metadata: Optional[AnyModelRepoMetadata],
+ destdir: Path,
+ subfolder: Optional[Path] = None,
+ subfolders: Optional[List[Path]] = None,
+ resume_metadata: Optional[dict] = None,
+ clear_partials: bool = False,
+ ) -> ModelInstallJob:
+ job.source_metadata = metadata
+ job.local_path = destdir
+ job._install_tmpdir = destdir
+ job.total_bytes = sum((x.size or 0) for x in remote_files)
+
+ multifile_job = self._multifile_download(
+ remote_files=remote_files,
+ dest=destdir,
+ subfolder=subfolder,
+ subfolders=subfolders,
+ access_token=source.access_token,
+ submit_job=False, # Important! Don't submit the job until we have set our _download_cache dict
+ )
+ if clear_partials:
+ for part in multifile_job.download_parts:
+ target_path = part.dest
+ if target_path.exists():
+ try:
+ self._logger.info(f"Deleting partial file before restart: {target_path}")
+ target_path.unlink()
+ except Exception:
+ pass
+ in_progress_path = target_path.with_name(target_path.name + ".downloading")
+ if in_progress_path.exists():
+ try:
+ self._logger.info(f"Deleting partial file before restart: {in_progress_path}")
+ in_progress_path.unlink()
+ except Exception:
+ pass
+ if resume_metadata:
+ for part in multifile_job.download_parts:
+ meta = resume_metadata.get(str(part.source))
+ if not meta:
+ continue
+ part.canonical_url = meta.get("canonical_url") or part.canonical_url
+ part.etag = meta.get("etag") or part.etag
+ part.last_modified = meta.get("last_modified") or part.last_modified
+ part.expected_total_bytes = meta.get("expected_total_bytes") or part.expected_total_bytes
+ part.final_url = meta.get("final_url") or part.final_url
+ if meta.get("download_path"):
+ part.download_path = Path(meta.get("download_path"))
+ self._download_cache[multifile_job.id] = job
+ job._multifile_job = multifile_job
+
+ self._write_install_marker(job, status=InstallStatus.WAITING)
+ files_string = "file" if len(remote_files) == 1 else "files"
+ self._logger.info(f"Queueing model install: {source} ({len(remote_files)} {files_string})")
+ self._logger.debug(f"remote_files={remote_files}")
+ self._download_queue.submit_multifile_download(multifile_job)
+ return job
+
+ def _stat_size(self, path: Path) -> int:
+ size = 0
+ if path.is_file():
+ size = path.stat().st_size
+ elif path.is_dir():
+ for root, _, files in os.walk(path):
+ size += sum(self._stat_size(Path(root, x)) for x in files)
+ return size
+
+ def _multifile_download(
+ self,
+ remote_files: List[RemoteModelFile],
+ dest: Path,
+ subfolder: Optional[Path] = None,
+ subfolders: Optional[List[Path]] = None,
+ access_token: Optional[str] = None,
+ submit_job: bool = True,
+ ) -> MultiFileDownloadJob:
+ # HuggingFace repo subfolders are a little tricky. If the name of the model is "sdxl-turbo", and
+ # we are installing the "vae" subfolder, we do not want to create an additional folder level, such
+ # as "sdxl-turbo/vae", nor do we want to put the contents of the vae folder directly into "sdxl-turbo".
+ # So what we do is to synthesize a folder named "sdxl-turbo_vae" here.
+ #
+ # For multiple subfolders (e.g., text_encoder+tokenizer), we create a combined folder name
+ # (e.g., sdxl-turbo_text_encoder_tokenizer) and keep each subfolder's contents in its own
+ # subdirectory within the model folder.
+
+ if subfolders and len(subfolders) > 1:
+ # Multiple subfolders: create combined name and keep subfolder structure
+ top = Path(remote_files[0].path.parts[0]) # e.g. "Z-Image-Turbo/"
+ subfolder_names = [sf.name.replace("/", "_").replace("\\", "_") for sf in subfolders]
+ combined_name = "_".join(subfolder_names)
+ path_to_add = Path(f"{top}_{combined_name}")
+
+ parts: List[RemoteModelFile] = []
+ for model_file in remote_files:
+ assert model_file.size is not None
+ # Determine which subfolder this file belongs to
+ file_path = model_file.path
+ new_path: Optional[Path] = None
+ for sf in subfolders:
+ try:
+ # Try to get relative path from this subfolder
+ relative = file_path.relative_to(top / sf)
+ # Keep the subfolder name as a subdirectory
+ new_path = path_to_add / sf.name / relative
+ break
+ except ValueError:
+ continue
+
+ if new_path is None:
+ # File doesn't match any subfolder, keep original path structure
+ new_path = path_to_add / file_path.relative_to(top)
+
+ parts.append(RemoteModelFile(url=model_file.url, path=new_path))
+ elif subfolder:
+ # Single subfolder: flatten into renamed folder
+ top = Path(remote_files[0].path.parts[0]) # e.g. "sdxl-turbo/"
+ path_to_remove = top / subfolder # sdxl-turbo/vae/
+ subfolder_rename = subfolder.name.replace("/", "_").replace("\\", "_")
+ path_to_add = Path(f"{top}_{subfolder_rename}")
+
+ parts = []
+ for model_file in remote_files:
+ assert model_file.size is not None
+ parts.append(
+ RemoteModelFile(
+ url=model_file.url,
+ path=path_to_add / model_file.path.relative_to(path_to_remove),
+ )
+ )
+ else:
+ # No subfolder specified - pass through unchanged
+ parts = []
+ for model_file in remote_files:
+ assert model_file.size is not None
+ parts.append(RemoteModelFile(url=model_file.url, path=model_file.path))
+
+ return self._download_queue.multifile_download(
+ parts=parts,
+ dest=dest,
+ access_token=access_token,
+ submit_job=submit_job,
+ on_start=self._download_started_callback,
+ on_progress=self._download_progress_callback,
+ on_complete=self._download_complete_callback,
+ on_error=self._download_error_callback,
+ on_cancelled=self._download_cancelled_callback,
+ )
+
+ # ------------------------------------------------------------------
+ # Callbacks are executed by the download queue in a separate thread
+ # ------------------------------------------------------------------
+ def _download_started_callback(self, download_job: MultiFileDownloadJob) -> None:
+ with self._lock:
+ if install_job := self._download_cache.get(download_job.id, None):
+ install_job.status = InstallStatus.DOWNLOADING
+
+ if install_job.local_path == install_job._install_tmpdir: # first time
+ assert download_job.download_path
+ install_job.local_path = download_job.download_path
+ install_job.download_parts = download_job.download_parts
+ install_job.bytes = sum(x.bytes for x in download_job.download_parts)
+ total_parts = sum(x.total_bytes for x in download_job.download_parts)
+ if total_parts > 0:
+ install_job.total_bytes = max(install_job.total_bytes or 0, total_parts)
+ self._signal_job_download_started(install_job)
+
+ def _download_progress_callback(self, download_job: MultiFileDownloadJob) -> None:
+ with self._lock:
+ if install_job := self._download_cache.get(download_job.id, None):
+ if install_job.cancelled: # This catches the case in which the caller directly calls job.cancel()
+ self._download_queue.cancel_job(download_job)
+ else:
+ # update sizes
+ install_job.bytes = sum(x.bytes for x in download_job.download_parts)
+ total_parts = sum(x.total_bytes for x in download_job.download_parts)
+ if total_parts > 0:
+ install_job.total_bytes = max(install_job.total_bytes or 0, total_parts)
+ self._signal_job_downloading(install_job)
+
+ def _download_complete_callback(self, download_job: MultiFileDownloadJob) -> None:
+ with self._lock:
+ if install_job := self._download_cache.pop(download_job.id, None):
+ self._signal_job_downloads_done(install_job)
+ self._put_in_queue(install_job) # this starts the installation and registration
+
+ # Let other threads know that the number of downloads has changed
+ self._downloads_changed_event.set()
+
+ def _download_error_callback(self, download_job: MultiFileDownloadJob, excp: Optional[Exception] = None) -> None:
+ with self._lock:
+ if install_job := self._download_cache.pop(download_job.id, None):
+ assert excp is not None
+ self._set_error(install_job, excp)
+ self._download_queue.cancel_job(download_job)
+ if install_job._install_tmpdir is not None:
+ self._safe_rmtree(install_job._install_tmpdir, self._logger)
+
+ # Let other threads know that the number of downloads has changed
+ self._downloads_changed_event.set()
+
+ def _download_cancelled_callback(self, download_job: MultiFileDownloadJob) -> None:
+ with self._lock:
+ if install_job := self._download_cache.pop(download_job.id, None):
+ self._downloads_changed_event.set()
+ if any(part.resume_required for part in download_job.download_parts):
+ install_job.status = InstallStatus.PAUSED
+ self._write_install_marker(install_job, status=InstallStatus.PAUSED)
+ self._downloads_changed_event.set()
+ return
+ # if install job has already registered an error, then do not replace its status with cancelled
+ if not install_job.errored and not install_job.paused:
+ install_job.cancel()
+ if install_job._install_tmpdir is not None:
+ # Mark cancelled before cleanup so we don't reuse the folder if deletion fails.
+ self._write_install_marker(install_job, status=InstallStatus.CANCELLED)
+ self._delete_install_marker(install_job._install_tmpdir)
+ self._safe_rmtree(install_job._install_tmpdir, self._logger)
+
+ # Let other threads know that the number of downloads has changed
+ self._downloads_changed_event.set()
+
+ # ------------------------------------------------------------------------------------------------
+ # Internal methods that put events on the event bus
+ # ------------------------------------------------------------------------------------------------
+ def _signal_job_running(self, job: ModelInstallJob) -> None:
+ job.status = InstallStatus.RUNNING
+ self._logger.info(f"Model install started: {job.source}")
+ self._write_install_marker(job, status=InstallStatus.RUNNING)
+ if self._event_bus:
+ self._event_bus.emit_model_install_started(job)
+
+ def _signal_job_download_started(self, job: ModelInstallJob) -> None:
+ if self._event_bus:
+ assert job._multifile_job is not None
+ assert job.bytes is not None
+ assert job.total_bytes is not None
+ self._event_bus.emit_model_install_download_started(job)
+ self._write_install_marker(job, status=InstallStatus.DOWNLOADING)
+
+ def _signal_job_downloading(self, job: ModelInstallJob) -> None:
+ if self._event_bus:
+ assert job._multifile_job is not None
+ assert job.bytes is not None
+ assert job.total_bytes is not None
+ self._event_bus.emit_model_install_download_progress(job)
+
+ def _signal_job_downloads_done(self, job: ModelInstallJob) -> None:
+ job.status = InstallStatus.DOWNLOADS_DONE
+ self._logger.info(f"Model download complete: {job.source}")
+ self._write_install_marker(job, status=InstallStatus.DOWNLOADS_DONE)
+ if self._event_bus:
+ self._event_bus.emit_model_install_downloads_complete(job)
+
+ def _signal_job_completed(self, job: ModelInstallJob) -> None:
+ job.status = InstallStatus.COMPLETED
+ assert job.config_out
+ self._logger.info(f"Model install complete: {job.source}")
+ self._logger.debug(f"{job.local_path} registered key {job.config_out.key}")
+ if job._install_tmpdir is not None:
+ self._delete_install_marker(job._install_tmpdir)
+ if self._event_bus:
+ assert job.local_path is not None
+ assert job.config_out is not None
+ self._event_bus.emit_model_install_complete(job)
+
+ def _signal_job_errored(self, job: ModelInstallJob) -> None:
+ self._logger.error(f"Model install error: {job.source}\n{job.error_type}: {job.error}")
+ if job._install_tmpdir is not None:
+ self._delete_install_marker(job._install_tmpdir)
+ if self._event_bus:
+ assert job.error_type is not None
+ assert job.error is not None
+ self._event_bus.emit_model_install_error(job)
+
+ def _signal_job_cancelled(self, job: ModelInstallJob) -> None:
+ self._logger.info(f"Model install canceled: {job.source}")
+ if job._install_tmpdir is not None:
+ self._delete_install_marker(job._install_tmpdir)
+ if self._event_bus:
+ self._event_bus.emit_model_install_cancelled(job)
+
+ @staticmethod
+ def get_fetcher_from_url(url: str) -> Type[ModelMetadataFetchBase]:
+ """
+ Return a metadata fetcher appropriate for provided url.
+
+ This used to be more useful, but the number of supported model
+ sources has been reduced to HuggingFace alone.
+ """
+ if re.match(r"^https?://huggingface.co/[^/]+/[^/]+$", url.lower()):
+ return HuggingFaceMetadataFetch
+ raise ValueError(f"Unsupported model source: '{url}'")
+
+ @staticmethod
+ def _normalize_huggingface_blob_url(url: AnyHttpUrl) -> Url:
+ """Convert Hugging Face file page URLs to direct download URLs."""
+ return Url(
+ re.sub(
+ r"^(https?://huggingface\.co/[^/]+/[^/]+)/blob/([^?#]+)([?#].*)?$",
+ r"\1/resolve/\2\3",
+ str(url),
+ flags=re.IGNORECASE,
+ )
+ )
diff --git a/invokeai/app/services/model_load/__init__.py b/invokeai/app/services/model_load/__init__.py
new file mode 100644
index 00000000000..4c7e40c8c76
--- /dev/null
+++ b/invokeai/app/services/model_load/__init__.py
@@ -0,0 +1,6 @@
+"""Initialization file for model load service module."""
+
+from invokeai.app.services.model_load.model_load_base import ModelLoadServiceBase
+from invokeai.app.services.model_load.model_load_default import ModelLoadService
+
+__all__ = ["ModelLoadServiceBase", "ModelLoadService"]
diff --git a/invokeai/app/services/model_load/model_load_base.py b/invokeai/app/services/model_load/model_load_base.py
new file mode 100644
index 00000000000..87a405b4ea4
--- /dev/null
+++ b/invokeai/app/services/model_load/model_load_base.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Team
+"""Base class for model loader."""
+
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Callable, Optional
+
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.load import LoadedModel, LoadedModelWithoutConfig
+from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
+from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType
+
+
+class ModelLoadServiceBase(ABC):
+ """Wrapper around AnyModelLoader."""
+
+ @abstractmethod
+ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel:
+ """
+ Given a model's configuration, load it and return the LoadedModel object.
+
+ :param model_config: Model configuration record (as returned by ModelRecordBase.get_model())
+ :param submodel: For main (pipeline models), the submodel to fetch.
+ """
+
+ @property
+ @abstractmethod
+ def ram_cache(self) -> ModelCache:
+ """Return the RAM cache used by this loader."""
+
+ @abstractmethod
+ def load_model_from_path(
+ self, model_path: Path, loader: Optional[Callable[[Path], AnyModel]] = None
+ ) -> LoadedModelWithoutConfig:
+ """
+ Load the model file or directory located at the indicated Path.
+
+ This will load an arbitrary model file into the RAM cache. If the optional loader
+ argument is provided, the loader will be invoked to load the model into
+ memory. Otherwise the method will call safetensors.torch.load_file() or
+ torch.load() as appropriate to the file suffix.
+
+ Be aware that this returns a LoadedModelWithoutConfig object, which is the same as
+ LoadedModel, but without the config attribute.
+
+ Args:
+ model_path: A pathlib.Path to a checkpoint-style models file
+ loader: A Callable that expects a Path and returns a Dict[str, Tensor]
+
+ Returns:
+ A LoadedModel object.
+ """
diff --git a/invokeai/app/services/model_load/model_load_default.py b/invokeai/app/services/model_load/model_load_default.py
new file mode 100644
index 00000000000..2e2d2ae219d
--- /dev/null
+++ b/invokeai/app/services/model_load/model_load_default.py
@@ -0,0 +1,128 @@
+# Copyright (c) 2024 Lincoln D. Stein and the InvokeAI Team
+"""Implementation of model loader service."""
+
+from pathlib import Path
+from typing import Callable, Optional, Type
+
+from picklescan.scanner import scan_file_path
+from safetensors.torch import load_file as safetensors_load_file
+from torch import load as torch_load
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_load.model_load_base import ModelLoadServiceBase
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.load import (
+ LoadedModel,
+ LoadedModelWithoutConfig,
+ ModelLoaderRegistry,
+ ModelLoaderRegistryBase,
+)
+from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
+from invokeai.backend.model_manager.load.model_loaders.generic_diffusers import GenericDiffusersLoader
+from invokeai.backend.model_manager.taxonomy import AnyModel, SubModelType
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.logging import InvokeAILogger
+
+
+class ModelLoadService(ModelLoadServiceBase):
+ """Wrapper around ModelLoaderRegistry."""
+
+ def __init__(
+ self,
+ app_config: InvokeAIAppConfig,
+ ram_cache: ModelCache,
+ registry: Optional[Type[ModelLoaderRegistryBase]] = ModelLoaderRegistry,
+ ):
+ """Initialize the model load service."""
+ logger = InvokeAILogger.get_logger(self.__class__.__name__)
+ logger.setLevel(app_config.log_level.upper())
+ self._logger = logger
+ self._app_config = app_config
+ self._ram_cache = ram_cache
+ self._registry = registry
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ @property
+ def ram_cache(self) -> ModelCache:
+ """Return the RAM cache used by this loader."""
+ return self._ram_cache
+
+ def load_model(self, model_config: AnyModelConfig, submodel_type: Optional[SubModelType] = None) -> LoadedModel:
+ """
+ Given a model's configuration, load it and return the LoadedModel object.
+
+ :param model_config: Model configuration record (as returned by ModelRecordBase.get_model())
+ :param submodel: For main (pipeline models), the submodel to fetch.
+ """
+
+ # We don't have an invoker during testing
+ # TODO(psyche): Mock this method on the invoker in the tests
+ if hasattr(self, "_invoker"):
+ self._invoker.services.events.emit_model_load_started(model_config, submodel_type)
+
+ implementation, model_config, submodel_type = self._registry.get_implementation(model_config, submodel_type) # type: ignore
+ loaded_model: LoadedModel = implementation(
+ app_config=self._app_config,
+ logger=self._logger,
+ ram_cache=self._ram_cache,
+ ).load_model(model_config, submodel_type)
+
+ if hasattr(self, "_invoker"):
+ self._invoker.services.events.emit_model_load_complete(model_config, submodel_type)
+
+ return loaded_model
+
+ def load_model_from_path(
+ self, model_path: Path, loader: Optional[Callable[[Path], AnyModel]] = None
+ ) -> LoadedModelWithoutConfig:
+ cache_key = str(model_path)
+ try:
+ return LoadedModelWithoutConfig(cache_record=self._ram_cache.get(key=cache_key), cache=self._ram_cache)
+ except IndexError:
+ pass
+
+ def torch_load_file(checkpoint: Path) -> AnyModel:
+ scan_result = scan_file_path(checkpoint)
+ if scan_result.infected_files != 0:
+ if self._app_config.unsafe_disable_picklescan:
+ self._logger.warning(
+ f"Model at {checkpoint} is potentially infected by malware, but picklescan is disabled. "
+ "Proceeding with caution."
+ )
+ else:
+ raise Exception(f"The model at {checkpoint} is potentially infected by malware. Aborting load.")
+ if scan_result.scan_err:
+ if self._app_config.unsafe_disable_picklescan:
+ self._logger.warning(
+ f"Error scanning model at {checkpoint} for malware, but picklescan is disabled. "
+ "Proceeding with caution."
+ )
+ else:
+ raise Exception(f"Error scanning model at {checkpoint} for malware. Aborting load.")
+
+ result = torch_load(checkpoint, map_location="cpu")
+ return result
+
+ def diffusers_load_directory(directory: Path) -> AnyModel:
+ load_class = GenericDiffusersLoader(
+ app_config=self._app_config,
+ logger=self._logger,
+ ram_cache=self._ram_cache,
+ convert_cache=self.convert_cache,
+ ).get_hf_load_class(directory)
+ return load_class.from_pretrained(model_path, torch_dtype=TorchDevice.choose_torch_dtype())
+
+ loader = loader or (
+ diffusers_load_directory
+ if model_path.is_dir()
+ else torch_load_file
+ if model_path.suffix.endswith((".ckpt", ".pt", ".pth", ".bin"))
+ else lambda path: safetensors_load_file(path, device="cpu")
+ )
+ assert loader is not None
+ raw_model = loader(model_path)
+ self._ram_cache.put(key=cache_key, model=raw_model)
+ return LoadedModelWithoutConfig(cache_record=self._ram_cache.get(key=cache_key), cache=self._ram_cache)
diff --git a/invokeai/app/services/model_manager/__init__.py b/invokeai/app/services/model_manager/__init__.py
new file mode 100644
index 00000000000..e703d4f1ffc
--- /dev/null
+++ b/invokeai/app/services/model_manager/__init__.py
@@ -0,0 +1,10 @@
+"""Initialization file for model manager service."""
+
+from invokeai.app.services.model_manager.model_manager_default import ModelManagerService, ModelManagerServiceBase
+from invokeai.backend.model_manager.load import LoadedModel
+
+__all__ = [
+ "ModelManagerServiceBase",
+ "ModelManagerService",
+ "LoadedModel",
+]
diff --git a/invokeai/app/services/model_manager/model_manager_base.py b/invokeai/app/services/model_manager/model_manager_base.py
new file mode 100644
index 00000000000..a906076b163
--- /dev/null
+++ b/invokeai/app/services/model_manager/model_manager_base.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team
+
+from abc import ABC, abstractmethod
+
+import torch
+from typing_extensions import Self
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.download.download_base import DownloadQueueServiceBase
+from invokeai.app.services.events.events_base import EventServiceBase
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
+from invokeai.app.services.model_load.model_load_base import ModelLoadServiceBase
+from invokeai.app.services.model_records.model_records_base import ModelRecordServiceBase
+
+
+class ModelManagerServiceBase(ABC):
+ """Abstract base class for the model manager service."""
+
+ # attributes:
+ # store: ModelRecordServiceBase = Field(description="An instance of the model record configuration service.")
+ # install: ModelInstallServiceBase = Field(description="An instance of the model install service.")
+ # load: ModelLoadServiceBase = Field(description="An instance of the model load service.")
+
+ @classmethod
+ @abstractmethod
+ def build_model_manager(
+ cls,
+ app_config: InvokeAIAppConfig,
+ model_record_service: ModelRecordServiceBase,
+ download_queue: DownloadQueueServiceBase,
+ events: EventServiceBase,
+ execution_device: torch.device,
+ ) -> Self:
+ """
+ Construct the model manager service instance.
+
+ Use it rather than the __init__ constructor. This class
+ method simplifies the construction considerably.
+ """
+ pass
+
+ @property
+ @abstractmethod
+ def store(self) -> ModelRecordServiceBase:
+ """Return the ModelRecordServiceBase used to store and retrieve configuration records."""
+ pass
+
+ @property
+ @abstractmethod
+ def load(self) -> ModelLoadServiceBase:
+ """Return the ModelLoadServiceBase used to load models from their configuration records."""
+ pass
+
+ @property
+ @abstractmethod
+ def install(self) -> ModelInstallServiceBase:
+ """Return the ModelInstallServiceBase used to download and manipulate model files."""
+ pass
+
+ @abstractmethod
+ def start(self, invoker: Invoker) -> None:
+ pass
+
+ @abstractmethod
+ def stop(self, invoker: Invoker) -> None:
+ pass
diff --git a/invokeai/app/services/model_manager/model_manager_common.py b/invokeai/app/services/model_manager/model_manager_common.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/model_manager/model_manager_default.py b/invokeai/app/services/model_manager/model_manager_default.py
new file mode 100644
index 00000000000..6141a635f4d
--- /dev/null
+++ b/invokeai/app/services/model_manager/model_manager_default.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Team
+"""Implementation of ModelManagerServiceBase."""
+
+from typing import Optional
+
+import torch
+from typing_extensions import Self
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.download.download_base import DownloadQueueServiceBase
+from invokeai.app.services.events.events_base import EventServiceBase
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_install.model_install_base import ModelInstallServiceBase
+from invokeai.app.services.model_install.model_install_default import ModelInstallService
+from invokeai.app.services.model_load.model_load_base import ModelLoadServiceBase
+from invokeai.app.services.model_load.model_load_default import ModelLoadService
+from invokeai.app.services.model_manager.model_manager_base import ModelManagerServiceBase
+from invokeai.app.services.model_records.model_records_base import ModelRecordServiceBase
+from invokeai.backend.model_manager.load.model_cache.model_cache import ModelCache
+from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.logging import InvokeAILogger
+
+
+class ModelManagerService(ModelManagerServiceBase):
+ """
+ The ModelManagerService handles various aspects of model installation, maintenance and loading.
+
+ It bundles three distinct services:
+ model_manager.store -- Routines to manage the database of model configuration records.
+ model_manager.install -- Routines to install, move and delete models.
+ model_manager.load -- Routines to load models into memory.
+ """
+
+ def __init__(
+ self,
+ store: ModelRecordServiceBase,
+ install: ModelInstallServiceBase,
+ load: ModelLoadServiceBase,
+ ):
+ self._store = store
+ self._install = install
+ self._load = load
+
+ @property
+ def store(self) -> ModelRecordServiceBase:
+ return self._store
+
+ @property
+ def install(self) -> ModelInstallServiceBase:
+ return self._install
+
+ @property
+ def load(self) -> ModelLoadServiceBase:
+ return self._load
+
+ def start(self, invoker: Invoker) -> None:
+ for service in [self._store, self._install, self._load]:
+ if hasattr(service, "start"):
+ service.start(invoker)
+
+ def stop(self, invoker: Invoker) -> None:
+ # Shutdown the model cache to cancel any pending timers
+ if hasattr(self._load, "ram_cache"):
+ self._load.ram_cache.shutdown()
+
+ for service in [self._store, self._install, self._load]:
+ if hasattr(service, "stop"):
+ service.stop(invoker)
+
+ @classmethod
+ def build_model_manager(
+ cls,
+ app_config: InvokeAIAppConfig,
+ model_record_service: ModelRecordServiceBase,
+ download_queue: DownloadQueueServiceBase,
+ events: EventServiceBase,
+ execution_device: Optional[torch.device] = None,
+ ) -> Self:
+ """
+ Construct the model manager service instance.
+
+ For simplicity, use this class method rather than the __init__ constructor.
+ """
+ logger = InvokeAILogger.get_logger(cls.__name__)
+ logger.setLevel(app_config.log_level.upper())
+
+ ram_cache = ModelCache(
+ execution_device_working_mem_gb=app_config.device_working_mem_gb,
+ enable_partial_loading=app_config.enable_partial_loading,
+ keep_ram_copy_of_weights=app_config.keep_ram_copy_of_weights,
+ max_ram_cache_size_gb=app_config.max_cache_ram_gb,
+ max_vram_cache_size_gb=app_config.max_cache_vram_gb,
+ execution_device=execution_device or TorchDevice.choose_torch_device(),
+ storage_device="cpu",
+ log_memory_usage=app_config.log_memory_usage,
+ logger=logger,
+ keep_alive_minutes=app_config.model_cache_keep_alive_min,
+ )
+ loader = ModelLoadService(
+ app_config=app_config,
+ ram_cache=ram_cache,
+ registry=ModelLoaderRegistry,
+ )
+ installer = ModelInstallService(
+ app_config=app_config,
+ record_store=model_record_service,
+ download_queue=download_queue,
+ event_bus=events,
+ )
+ return cls(store=model_record_service, install=installer, load=loader)
diff --git a/invokeai/app/services/model_records/__init__.py b/invokeai/app/services/model_records/__init__.py
new file mode 100644
index 00000000000..4fee477466d
--- /dev/null
+++ b/invokeai/app/services/model_records/__init__.py
@@ -0,0 +1,23 @@
+"""Init file for model record services."""
+
+from .model_records_base import ( # noqa F401
+ DuplicateModelException,
+ InvalidModelException,
+ ModelRecordServiceBase,
+ UnknownModelException,
+ ModelSummary,
+ ModelRecordChanges,
+ ModelRecordOrderBy,
+)
+from .model_records_sql import ModelRecordServiceSQL # noqa F401
+
+__all__ = [
+ "ModelRecordServiceBase",
+ "ModelRecordServiceSQL",
+ "DuplicateModelException",
+ "InvalidModelException",
+ "UnknownModelException",
+ "ModelSummary",
+ "ModelRecordChanges",
+ "ModelRecordOrderBy",
+]
diff --git a/invokeai/app/services/model_records/model_records_base.py b/invokeai/app/services/model_records/model_records_base.py
new file mode 100644
index 00000000000..e06f8f2df91
--- /dev/null
+++ b/invokeai/app/services/model_records/model_records_base.py
@@ -0,0 +1,298 @@
+# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team
+"""
+Abstract base class for storing and retrieving model configuration records.
+"""
+
+from abc import ABC, abstractmethod
+from enum import Enum
+from pathlib import Path
+from typing import Any, List, Optional, Set, Union
+
+from pydantic import BaseModel, Field, field_validator
+
+from invokeai.app.services.shared.pagination import PaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+from invokeai.backend.model_manager.configs.controlnet import ControlAdapterDefaultSettings
+from invokeai.backend.model_manager.configs.external_api import (
+ ExternalApiModelDefaultSettings,
+ ExternalModelCapabilities,
+)
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.configs.lora import LoraModelDefaultSettings
+from invokeai.backend.model_manager.configs.main import MainModelDefaultSettings
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ClipVariantType,
+ Flux2VariantType,
+ FluxVariantType,
+ ModelFormat,
+ ModelSourceType,
+ ModelType,
+ ModelVariantType,
+ Qwen3VariantType,
+ QwenImageVariantType,
+ SchedulerPredictionType,
+ ZImageVariantType,
+)
+
+
+class DuplicateModelException(Exception):
+ """Raised on an attempt to add a model with the same key twice."""
+
+
+class InvalidModelException(Exception):
+ """Raised when an invalid model is detected."""
+
+
+class UnknownModelException(Exception):
+ """Raised on an attempt to fetch or delete a model with a nonexistent key."""
+
+
+class ConfigFileVersionMismatchException(Exception):
+ """Raised on an attempt to open a config with an incompatible version."""
+
+
+class ModelRecordOrderBy(str, Enum):
+ """The order in which to return model summaries."""
+
+ Default = "default" # order by type, base, format and name
+ Type = "type"
+ Base = "base"
+ Name = "name"
+ Format = "format"
+ Size = "size"
+ DateAdded = "created_at"
+ DateModified = "updated_at"
+ Path = "path"
+
+
+class ModelSummary(BaseModel):
+ """A short summary of models for UI listing purposes."""
+
+ key: str = Field(description="model key")
+ type: ModelType = Field(description="model type")
+ base: BaseModelType = Field(description="base model")
+ format: ModelFormat = Field(description="model format")
+ name: str = Field(description="model name")
+ description: str = Field(description="short description of model")
+ tags: Set[str] = Field(description="tags associated with model")
+
+
+class ModelRecordChanges(BaseModelExcludeNull):
+ """A set of changes to apply to a model."""
+
+ # Changes applicable to all models
+ source: Optional[str] = Field(description="original source of the model", default=None)
+ source_type: Optional[ModelSourceType] = Field(description="type of model source", default=None)
+ source_api_response: Optional[str] = Field(description="metadata from remote source", default=None)
+ source_url: Optional[str] = Field(description="Optional URL for the model (e.g. download page)", default=None)
+
+ @field_validator("source_url", mode="before")
+ @classmethod
+ def validate_source_url(cls, v: Any) -> Optional[str]:
+ if v is None or v == "":
+ return None
+ if not isinstance(v, str):
+ raise ValueError("source_url must be a string")
+ if not v.startswith(("https://", "http://")):
+ raise ValueError("source_url must be an http or https URL")
+ return v
+
+ name: Optional[str] = Field(description="Name of the model.", default=None)
+ path: Optional[str] = Field(description="Path to the model.", default=None)
+ description: Optional[str] = Field(description="Model description", default=None)
+ base: Optional[BaseModelType] = Field(description="The base model.", default=None)
+ type: Optional[ModelType] = Field(description="Type of model", default=None)
+ key: Optional[str] = Field(description="Database ID for this model", default=None)
+ hash: Optional[str] = Field(description="hash of model file", default=None)
+ file_size: Optional[int] = Field(description="Size of model file", default=None)
+ format: Optional[str] = Field(description="format of model file", default=None)
+ trigger_phrases: Optional[set[str]] = Field(description="Set of trigger phrases for this model", default=None)
+ default_settings: Optional[
+ MainModelDefaultSettings
+ | LoraModelDefaultSettings
+ | ControlAdapterDefaultSettings
+ | ExternalApiModelDefaultSettings
+ ] = Field(description="Default settings for this model", default=None)
+
+ # External API model changes
+ provider_id: Optional[str] = Field(description="External provider identifier", default=None)
+ provider_model_id: Optional[str] = Field(description="External provider model identifier", default=None)
+ capabilities: Optional[ExternalModelCapabilities] = Field(
+ description="External model capabilities",
+ default=None,
+ )
+ cpu_only: Optional[bool] = Field(description="Whether this model should run on CPU only", default=None)
+
+ # Checkpoint-specific changes
+ # TODO(MM2): Should we expose these? Feels footgun-y...
+ variant: Optional[
+ ModelVariantType
+ | ClipVariantType
+ | FluxVariantType
+ | Flux2VariantType
+ | ZImageVariantType
+ | QwenImageVariantType
+ | Qwen3VariantType
+ ] = Field(description="The variant of the model.", default=None)
+ prediction_type: Optional[SchedulerPredictionType] = Field(
+ description="The prediction type of the model.", default=None
+ )
+ upcast_attention: Optional[bool] = Field(description="Whether to upcast attention.", default=None)
+ config_path: Optional[str] = Field(description="Path to config file for model", default=None)
+
+
+class ModelRecordServiceBase(ABC):
+ """Abstract base class for storage and retrieval of model configs."""
+
+ @abstractmethod
+ def add_model(self, config: AnyModelConfig) -> AnyModelConfig:
+ """
+ Add a model to the database.
+
+ :param key: Unique key for the model
+ :param config: Model configuration record, either a dict with the
+ required fields or a ModelConfigBase instance.
+
+ Can raise DuplicateModelException and InvalidModelConfigException exceptions.
+ """
+ pass
+
+ @abstractmethod
+ def del_model(self, key: str) -> None:
+ """
+ Delete a model.
+
+ :param key: Unique key for the model to be deleted
+
+ Can raise an UnknownModelException
+ """
+ pass
+
+ @abstractmethod
+ def update_model(self, key: str, changes: ModelRecordChanges, allow_class_change: bool = False) -> AnyModelConfig:
+ """
+ Update the model, returning the updated version.
+
+ :param key: Unique key for the model to be updated.
+ :param changes: A set of changes to apply to this model. Changes are validated before being written.
+ :param allow_class_change: If True, allows changes that would change the model config class. For example,
+ changing a LoRA into a Main model. This does not disable validation, so the changes must still be valid.
+ """
+ pass
+
+ @abstractmethod
+ def replace_model(self, key: str, new_config: AnyModelConfig) -> AnyModelConfig:
+ """
+ Replace the model record entirely, returning the new record.
+
+ This is used when we re-identify a model and have a new config object.
+
+ :param key: Unique key for the model to be updated.
+ :param new_config: The new model config to write.
+ """
+ pass
+
+ @abstractmethod
+ def get_model(self, key: str) -> AnyModelConfig:
+ """
+ Retrieve the configuration for the indicated model.
+
+ :param key: Key of model config to be fetched.
+
+ Exceptions: UnknownModelException
+ """
+ pass
+
+ @abstractmethod
+ def get_model_by_hash(self, hash: str) -> AnyModelConfig:
+ """
+ Retrieve the configuration for the indicated model.
+
+ :param hash: Hash of model config to be fetched.
+
+ Exceptions: UnknownModelException
+ """
+ pass
+
+ @abstractmethod
+ def list_models(
+ self,
+ page: int = 0,
+ per_page: int = 10,
+ order_by: ModelRecordOrderBy = ModelRecordOrderBy.Default,
+ direction: SQLiteDirection = SQLiteDirection.Ascending,
+ ) -> PaginatedResults[ModelSummary]:
+ """Return a paginated summary listing of each model in the database."""
+ pass
+
+ @abstractmethod
+ def exists(self, key: str) -> bool:
+ """
+ Return True if a model with the indicated key exists in the database.
+
+ :param key: Unique key for the model to be deleted
+ """
+ pass
+
+ @abstractmethod
+ def search_by_path(
+ self,
+ path: Union[str, Path],
+ ) -> List[AnyModelConfig]:
+ """Return the model(s) having the indicated path."""
+ pass
+
+ @abstractmethod
+ def search_by_hash(
+ self,
+ hash: str,
+ ) -> List[AnyModelConfig]:
+ """Return the model(s) having the indicated original hash."""
+ pass
+
+ @abstractmethod
+ def search_by_attr(
+ self,
+ model_name: Optional[str] = None,
+ base_model: Optional[BaseModelType] = None,
+ model_type: Optional[ModelType] = None,
+ model_format: Optional[ModelFormat] = None,
+ order_by: ModelRecordOrderBy = ModelRecordOrderBy.Default,
+ direction: SQLiteDirection = SQLiteDirection.Ascending,
+ ) -> List[AnyModelConfig]:
+ """
+ Return models matching name, base and/or type.
+
+ :param model_name: Filter by name of model (optional)
+ :param base_model: Filter by base model (optional)
+ :param model_type: Filter by type of model (optional)
+ :param model_format: Filter by model format (e.g. "diffusers") (optional)
+
+ If none of the optional filters are passed, will return all
+ models in the database.
+ """
+ pass
+
+ def all_models(self) -> List[AnyModelConfig]:
+ """Return all the model configs in the database."""
+ return self.search_by_attr()
+
+ def model_info_by_name(self, model_name: str, base_model: BaseModelType, model_type: ModelType) -> AnyModelConfig:
+ """
+ Return information about a single model using its name, base type and model type.
+
+ If there are more than one model that match, raises a DuplicateModelException.
+ If no model matches, raises an UnknownModelException
+ """
+ model_configs = self.search_by_attr(model_name=model_name, base_model=base_model, model_type=model_type)
+ if len(model_configs) > 1:
+ raise DuplicateModelException(
+ f"More than one model matched the search criteria: base_model='{base_model}', model_type='{model_type}', model_name='{model_name}'."
+ )
+ if len(model_configs) == 0:
+ raise UnknownModelException(
+ f"More than one model matched the search criteria: base_model='{base_model}', model_type='{model_type}', model_name='{model_name}'."
+ )
+ return model_configs[0]
diff --git a/invokeai/app/services/model_records/model_records_sql.py b/invokeai/app/services/model_records/model_records_sql.py
new file mode 100644
index 00000000000..3452ba44316
--- /dev/null
+++ b/invokeai/app/services/model_records/model_records_sql.py
@@ -0,0 +1,458 @@
+# Copyright (c) 2023 Lincoln D. Stein and the InvokeAI Development Team
+"""
+SQL Implementation of the ModelRecordServiceBase API
+
+Typical usage:
+
+ from invokeai.backend.model_manager import ModelConfigStoreSQL
+ store = ModelConfigStoreSQL(sqlite_db)
+ config = dict(
+ path='/tmp/pokemon.bin',
+ name='old name',
+ base_model='sd-1',
+ type='embedding',
+ format='embedding_file',
+ )
+
+ # adding - the key becomes the model's "key" field
+ store.add_model('key1', config)
+
+ # updating
+ config.name='new name'
+ store.update_model('key1', config)
+
+ # checking for existence
+ if store.exists('key1'):
+ print("yes")
+
+ # fetching config
+ new_config = store.get_model('key1')
+ print(new_config.name, new_config.base)
+ assert new_config.key == 'key1'
+
+ # deleting
+ store.del_model('key1')
+
+ # searching
+ configs = store.search_by_path(path='/tmp/pokemon.bin')
+ configs = store.search_by_hash('750a499f35e43b7e1b4d15c207aa2f01')
+ configs = store.search_by_attr(base_model='sd-2', model_type='main')
+"""
+
+import json
+import logging
+import sqlite3
+from math import ceil
+from pathlib import Path
+from typing import List, Optional, Union
+
+import pydantic
+from pydantic import ValidationError
+
+from invokeai.app.services.model_records.model_records_base import (
+ DuplicateModelException,
+ ModelRecordChanges,
+ ModelRecordOrderBy,
+ ModelRecordServiceBase,
+ ModelSummary,
+ UnknownModelException,
+)
+from invokeai.app.services.shared.pagination import PaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.backend.model_manager.configs.base import Config_Base
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig, ModelConfigFactory
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
+
+
+def _construct_config_for_type(fields: dict, target_type: ModelType) -> AnyModelConfig:
+ """Try every config class whose `type` default matches `target_type` and return the first that validates.
+
+ Used when changing a model's type via the update endpoint: the existing record's `format`/`variant`
+ fields belong to the old class and may not have a discriminator match in the new type space, so we
+ fall back to constructing each candidate class directly with whatever fields it accepts.
+ """
+ last_error: Exception | None = None
+ for candidate_class in Config_Base.CONFIG_CLASSES:
+ type_field = candidate_class.model_fields.get("type")
+ if type_field is None or type_field.default != target_type:
+ continue
+ try:
+ return candidate_class(**fields) # type: ignore[return-value]
+ except ValidationError as e:
+ last_error = e
+ if last_error is not None:
+ raise last_error
+ raise ValidationError.from_exception_data(
+ f"No model config class found for type={target_type!r}",
+ line_errors=[],
+ )
+
+
+class ModelRecordServiceSQL(ModelRecordServiceBase):
+ """Implementation of the ModelConfigStore ABC using a SQL database."""
+
+ def __init__(self, db: SqliteDatabase, logger: logging.Logger):
+ """
+ Initialize a new object from preexisting sqlite3 connection and threading lock objects.
+
+ :param db: Sqlite connection object
+ """
+ super().__init__()
+ self._db = db
+ self._logger = logger
+
+ def add_model(self, config: AnyModelConfig) -> AnyModelConfig:
+ """
+ Add a model to the database.
+
+ :param key: Unique key for the model
+ :param config: Model configuration record, either a dict with the
+ required fields or a ModelConfigBase instance.
+
+ Can raise DuplicateModelException and InvalidModelConfigException exceptions.
+ """
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """--sql
+ INSERT INTO models (
+ id,
+ config
+ )
+ VALUES (?,?);
+ """,
+ (
+ config.key,
+ config.model_dump_json(),
+ ),
+ )
+
+ except sqlite3.IntegrityError as e:
+ if "UNIQUE constraint failed" in str(e):
+ if "models.path" in str(e):
+ msg = f"A model with path '{config.path}' is already installed"
+ elif "models.name" in str(e):
+ msg = f"A model with name='{config.name}', type='{config.type}', base='{config.base}' is already installed"
+ else:
+ msg = f"A model with key '{config.key}' is already installed"
+ raise DuplicateModelException(msg) from e
+ else:
+ raise e
+
+ return self.get_model(config.key)
+
+ def del_model(self, key: str) -> None:
+ """
+ Delete a model.
+
+ :param key: Unique key for the model to be deleted
+
+ Can raise an UnknownModelException
+ """
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE FROM models
+ WHERE id=?;
+ """,
+ (key,),
+ )
+ if cursor.rowcount == 0:
+ raise UnknownModelException("model not found")
+
+ def update_model(self, key: str, changes: ModelRecordChanges, allow_class_change: bool = False) -> AnyModelConfig:
+ with self._db.transaction() as cursor:
+ record = self.get_model(key)
+
+ if allow_class_change:
+ # The changes may cause the model config class to change. To handle this, we need to construct the new
+ # class from scratch rather than trying to modify the existing instance in place.
+ #
+ # 1. Convert the existing record to a dict
+ # 2. Apply the changes to the dict
+ # 3. Attempt to create a new model config from the updated dict
+
+ # 1. Convert the existing record to a dict
+ record_as_dict = record.model_dump()
+
+ # 2. Apply the changes to the dict
+ for field_name in changes.model_fields_set:
+ record_as_dict[field_name] = getattr(changes, field_name)
+
+ # 3. Attempt to create a new model config from the updated dict.
+ #
+ # When the model type is being changed, the previous record's `format` and `variant` likely
+ # belong to the old config class and won't validate against the new one (e.g. switching a
+ # Qwen3 encoder to a Text LLM keeps format=qwen3_encoder, which has no matching discriminator
+ # under text_llm). If the initial validation fails and the type changed, retry with stale
+ # format/variant fields stripped so the new class can apply its own defaults.
+ type_changed = "type" in changes.model_fields_set and changes.type != record.type
+ try:
+ record = ModelConfigFactory.from_dict(record_as_dict)
+ except ValidationError:
+ if not type_changed:
+ raise
+ fallback_dict = dict(record_as_dict)
+ for stale_field in ("format", "variant"):
+ if stale_field not in changes.model_fields_set:
+ fallback_dict.pop(stale_field, None)
+ record = _construct_config_for_type(fallback_dict, changes.type)
+
+ # If we get this far, the updated model config is valid, so we can save it to the database.
+ json_serialized = record.model_dump_json()
+ else:
+ # We are not allowing the model config class to change, so we can just update the existing instance in
+ # place. If the changes are invalid for the existing class, an exception will be raised by pydantic.
+ for field_name in changes.model_fields_set:
+ setattr(record, field_name, getattr(changes, field_name))
+ json_serialized = record.model_dump_json()
+
+ cursor.execute(
+ """--sql
+ UPDATE models
+ SET
+ config=?
+ WHERE id=?;
+ """,
+ (json_serialized, key),
+ )
+ if cursor.rowcount == 0:
+ raise UnknownModelException("model not found")
+
+ return self.get_model(key)
+
+ def replace_model(self, key: str, new_config: AnyModelConfig) -> AnyModelConfig:
+ if key != new_config.key:
+ raise ValueError("key does not match new_config.key")
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ UPDATE models
+ SET
+ config=?
+ WHERE id=?;
+ """,
+ (new_config.model_dump_json(), key),
+ )
+ if cursor.rowcount == 0:
+ raise UnknownModelException("model not found")
+ return self.get_model(key)
+
+ def get_model(self, key: str) -> AnyModelConfig:
+ """
+ Retrieve the ModelConfigBase instance for the indicated model.
+
+ :param key: Key of model config to be fetched.
+
+ Exceptions: UnknownModelException
+ """
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT config FROM models
+ WHERE id=?;
+ """,
+ (key,),
+ )
+ rows = cursor.fetchone()
+ if not rows:
+ raise UnknownModelException("model not found")
+ model = ModelConfigFactory.from_dict(json.loads(rows[0]))
+ return model
+
+ def get_model_by_hash(self, hash: str) -> AnyModelConfig:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT config FROM models
+ WHERE hash=?;
+ """,
+ (hash,),
+ )
+ rows = cursor.fetchone()
+ if not rows:
+ raise UnknownModelException("model not found")
+ model = ModelConfigFactory.from_dict(json.loads(rows[0]))
+ return model
+
+ def exists(self, key: str) -> bool:
+ """
+ Return True if a model with the indicated key exists in the databse.
+
+ :param key: Unique key for the model to be deleted
+ """
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ select count(*) FROM models
+ WHERE id=?;
+ """,
+ (key,),
+ )
+ count = cursor.fetchone()[0]
+ return count > 0
+
+ def search_by_attr(
+ self,
+ model_name: Optional[str] = None,
+ base_model: Optional[BaseModelType] = None,
+ model_type: Optional[ModelType] = None,
+ model_format: Optional[ModelFormat] = None,
+ order_by: ModelRecordOrderBy = ModelRecordOrderBy.Default,
+ direction: SQLiteDirection = SQLiteDirection.Ascending,
+ ) -> List[AnyModelConfig]:
+ """
+ Return models matching name, base and/or type.
+
+ :param model_name: Filter by name of model (optional)
+ :param base_model: Filter by base model (optional)
+ :param model_type: Filter by type of model (optional)
+ :param model_format: Filter by model format (e.g. "diffusers") (optional)
+ :param order_by: Result order
+ :param direction: Result direction
+
+ If none of the optional filters are passed, will return all
+ models in the database.
+ """
+ with self._db.transaction() as cursor:
+ assert isinstance(order_by, ModelRecordOrderBy)
+ order_dir = "DESC" if direction == SQLiteDirection.Descending else "ASC"
+ ordering = {
+ ModelRecordOrderBy.Default: f"type {order_dir}, base COLLATE NOCASE {order_dir}, name COLLATE NOCASE {order_dir}, format",
+ ModelRecordOrderBy.Type: "type",
+ ModelRecordOrderBy.Base: "base COLLATE NOCASE",
+ ModelRecordOrderBy.Name: "name COLLATE NOCASE",
+ ModelRecordOrderBy.Format: "format",
+ ModelRecordOrderBy.Size: "IFNULL(json_extract(config, '$.file_size'), 0)",
+ ModelRecordOrderBy.DateAdded: "created_at",
+ ModelRecordOrderBy.DateModified: "updated_at",
+ ModelRecordOrderBy.Path: "path",
+ }
+
+ where_clause: list[str] = []
+ bindings: list[str] = []
+ if model_name:
+ where_clause.append("name=?")
+ bindings.append(model_name)
+ if base_model:
+ where_clause.append("base=?")
+ bindings.append(base_model)
+ if model_type:
+ where_clause.append("type=?")
+ bindings.append(model_type)
+ if model_format:
+ where_clause.append("format=?")
+ bindings.append(model_format)
+ where = f"WHERE {' AND '.join(where_clause)}" if where_clause else ""
+
+ cursor.execute(
+ f"""--sql
+ SELECT config
+ FROM models
+ {where}
+ ORDER BY {ordering[order_by]} {order_dir} -- using ? to bind doesn't work here for some reason;
+ """,
+ tuple(bindings),
+ )
+ result = cursor.fetchall()
+
+ # Parse the model configs.
+ results: list[AnyModelConfig] = []
+ for row in result:
+ try:
+ model_config = ModelConfigFactory.from_dict(json.loads(row[0]))
+ except pydantic.ValidationError as e:
+ # We catch this error so that the app can still run if there are invalid model configs in the database.
+ # One reason that an invalid model config might be in the database is if someone had to rollback from a
+ # newer version of the app that added a new model type.
+ row_data = f"{row[0][:64]}..." if len(row[0]) > 64 else row[0]
+ try:
+ name = json.loads(row[0]).get("name", "")
+ except Exception:
+ name = ""
+ self._logger.warning(
+ f"Skipping invalid model config in the database with name {name}. Ignoring this model. ({row_data})"
+ )
+ self._logger.warning(f"Validation error: {e}")
+ else:
+ results.append(model_config)
+
+ return results
+
+ def search_by_path(self, path: Union[str, Path]) -> List[AnyModelConfig]:
+ """Return models with the indicated path."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT config FROM models
+ WHERE path=?;
+ """,
+ (str(path),),
+ )
+ results = [ModelConfigFactory.from_dict(json.loads(x[0])) for x in cursor.fetchall()]
+ return results
+
+ def search_by_hash(self, hash: str) -> List[AnyModelConfig]:
+ """Return models with the indicated hash."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT config FROM models
+ WHERE hash=?;
+ """,
+ (hash,),
+ )
+ results = [ModelConfigFactory.from_dict(json.loads(x[0])) for x in cursor.fetchall()]
+ return results
+
+ def list_models(
+ self,
+ page: int = 0,
+ per_page: int = 10,
+ order_by: ModelRecordOrderBy = ModelRecordOrderBy.Default,
+ direction: SQLiteDirection = SQLiteDirection.Ascending,
+ ) -> PaginatedResults[ModelSummary]:
+ """Return a paginated summary listing of each model in the database."""
+ with self._db.transaction() as cursor:
+ assert isinstance(order_by, ModelRecordOrderBy)
+ order_dir = "DESC" if direction == SQLiteDirection.Descending else "ASC"
+ ordering = {
+ ModelRecordOrderBy.Default: f"type {order_dir}, base COLLATE NOCASE {order_dir}, name COLLATE NOCASE {order_dir}, format",
+ ModelRecordOrderBy.Type: "type",
+ ModelRecordOrderBy.Base: "base COLLATE NOCASE",
+ ModelRecordOrderBy.Name: "name COLLATE NOCASE",
+ ModelRecordOrderBy.Format: "format",
+ ModelRecordOrderBy.Size: "IFNULL(json_extract(config, '$.file_size'), 0)",
+ ModelRecordOrderBy.DateAdded: "created_at",
+ ModelRecordOrderBy.DateModified: "updated_at",
+ ModelRecordOrderBy.Path: "path",
+ }
+
+ # Lock so that the database isn't updated while we're doing the two queries.
+ # query1: get the total number of model configs
+ cursor.execute(
+ """--sql
+ select count(*) from models;
+ """,
+ (),
+ )
+ total = int(cursor.fetchone()[0])
+
+ # query2: fetch key fields
+ cursor.execute(
+ f"""--sql
+ SELECT config
+ FROM models
+ ORDER BY {ordering[order_by]} {order_dir} -- using ? to bind doesn't work here for some reason
+ LIMIT ?
+ OFFSET ?;
+ """,
+ (
+ per_page,
+ page * per_page,
+ ),
+ )
+ rows = cursor.fetchall()
+ items = [ModelSummary.model_validate(dict(x)) for x in rows]
+ return PaginatedResults(page=page, pages=ceil(total / per_page), per_page=per_page, total=total, items=items)
diff --git a/invokeai/app/services/model_relationship_records/model_relationship_records_base.py b/invokeai/app/services/model_relationship_records/model_relationship_records_base.py
new file mode 100644
index 00000000000..94fa179bc0a
--- /dev/null
+++ b/invokeai/app/services/model_relationship_records/model_relationship_records_base.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+
+
+class ModelRelationshipRecordStorageBase(ABC):
+ """Abstract base class for model-to-model relationship record storage."""
+
+ @abstractmethod
+ def add_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ """Creates a relationship between two models by keys."""
+ pass
+
+ @abstractmethod
+ def remove_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ """Removes a relationship between two models by keys."""
+ pass
+
+ @abstractmethod
+ def get_related_model_keys(self, model_key: str) -> list[str]:
+ """Gets all models keys related to a given model key."""
+ pass
+
+ @abstractmethod
+ def get_related_model_keys_batch(self, model_keys: list[str]) -> list[str]:
+ """Get related model keys for multiple models given a list of keys."""
+ pass
diff --git a/invokeai/app/services/model_relationship_records/model_relationship_records_sqlite.py b/invokeai/app/services/model_relationship_records/model_relationship_records_sqlite.py
new file mode 100644
index 00000000000..c12990b8c3a
--- /dev/null
+++ b/invokeai/app/services/model_relationship_records/model_relationship_records_sqlite.py
@@ -0,0 +1,55 @@
+from invokeai.app.services.model_relationship_records.model_relationship_records_base import (
+ ModelRelationshipRecordStorageBase,
+)
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class SqliteModelRelationshipRecordStorage(ModelRelationshipRecordStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def add_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ with self._db.transaction() as cursor:
+ if model_key_1 == model_key_2:
+ raise ValueError("Cannot relate a model to itself.")
+ a, b = sorted([model_key_1, model_key_2])
+ cursor.execute(
+ "INSERT OR IGNORE INTO model_relationships (model_key_1, model_key_2) VALUES (?, ?)",
+ (a, b),
+ )
+
+ def remove_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ with self._db.transaction() as cursor:
+ a, b = sorted([model_key_1, model_key_2])
+ cursor.execute(
+ "DELETE FROM model_relationships WHERE model_key_1 = ? AND model_key_2 = ?",
+ (a, b),
+ )
+
+ def get_related_model_keys(self, model_key: str) -> list[str]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT model_key_2 FROM model_relationships WHERE model_key_1 = ?
+ UNION
+ SELECT model_key_1 FROM model_relationships WHERE model_key_2 = ?
+ """,
+ (model_key, model_key),
+ )
+ result = [row[0] for row in cursor.fetchall()]
+ return result
+
+ def get_related_model_keys_batch(self, model_keys: list[str]) -> list[str]:
+ with self._db.transaction() as cursor:
+ key_list = ",".join("?" for _ in model_keys)
+ cursor.execute(
+ f"""
+ SELECT model_key_2 FROM model_relationships WHERE model_key_1 IN ({key_list})
+ UNION
+ SELECT model_key_1 FROM model_relationships WHERE model_key_2 IN ({key_list})
+ """,
+ model_keys + model_keys,
+ )
+ result = [row[0] for row in cursor.fetchall()]
+ return result
diff --git a/invokeai/app/services/model_relationships/model_relationships_base.py b/invokeai/app/services/model_relationships/model_relationships_base.py
new file mode 100644
index 00000000000..1ea744a8dcb
--- /dev/null
+++ b/invokeai/app/services/model_relationships/model_relationships_base.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+
+
+class ModelRelationshipsServiceABC(ABC):
+ """High-level service for managing model-to-model relationships."""
+
+ @abstractmethod
+ def add_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ """Creates a relationship between two models keys."""
+ pass
+
+ @abstractmethod
+ def remove_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ """Removes a relationship between two models keys."""
+ pass
+
+ @abstractmethod
+ def get_related_model_keys(self, model_key: str) -> list[str]:
+ """Gets all models keys related to a given model key."""
+ pass
+
+ @abstractmethod
+ def get_related_model_keys_batch(self, model_keys: list[str]) -> list[str]:
+ """Get related model keys for multiple models."""
+ pass
diff --git a/invokeai/app/services/model_relationships/model_relationships_common.py b/invokeai/app/services/model_relationships/model_relationships_common.py
new file mode 100644
index 00000000000..5876be6b0b6
--- /dev/null
+++ b/invokeai/app/services/model_relationships/model_relationships_common.py
@@ -0,0 +1,9 @@
+from datetime import datetime
+
+from invokeai.app.util.model_exclude_null import BaseModelExcludeNull
+
+
+class ModelRelationship(BaseModelExcludeNull):
+ model_key_1: str
+ model_key_2: str
+ created_at: datetime
diff --git a/invokeai/app/services/model_relationships/model_relationships_default.py b/invokeai/app/services/model_relationships/model_relationships_default.py
new file mode 100644
index 00000000000..e4da482ff27
--- /dev/null
+++ b/invokeai/app/services/model_relationships/model_relationships_default.py
@@ -0,0 +1,31 @@
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.model_relationships.model_relationships_base import ModelRelationshipsServiceABC
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+
+
+class ModelRelationshipsService(ModelRelationshipsServiceABC):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+
+ def add_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ self.__invoker.services.model_relationship_records.add_model_relationship(model_key_1, model_key_2)
+
+ def remove_model_relationship(self, model_key_1: str, model_key_2: str) -> None:
+ self.__invoker.services.model_relationship_records.remove_model_relationship(model_key_1, model_key_2)
+
+ def get_related_model_keys(self, model_key: str) -> list[str]:
+ return self.__invoker.services.model_relationship_records.get_related_model_keys(model_key)
+
+ def add_relationship_from_models(self, model_1: AnyModelConfig, model_2: AnyModelConfig) -> None:
+ self.add_model_relationship(model_1.key, model_2.key)
+
+ def remove_relationship_from_models(self, model_1: AnyModelConfig, model_2: AnyModelConfig) -> None:
+ self.remove_model_relationship(model_1.key, model_2.key)
+
+ def get_related_keys_from_model(self, model: AnyModelConfig) -> list[str]:
+ return self.get_related_model_keys(model.key)
+
+ def get_related_model_keys_batch(self, model_keys: list[str]) -> list[str]:
+ return self.__invoker.services.model_relationship_records.get_related_model_keys_batch(model_keys)
diff --git a/invokeai/app/services/names/__init__.py b/invokeai/app/services/names/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/names/names_base.py b/invokeai/app/services/names/names_base.py
new file mode 100644
index 00000000000..f892c43c55a
--- /dev/null
+++ b/invokeai/app/services/names/names_base.py
@@ -0,0 +1,11 @@
+from abc import ABC, abstractmethod
+
+
+class NameServiceBase(ABC):
+ """Low-level service responsible for naming resources (images, latents, etc)."""
+
+ # TODO: Add customizable naming schemes
+ @abstractmethod
+ def create_image_name(self) -> str:
+ """Creates a name for an image."""
+ pass
diff --git a/invokeai/app/services/names/names_common.py b/invokeai/app/services/names/names_common.py
new file mode 100644
index 00000000000..7c69f8abe8d
--- /dev/null
+++ b/invokeai/app/services/names/names_common.py
@@ -0,0 +1,8 @@
+from enum import Enum, EnumMeta
+
+
+class ResourceType(str, Enum, metaclass=EnumMeta):
+ """Enum for resource types."""
+
+ IMAGE = "image"
+ LATENT = "latent"
diff --git a/invokeai/app/services/names/names_default.py b/invokeai/app/services/names/names_default.py
new file mode 100644
index 00000000000..5804a937d6a
--- /dev/null
+++ b/invokeai/app/services/names/names_default.py
@@ -0,0 +1,12 @@
+from invokeai.app.services.names.names_base import NameServiceBase
+from invokeai.app.util.misc import uuid_string
+
+
+class SimpleNameService(NameServiceBase):
+ """Creates image names from UUIDs."""
+
+ # TODO: Add customizable naming schemes
+ def create_image_name(self) -> str:
+ uuid_str = uuid_string()
+ filename = f"{uuid_str}.png"
+ return filename
diff --git a/invokeai/app/services/object_serializer/object_serializer_base.py b/invokeai/app/services/object_serializer/object_serializer_base.py
new file mode 100644
index 00000000000..ff19b4a039d
--- /dev/null
+++ b/invokeai/app/services/object_serializer/object_serializer_base.py
@@ -0,0 +1,44 @@
+from abc import ABC, abstractmethod
+from typing import Callable, Generic, TypeVar
+
+T = TypeVar("T")
+
+
+class ObjectSerializerBase(ABC, Generic[T]):
+ """Saves and loads arbitrary python objects."""
+
+ def __init__(self) -> None:
+ self._on_deleted_callbacks: list[Callable[[str], None]] = []
+
+ @abstractmethod
+ def load(self, name: str) -> T:
+ """
+ Loads the object.
+ :param name: The name of the object to load.
+ :raises ObjectNotFoundError: if the object is not found
+ """
+ pass
+
+ @abstractmethod
+ def save(self, obj: T) -> str:
+ """
+ Saves the object, returning its name.
+ :param obj: The object to save.
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, name: str) -> None:
+ """
+ Deletes the object, if it exists.
+ :param name: The name of the object to delete.
+ """
+ pass
+
+ def on_deleted(self, on_deleted: Callable[[str], None]) -> None:
+ """Register a callback for when an object is deleted"""
+ self._on_deleted_callbacks.append(on_deleted)
+
+ def _on_deleted(self, name: str) -> None:
+ for callback in self._on_deleted_callbacks:
+ callback(name)
diff --git a/invokeai/app/services/object_serializer/object_serializer_common.py b/invokeai/app/services/object_serializer/object_serializer_common.py
new file mode 100644
index 00000000000..7057386541f
--- /dev/null
+++ b/invokeai/app/services/object_serializer/object_serializer_common.py
@@ -0,0 +1,5 @@
+class ObjectNotFoundError(KeyError):
+ """Raised when an object is not found while loading"""
+
+ def __init__(self, name: str) -> None:
+ super().__init__(f"Object with name {name} not found")
diff --git a/invokeai/app/services/object_serializer/object_serializer_disk.py b/invokeai/app/services/object_serializer/object_serializer_disk.py
new file mode 100644
index 00000000000..bbd3f785507
--- /dev/null
+++ b/invokeai/app/services/object_serializer/object_serializer_disk.py
@@ -0,0 +1,93 @@
+import shutil
+import tempfile
+import typing
+from pathlib import Path
+from typing import TYPE_CHECKING, Optional, TypeVar
+
+import torch
+
+from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase
+from invokeai.app.services.object_serializer.object_serializer_common import ObjectNotFoundError
+from invokeai.app.util.misc import uuid_string
+
+if TYPE_CHECKING:
+ from invokeai.app.services.invoker import Invoker
+
+
+T = TypeVar("T")
+
+
+class ObjectSerializerDisk(ObjectSerializerBase[T]):
+ """Disk-backed storage for arbitrary python objects. Serialization is handled by `torch.save` and `torch.load`.
+
+ :param output_dir: The folder where the serialized objects will be stored
+ :param safe_globals: A list of types to be added to the safe globals for torch serialization
+ :param ephemeral: If True, objects will be stored in a temporary directory inside the given output_dir and cleaned up on exit
+ """
+
+ def __init__(
+ self,
+ output_dir: Path,
+ safe_globals: list[type],
+ ephemeral: bool = False,
+ ) -> None:
+ super().__init__()
+ self._ephemeral = ephemeral
+ self._base_output_dir = output_dir
+ self._base_output_dir.mkdir(parents=True, exist_ok=True)
+
+ if self._ephemeral:
+ # Remove dangling tempdirs that might have been left over from an earlier unplanned shutdown.
+ for temp_dir in filter(Path.is_dir, self._base_output_dir.glob("tmp*")):
+ shutil.rmtree(temp_dir)
+
+ # Must specify `ignore_cleanup_errors` to avoid fatal errors during cleanup on Windows
+ self._tempdir = (
+ tempfile.TemporaryDirectory(dir=self._base_output_dir, ignore_cleanup_errors=True) if ephemeral else None
+ )
+ self._output_dir = Path(self._tempdir.name) if self._tempdir else self._base_output_dir
+ self.__obj_class_name: Optional[str] = None
+
+ torch.serialization.add_safe_globals(safe_globals) if safe_globals else None
+
+ def load(self, name: str) -> T:
+ file_path = self._get_path(name)
+ try:
+ return torch.load(file_path) # pyright: ignore [reportUnknownMemberType]
+ except FileNotFoundError as e:
+ raise ObjectNotFoundError(name) from e
+
+ def save(self, obj: T) -> str:
+ name = self._new_name()
+ file_path = self._get_path(name)
+ torch.save(obj, file_path) # pyright: ignore [reportUnknownMemberType]
+ return name
+
+ def delete(self, name: str) -> None:
+ file_path = self._get_path(name)
+ file_path.unlink()
+
+ @property
+ def _obj_class_name(self) -> str:
+ if not self.__obj_class_name:
+ # `__orig_class__` is not available in the constructor for some technical, undoubtedly very pythonic reason
+ self.__obj_class_name = typing.get_args(self.__orig_class__)[0].__name__ # pyright: ignore [reportUnknownMemberType, reportAttributeAccessIssue]
+ return self.__obj_class_name
+
+ def _get_path(self, name: str) -> Path:
+ return self._output_dir / name
+
+ def _new_name(self) -> str:
+ return f"{self._obj_class_name}_{uuid_string()}"
+
+ def _tempdir_cleanup(self) -> None:
+ """Calls `cleanup` on the temporary directory, if it exists."""
+ if self._tempdir:
+ self._tempdir.cleanup()
+
+ def __del__(self) -> None:
+ # In case the service is not properly stopped, clean up the temporary directory when the class instance is GC'd.
+ self._tempdir_cleanup()
+
+ def stop(self, invoker: "Invoker") -> None:
+ self._tempdir_cleanup()
diff --git a/invokeai/app/services/object_serializer/object_serializer_forward_cache.py b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py
new file mode 100644
index 00000000000..b361259a4b1
--- /dev/null
+++ b/invokeai/app/services/object_serializer/object_serializer_forward_cache.py
@@ -0,0 +1,65 @@
+from queue import Queue
+from typing import TYPE_CHECKING, Optional, TypeVar
+
+from invokeai.app.services.object_serializer.object_serializer_base import ObjectSerializerBase
+
+T = TypeVar("T")
+
+if TYPE_CHECKING:
+ from invokeai.app.services.invoker import Invoker
+
+
+class ObjectSerializerForwardCache(ObjectSerializerBase[T]):
+ """
+ Provides a LRU cache for an instance of `ObjectSerializerBase`.
+ Saving an object to the cache always writes through to the underlying storage.
+ """
+
+ def __init__(self, underlying_storage: ObjectSerializerBase[T], max_cache_size: int = 20):
+ super().__init__()
+ self._underlying_storage = underlying_storage
+ self._cache: dict[str, T] = {}
+ self._cache_ids = Queue[str]()
+ self._max_cache_size = max_cache_size
+
+ def start(self, invoker: "Invoker") -> None:
+ self._invoker = invoker
+ start_op = getattr(self._underlying_storage, "start", None)
+ if callable(start_op):
+ start_op(invoker)
+
+ def stop(self, invoker: "Invoker") -> None:
+ self._invoker = invoker
+ stop_op = getattr(self._underlying_storage, "stop", None)
+ if callable(stop_op):
+ stop_op(invoker)
+
+ def load(self, name: str) -> T:
+ cache_item = self._get_cache(name)
+ if cache_item is not None:
+ return cache_item
+
+ obj = self._underlying_storage.load(name)
+ self._set_cache(name, obj)
+ return obj
+
+ def save(self, obj: T) -> str:
+ name = self._underlying_storage.save(obj)
+ self._set_cache(name, obj)
+ return name
+
+ def delete(self, name: str) -> None:
+ self._underlying_storage.delete(name)
+ if name in self._cache:
+ del self._cache[name]
+ self._on_deleted(name)
+
+ def _get_cache(self, name: str) -> Optional[T]:
+ return None if name not in self._cache else self._cache[name]
+
+ def _set_cache(self, name: str, data: T):
+ if name not in self._cache:
+ self._cache[name] = data
+ self._cache_ids.put(name)
+ if self._cache_ids.qsize() > self._max_cache_size:
+ self._cache.pop(self._cache_ids.get())
diff --git a/invokeai/app/services/orphaned_models/__init__.py b/invokeai/app/services/orphaned_models/__init__.py
new file mode 100644
index 00000000000..db9eaae7bb4
--- /dev/null
+++ b/invokeai/app/services/orphaned_models/__init__.py
@@ -0,0 +1,5 @@
+"""Service for finding and removing orphaned model files."""
+
+from invokeai.app.services.orphaned_models.orphaned_models_service import OrphanedModelInfo, OrphanedModelsService
+
+__all__ = ["OrphanedModelsService", "OrphanedModelInfo"]
diff --git a/invokeai/app/services/orphaned_models/orphaned_models_service.py b/invokeai/app/services/orphaned_models/orphaned_models_service.py
new file mode 100644
index 00000000000..8d2894c8671
--- /dev/null
+++ b/invokeai/app/services/orphaned_models/orphaned_models_service.py
@@ -0,0 +1,209 @@
+"""Service for finding and removing orphaned model files.
+
+Orphaned models are files in the models directory that are not referenced
+in the database models table.
+"""
+
+import json
+import shutil
+from pathlib import Path
+from typing import Set
+
+from pydantic import BaseModel, Field
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class OrphanedModelInfo(BaseModel):
+ """Information about an orphaned model directory."""
+
+ path: str = Field(description="Relative path to the orphaned directory from models root")
+ absolute_path: str = Field(description="Absolute path to the orphaned directory")
+ files: list[str] = Field(description="List of model files in this directory")
+ size_bytes: int = Field(description="Total size of all files in bytes")
+
+
+class OrphanedModelsService:
+ """Service for finding and removing orphaned model files."""
+
+ # Common model file extensions
+ MODEL_EXTENSIONS = {
+ ".safetensors",
+ ".ckpt",
+ ".pt",
+ ".pth",
+ ".bin",
+ ".onnx",
+ ".gguf",
+ }
+
+ # Directories to skip during scan
+ SKIP_DIRS = {
+ ".download_cache",
+ ".convert_cache",
+ "__pycache__",
+ ".git",
+ }
+
+ def __init__(self, config: InvokeAIAppConfig, db: SqliteDatabase):
+ """Initialize the service.
+
+ Args:
+ config: Application configuration containing models path
+ db: Database connection for querying registered models
+ """
+ self._config = config
+ self._db = db
+
+ def find_orphaned_models(self) -> list[OrphanedModelInfo]:
+ """Find all orphaned model directories.
+
+ Returns:
+ List of OrphanedModelInfo objects describing orphaned directories
+ """
+ models_path = self._config.models_path
+
+ # Get all model directories registered in the database
+ db_model_directories = self._get_registered_model_directories(models_path)
+
+ # Find all model files on disk
+ disk_model_files = self._get_all_model_files(models_path)
+
+ # Find orphaned files (files not under any registered model directory)
+ orphaned_files = set()
+ for disk_file in disk_model_files:
+ is_under_model_dir = False
+ for model_dir in db_model_directories:
+ try:
+ # Check if disk_file is under model_dir
+ disk_file.relative_to(model_dir)
+ is_under_model_dir = True
+ break
+ except ValueError:
+ # Not under this model directory, continue checking
+ continue
+
+ if not is_under_model_dir:
+ orphaned_files.add(disk_file)
+
+ # Group orphaned files by their top-level directory
+ orphaned_dirs_map: dict[Path, list[Path]] = {}
+ for orphaned_file in orphaned_files:
+ # Get the top-level directory relative to models_path
+ try:
+ rel_path = orphaned_file.relative_to(models_path)
+ if rel_path.parts:
+ top_level_dir = models_path / rel_path.parts[0]
+ if top_level_dir not in orphaned_dirs_map:
+ orphaned_dirs_map[top_level_dir] = []
+ orphaned_dirs_map[top_level_dir].append(orphaned_file)
+ except ValueError:
+ # File is outside models_path, skip it
+ continue
+
+ # Convert to OrphanedModelInfo objects
+ result = []
+ for dir_path, files in orphaned_dirs_map.items():
+ # Calculate total size
+ total_size = sum(f.stat().st_size for f in files if f.exists())
+
+ # Get relative file paths
+ file_names = [str(f.relative_to(dir_path)) for f in files]
+
+ result.append(
+ OrphanedModelInfo(
+ path=str(dir_path.relative_to(models_path)),
+ absolute_path=str(dir_path),
+ files=file_names,
+ size_bytes=total_size,
+ )
+ )
+
+ return result
+
+ def delete_orphaned_models(self, orphaned_paths: list[str]) -> dict[str, str]:
+ """Delete the specified orphaned model directories.
+
+ Args:
+ orphaned_paths: List of relative paths to delete (relative to models root)
+
+ Returns:
+ Dictionary mapping paths to status messages ("deleted" or error message)
+ """
+ models_path = self._config.models_path
+ results = {}
+
+ for rel_path in orphaned_paths:
+ try:
+ full_path = models_path / rel_path
+ if not full_path.exists():
+ results[rel_path] = "error: path does not exist"
+ continue
+
+ # Safety check: ensure path is under models directory
+ try:
+ full_path.relative_to(models_path)
+ except ValueError:
+ results[rel_path] = "error: path is not under models directory"
+ continue
+
+ # Delete the directory
+ shutil.rmtree(full_path)
+ results[rel_path] = "deleted"
+
+ except Exception as e:
+ results[rel_path] = f"error: {str(e)}"
+
+ return results
+
+ def _get_registered_model_directories(self, models_dir: Path) -> Set[Path]:
+ """Get the set of all model directories from the database."""
+ model_directories = set()
+
+ with self._db.transaction() as cursor:
+ cursor.execute("SELECT config FROM models")
+ rows = cursor.fetchall()
+
+ for row in rows:
+ try:
+ config = json.loads(row[0])
+ if "path" in config and config["path"]:
+ path_str = config["path"]
+ path = Path(path_str)
+
+ # If the path is relative, resolve it relative to models_dir
+ if not path.is_absolute():
+ full_path = (models_dir / path).resolve()
+ else:
+ full_path = path.resolve()
+
+ # Extract the top-level directory under models_dir
+ try:
+ rel_path = full_path.relative_to(models_dir)
+ if rel_path.parts:
+ top_level_dir = models_dir / rel_path.parts[0]
+ model_directories.add(top_level_dir.resolve())
+ except ValueError:
+ # Path is not relative to models_dir
+ model_directories.add(full_path)
+
+ except (json.JSONDecodeError, KeyError, TypeError):
+ # Skip invalid model configs
+ continue
+
+ return model_directories
+
+ def _get_all_model_files(self, models_path: Path) -> Set[Path]:
+ """Get all model files in the models directory."""
+ model_files = set()
+
+ for item in models_path.rglob("*"):
+ # Skip directories we don't want to scan
+ if any(skip_dir in item.parts for skip_dir in self.SKIP_DIRS):
+ continue
+
+ if item.is_file() and item.suffix.lower() in self.MODEL_EXTENSIONS:
+ model_files.add(item.resolve())
+
+ return model_files
diff --git a/invokeai/app/services/session_processor/__init__.py b/invokeai/app/services/session_processor/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/session_processor/session_processor_base.py b/invokeai/app/services/session_processor/session_processor_base.py
new file mode 100644
index 00000000000..15611bb5f87
--- /dev/null
+++ b/invokeai/app/services/session_processor/session_processor_base.py
@@ -0,0 +1,153 @@
+from abc import ABC, abstractmethod
+from threading import Event
+from typing import Optional, Protocol
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput
+from invokeai.app.services.invocation_services import InvocationServices
+from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
+from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem
+from invokeai.app.util.profiler import Profiler
+
+
+class SessionRunnerBase(ABC):
+ """
+ Base class for session runner.
+ """
+
+ @abstractmethod
+ def start(self, services: InvocationServices, cancel_event: Event, profiler: Optional[Profiler] = None) -> None:
+ """Starts the session runner.
+
+ Args:
+ services: The invocation services.
+ cancel_event: The cancel event.
+ profiler: The profiler to use for session profiling via cProfile. Omit to disable profiling. Basic session
+ stats will be still be recorded and logged when profiling is disabled.
+ """
+ pass
+
+ @abstractmethod
+ def run(self, queue_item: SessionQueueItem) -> None:
+ """Runs a session.
+
+ Args:
+ queue_item: The session to run.
+ """
+ pass
+
+ @abstractmethod
+ def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem) -> None:
+ """Run a single node in the graph.
+
+ Args:
+ invocation: The invocation to run.
+ queue_item: The session queue item.
+ """
+ pass
+
+
+class SessionProcessorBase(ABC):
+ """
+ Base class for session processor.
+
+ The session processor is responsible for executing sessions. It runs a simple polling loop,
+ checking the session queue for new sessions to execute. It must coordinate with the
+ invocation queue to ensure only one session is executing at a time.
+ """
+
+ @abstractmethod
+ def resume(self) -> SessionProcessorStatus:
+ """Starts or resumes the session processor"""
+ pass
+
+ @abstractmethod
+ def pause(self) -> SessionProcessorStatus:
+ """Pauses the session processor"""
+ pass
+
+ @abstractmethod
+ def get_status(self) -> SessionProcessorStatus:
+ """Gets the status of the session processor"""
+ pass
+
+
+class OnBeforeRunNode(Protocol):
+ def __call__(self, invocation: BaseInvocation, queue_item: SessionQueueItem) -> None:
+ """Callback to run before executing a node.
+
+ Args:
+ invocation: The invocation that will be executed.
+ queue_item: The session queue item.
+ """
+ ...
+
+
+class OnAfterRunNode(Protocol):
+ def __call__(self, invocation: BaseInvocation, queue_item: SessionQueueItem, output: BaseInvocationOutput) -> None:
+ """Callback to run before executing a node.
+
+ Args:
+ invocation: The invocation that was executed.
+ queue_item: The session queue item.
+ """
+ ...
+
+
+class OnNodeError(Protocol):
+ def __call__(
+ self,
+ invocation: BaseInvocation,
+ queue_item: SessionQueueItem,
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> None:
+ """Callback to run when a node has an error.
+
+ Args:
+ invocation: The invocation that errored.
+ queue_item: The session queue item.
+ error_type: The type of error, e.g. "ValueError".
+ error_message: The error message, e.g. "Invalid value".
+ error_traceback: The stringified error traceback.
+ """
+ ...
+
+
+class OnBeforeRunSession(Protocol):
+ def __call__(self, queue_item: SessionQueueItem) -> None:
+ """Callback to run before executing a session.
+
+ Args:
+ queue_item: The session queue item.
+ """
+ ...
+
+
+class OnAfterRunSession(Protocol):
+ def __call__(self, queue_item: SessionQueueItem) -> None:
+ """Callback to run after executing a session.
+
+ Args:
+ queue_item: The session queue item.
+ """
+ ...
+
+
+class OnNonFatalProcessorError(Protocol):
+ def __call__(
+ self,
+ queue_item: Optional[SessionQueueItem],
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> None:
+ """Callback to run when a non-fatal error occurs in the processor.
+
+ Args:
+ queue_item: The session queue item, if one was being executed when the error occurred.
+ error_type: The type of error, e.g. "ValueError".
+ error_message: The error message, e.g. "Invalid value".
+ error_traceback: The stringified error traceback.
+ """
+ ...
diff --git a/invokeai/app/services/session_processor/session_processor_common.py b/invokeai/app/services/session_processor/session_processor_common.py
new file mode 100644
index 00000000000..346f12d8bbc
--- /dev/null
+++ b/invokeai/app/services/session_processor/session_processor_common.py
@@ -0,0 +1,33 @@
+from PIL.Image import Image as PILImageType
+from pydantic import BaseModel, Field
+
+from invokeai.backend.util.util import image_to_dataURL
+
+
+class SessionProcessorStatus(BaseModel):
+ is_started: bool = Field(description="Whether the session processor is started")
+ is_processing: bool = Field(description="Whether a session is being processed")
+
+
+class CanceledException(Exception):
+ """Execution canceled by user."""
+
+ pass
+
+
+class ProgressImage(BaseModel):
+ """The progress image sent intermittently during processing"""
+
+ width: int = Field(ge=1, description="The effective width of the image in pixels")
+ height: int = Field(ge=1, description="The effective height of the image in pixels")
+ dataURL: str = Field(description="The image data as a b64 data URL")
+
+ @classmethod
+ def build(cls, image: PILImageType, size: tuple[int, int] | None = None) -> "ProgressImage":
+ """Build a ProgressImage from a PIL image"""
+
+ return cls(
+ width=size[0] if size else image.width,
+ height=size[1] if size else image.height,
+ dataURL=image_to_dataURL(image, image_format="JPEG"),
+ )
diff --git a/invokeai/app/services/session_processor/session_processor_default.py b/invokeai/app/services/session_processor/session_processor_default.py
new file mode 100644
index 00000000000..7159c19e746
--- /dev/null
+++ b/invokeai/app/services/session_processor/session_processor_default.py
@@ -0,0 +1,525 @@
+import gc
+import traceback
+from contextlib import suppress
+from threading import BoundedSemaphore, Thread
+from threading import Event as ThreadEvent
+from typing import Optional
+
+from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput
+from invokeai.app.services.events.events_common import (
+ BatchEnqueuedEvent,
+ FastAPIEvent,
+ QueueClearedEvent,
+ QueueItemStatusChangedEvent,
+ register_events,
+)
+from invokeai.app.services.invocation_stats.invocation_stats_common import GESStatsNotFoundError
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.session_processor.session_processor_base import (
+ InvocationServices,
+ OnAfterRunNode,
+ OnAfterRunSession,
+ OnBeforeRunNode,
+ OnBeforeRunSession,
+ OnNodeError,
+ OnNonFatalProcessorError,
+ SessionProcessorBase,
+ SessionRunnerBase,
+)
+from invokeai.app.services.session_processor.session_processor_common import CanceledException, SessionProcessorStatus
+from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem, SessionQueueItemNotFoundError
+from invokeai.app.services.shared.graph import NodeInputError
+from invokeai.app.services.shared.invocation_context import InvocationContextData, build_invocation_context
+from invokeai.app.util.profiler import Profiler
+
+
+class DefaultSessionRunner(SessionRunnerBase):
+ """Processes a single session's invocations."""
+
+ def __init__(
+ self,
+ on_before_run_session_callbacks: Optional[list[OnBeforeRunSession]] = None,
+ on_before_run_node_callbacks: Optional[list[OnBeforeRunNode]] = None,
+ on_after_run_node_callbacks: Optional[list[OnAfterRunNode]] = None,
+ on_node_error_callbacks: Optional[list[OnNodeError]] = None,
+ on_after_run_session_callbacks: Optional[list[OnAfterRunSession]] = None,
+ ):
+ """
+ Args:
+ on_before_run_session_callbacks: Callbacks to run before the session starts.
+ on_before_run_node_callbacks: Callbacks to run before each node starts.
+ on_after_run_node_callbacks: Callbacks to run after each node completes.
+ on_node_error_callbacks: Callbacks to run when a node errors.
+ on_after_run_session_callbacks: Callbacks to run after the session completes.
+ """
+
+ self._on_before_run_session_callbacks = on_before_run_session_callbacks or []
+ self._on_before_run_node_callbacks = on_before_run_node_callbacks or []
+ self._on_after_run_node_callbacks = on_after_run_node_callbacks or []
+ self._on_node_error_callbacks = on_node_error_callbacks or []
+ self._on_after_run_session_callbacks = on_after_run_session_callbacks or []
+
+ def start(self, services: InvocationServices, cancel_event: ThreadEvent, profiler: Optional[Profiler] = None):
+ self._services = services
+ self._cancel_event = cancel_event
+ self._profiler = profiler
+
+ def _is_canceled(self) -> bool:
+ """Check if the cancel event is set. This is also passed to the invocation context builder and called during
+ denoising to check if the session has been canceled."""
+ return self._cancel_event.is_set()
+
+ def run(self, queue_item: SessionQueueItem):
+ # Exceptions raised outside `run_node` are handled by the processor. There is no need to catch them here.
+
+ self._on_before_run_session(queue_item=queue_item)
+
+ # Loop over invocations until the session is complete or canceled
+ while True:
+ try:
+ invocation = queue_item.session.next()
+ # Anything other than a `NodeInputError` is handled as a processor error
+ except NodeInputError as e:
+ error_type = e.__class__.__name__
+ error_message = str(e)
+ error_traceback = traceback.format_exc()
+ self._on_node_error(
+ invocation=e.node,
+ queue_item=queue_item,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+ break
+
+ if invocation is None or self._is_canceled():
+ break
+
+ self.run_node(invocation, queue_item)
+
+ # The session is complete if all invocations have been run or there is an error on the session.
+ # At this time, the queue item may be canceled, but the object itself here won't be updated yet. We must
+ # use the cancel event to check if the session is canceled.
+ if (
+ queue_item.session.is_complete()
+ or self._is_canceled()
+ or queue_item.status in ["failed", "canceled", "completed"]
+ ):
+ break
+
+ self._on_after_run_session(queue_item=queue_item)
+
+ def run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem):
+ try:
+ # Any unhandled exception in this scope is an invocation error & will fail the graph
+ with self._services.performance_statistics.collect_stats(invocation, queue_item.session_id):
+ self._on_before_run_node(invocation, queue_item)
+
+ data = InvocationContextData(
+ invocation=invocation,
+ source_invocation_id=queue_item.session.prepared_source_mapping[invocation.id],
+ queue_item=queue_item,
+ )
+ context = build_invocation_context(
+ data=data,
+ services=self._services,
+ is_canceled=self._is_canceled,
+ )
+
+ # Invoke the node
+ output = invocation.invoke_internal(context=context, services=self._services)
+ # Save output and history
+ queue_item.session.complete(invocation.id, output)
+
+ self._on_after_run_node(invocation, queue_item, output)
+
+ except CanceledException:
+ # A CanceledException is raised during the denoising step callback if the cancel event is set. We don't need
+ # to do any handling here, and no error should be set - just pass and the cancellation will be handled
+ # correctly in the next iteration of the session runner loop.
+ #
+ # See the comment in the processor's `_on_queue_item_status_changed()` method for more details on how we
+ # handle cancellation.
+ pass
+ except Exception as e:
+ error_type = e.__class__.__name__
+ error_message = str(e)
+ error_traceback = traceback.format_exc()
+ self._on_node_error(
+ invocation=invocation,
+ queue_item=queue_item,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+
+ def _on_before_run_session(self, queue_item: SessionQueueItem) -> None:
+ """Called before a session is run.
+
+ - Start the profiler if profiling is enabled.
+ - Run any callbacks registered for this event.
+ """
+
+ self._services.logger.debug(
+ f"On before run session: queue item {queue_item.item_id}, session {queue_item.session_id}"
+ )
+
+ # If profiling is enabled, start the profiler
+ if self._profiler is not None:
+ self._profiler.start(profile_id=queue_item.session_id)
+
+ for callback in self._on_before_run_session_callbacks:
+ callback(queue_item=queue_item)
+
+ def _on_after_run_session(self, queue_item: SessionQueueItem) -> None:
+ """Called after a session is run.
+
+ - Stop the profiler if profiling is enabled.
+ - Update the queue item's session object in the database.
+ - If not already canceled or failed, complete the queue item.
+ - Log and reset performance statistics.
+ - Run any callbacks registered for this event.
+ """
+
+ self._services.logger.debug(
+ f"On after run session: queue item {queue_item.item_id}, session {queue_item.session_id}"
+ )
+
+ # If we are profiling, stop the profiler and dump the profile & stats
+ if self._profiler is not None:
+ profile_path = self._profiler.stop()
+ stats_path = profile_path.with_suffix(".json")
+ self._services.performance_statistics.dump_stats(
+ graph_execution_state_id=queue_item.session.id, output_path=stats_path
+ )
+
+ try:
+ # Update the queue item with the completed session. If the queue item has been removed from the queue,
+ # we'll get a SessionQueueItemNotFoundError and we can ignore it. This can happen if the queue is cleared
+ # while the session is running.
+ queue_item = self._services.session_queue.set_queue_item_session(queue_item.item_id, queue_item.session)
+
+ # The queue item may have been canceled or failed while the session was running. We should only complete it
+ # if it is not already canceled or failed.
+ if queue_item.status not in ["canceled", "failed"]:
+ queue_item = self._services.session_queue.complete_queue_item(queue_item.item_id)
+
+ # We'll get a GESStatsNotFoundError if we try to log stats for an untracked graph, but in the processor
+ # we don't care about that - suppress the error.
+ with suppress(GESStatsNotFoundError):
+ self._services.performance_statistics.log_stats(queue_item.session.id)
+ self._services.performance_statistics.reset_stats(queue_item.session.id)
+
+ for callback in self._on_after_run_session_callbacks:
+ callback(queue_item=queue_item)
+ except SessionQueueItemNotFoundError:
+ pass
+
+ def _on_before_run_node(self, invocation: BaseInvocation, queue_item: SessionQueueItem):
+ """Called before a node is run.
+
+ - Emits an invocation started event.
+ - Run any callbacks registered for this event.
+ """
+
+ self._services.logger.debug(
+ f"On before run node: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})"
+ )
+
+ # Send starting event
+ self._services.events.emit_invocation_started(queue_item=queue_item, invocation=invocation)
+
+ for callback in self._on_before_run_node_callbacks:
+ callback(invocation=invocation, queue_item=queue_item)
+
+ def _on_after_run_node(
+ self, invocation: BaseInvocation, queue_item: SessionQueueItem, output: BaseInvocationOutput
+ ):
+ """Called after a node is run.
+
+ - Emits an invocation complete event.
+ - Run any callbacks registered for this event.
+ """
+
+ self._services.logger.debug(
+ f"On after run node: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})"
+ )
+
+ # Send complete event on successful runs
+ self._services.events.emit_invocation_complete(invocation=invocation, queue_item=queue_item, output=output)
+
+ for callback in self._on_after_run_node_callbacks:
+ callback(invocation=invocation, queue_item=queue_item, output=output)
+
+ def _on_node_error(
+ self,
+ invocation: BaseInvocation,
+ queue_item: SessionQueueItem,
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ):
+ """Called when a node errors. Node errors may occur when running or preparing the node..
+
+ - Set the node error on the session object.
+ - Log the error.
+ - Fail the queue item.
+ - Emits an invocation error event.
+ - Run any callbacks registered for this event.
+ """
+
+ self._services.logger.debug(
+ f"On node error: queue item {queue_item.item_id}, session {queue_item.session_id}, node {invocation.id} ({invocation.get_type()})"
+ )
+
+ # Node errors do not get the full traceback. Only the queue item gets the full traceback.
+ node_error = f"{error_type}: {error_message}"
+ queue_item.session.set_node_error(invocation.id, node_error)
+ self._services.logger.error(
+ f"Error while invoking session {queue_item.session_id}, invocation {invocation.id} ({invocation.get_type()}): {error_message}"
+ )
+ self._services.logger.error(error_traceback)
+
+ # Fail the queue item
+ queue_item = self._services.session_queue.set_queue_item_session(queue_item.item_id, queue_item.session)
+ queue_item = self._services.session_queue.fail_queue_item(
+ queue_item.item_id, error_type, error_message, error_traceback
+ )
+
+ # Send error event
+ self._services.events.emit_invocation_error(
+ queue_item=queue_item,
+ invocation=invocation,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+
+ for callback in self._on_node_error_callbacks:
+ callback(
+ invocation=invocation,
+ queue_item=queue_item,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+
+
+class DefaultSessionProcessor(SessionProcessorBase):
+ def __init__(
+ self,
+ session_runner: Optional[SessionRunnerBase] = None,
+ on_non_fatal_processor_error_callbacks: Optional[list[OnNonFatalProcessorError]] = None,
+ thread_limit: int = 1,
+ polling_interval: int = 1,
+ ) -> None:
+ super().__init__()
+
+ self.session_runner = session_runner if session_runner else DefaultSessionRunner()
+ self._on_non_fatal_processor_error_callbacks = on_non_fatal_processor_error_callbacks or []
+ self._thread_limit = thread_limit
+ self._polling_interval = polling_interval
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker: Invoker = invoker
+ self._queue_item: Optional[SessionQueueItem] = None
+ self._invocation: Optional[BaseInvocation] = None
+
+ self._resume_event = ThreadEvent()
+ self._stop_event = ThreadEvent()
+ self._poll_now_event = ThreadEvent()
+ self._cancel_event = ThreadEvent()
+
+ register_events(QueueClearedEvent, self._on_queue_cleared)
+ register_events(BatchEnqueuedEvent, self._on_batch_enqueued)
+ register_events(QueueItemStatusChangedEvent, self._on_queue_item_status_changed)
+
+ self._thread_semaphore = BoundedSemaphore(self._thread_limit)
+
+ # If profiling is enabled, create a profiler. The same profiler will be used for all sessions. Internally,
+ # the profiler will create a new profile for each session.
+ self._profiler = (
+ Profiler(
+ logger=self._invoker.services.logger,
+ output_dir=self._invoker.services.configuration.profiles_path,
+ prefix=self._invoker.services.configuration.profile_prefix,
+ )
+ if self._invoker.services.configuration.profile_graphs
+ else None
+ )
+
+ self.session_runner.start(services=invoker.services, cancel_event=self._cancel_event, profiler=self._profiler)
+ self._thread = Thread(
+ name="session_processor",
+ target=self._process,
+ daemon=True,
+ kwargs={
+ "stop_event": self._stop_event,
+ "poll_now_event": self._poll_now_event,
+ "resume_event": self._resume_event,
+ "cancel_event": self._cancel_event,
+ },
+ )
+ self._thread.start()
+
+ def stop(self, *args, **kwargs) -> None:
+ self._stop_event.set()
+ # Cancel any in-progress generation so that long-running nodes (e.g. denoising) stop at
+ # the next step boundary instead of running to completion. Without this, the generation
+ # thread may still be executing CUDA operations when Python teardown begins, which can
+ # cause a C++ std::terminate() crash ("terminate called without an active exception").
+ self._cancel_event.set()
+ # Wake the thread if it is sleeping in poll_now_event.wait() or blocked in resume_event.wait() (paused).
+ self._poll_now_event.set()
+ self._resume_event.set()
+
+ def _poll_now(self) -> None:
+ self._poll_now_event.set()
+
+ async def _on_queue_cleared(self, event: FastAPIEvent[QueueClearedEvent]) -> None:
+ if self._queue_item and self._queue_item.queue_id == event[1].queue_id:
+ self._cancel_event.set()
+ self._poll_now()
+
+ async def _on_batch_enqueued(self, event: FastAPIEvent[BatchEnqueuedEvent]) -> None:
+ self._poll_now()
+
+ async def _on_queue_item_status_changed(self, event: FastAPIEvent[QueueItemStatusChangedEvent]) -> None:
+ # Make sure the cancel event is for the currently processing queue item
+ if self._queue_item and self._queue_item.item_id != event[1].item_id:
+ return
+ if self._queue_item and event[1].status in ["completed", "failed", "canceled"]:
+ # When the queue item is canceled via HTTP, the queue item status is set to `"canceled"` and this event is
+ # emitted. We need to respond to this event and stop graph execution. This is done by setting the cancel
+ # event, which the session runner checks between invocations. If set, the session runner loop is broken.
+ #
+ # Long-running nodes that cannot be interrupted easily present a challenge. `denoise_latents` is one such
+ # node, but it gets a step callback, called on each step of denoising. This callback checks if the queue item
+ # is canceled, and if it is, raises a `CanceledException` to stop execution immediately.
+ if event[1].status == "canceled":
+ self._cancel_event.set()
+ self._poll_now()
+
+ def resume(self) -> SessionProcessorStatus:
+ if not self._resume_event.is_set():
+ self._resume_event.set()
+ return self.get_status()
+
+ def pause(self) -> SessionProcessorStatus:
+ if self._resume_event.is_set():
+ self._resume_event.clear()
+ return self.get_status()
+
+ def get_status(self) -> SessionProcessorStatus:
+ return SessionProcessorStatus(
+ is_started=self._resume_event.is_set(),
+ is_processing=self._queue_item is not None,
+ )
+
+ def _process(
+ self,
+ stop_event: ThreadEvent,
+ poll_now_event: ThreadEvent,
+ resume_event: ThreadEvent,
+ cancel_event: ThreadEvent,
+ ):
+ try:
+ # Any unhandled exception in this block is a fatal processor error and will stop the processor.
+ self._thread_semaphore.acquire()
+ stop_event.clear()
+ resume_event.set()
+ cancel_event.clear()
+
+ while not stop_event.is_set():
+ poll_now_event.clear()
+ try:
+ # Any unhandled exception in this block is a nonfatal processor error and will be handled.
+ # If we are paused, wait for resume event
+ resume_event.wait()
+
+ # Get the next session to process
+ self._queue_item = self._invoker.services.session_queue.dequeue()
+
+ if self._queue_item is None:
+ # The queue was empty, wait for next polling interval or event to try again
+ self._invoker.services.logger.debug("Waiting for next polling interval or event")
+ poll_now_event.wait(self._polling_interval)
+ continue
+
+ # GC-ing here can reduce peak memory usage of the invoke process by freeing allocated memory blocks.
+ # Most queue items take seconds to execute, so the relative cost of a GC is very small.
+ # Python will never cede allocated memory back to the OS, so anything we can do to reduce the peak
+ # allocation is well worth it.
+ gc.collect()
+
+ self._invoker.services.logger.info(
+ f"Executing queue item {self._queue_item.item_id}, session {self._queue_item.session_id}"
+ )
+ cancel_event.clear()
+
+ # Run the graph
+ self.session_runner.run(queue_item=self._queue_item)
+
+ except Exception as e:
+ error_type = e.__class__.__name__
+ error_message = str(e)
+ error_traceback = traceback.format_exc()
+ self._on_non_fatal_processor_error(
+ queue_item=self._queue_item,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+ # Wait for next polling interval or event to try again
+ poll_now_event.wait(self._polling_interval)
+ continue
+ except Exception as e:
+ # Fatal error in processor, log and pass - we're done here
+ error_type = e.__class__.__name__
+ error_message = str(e)
+ error_traceback = traceback.format_exc()
+ self._invoker.services.logger.error(f"Fatal Error in session processor {error_type}: {error_message}")
+ self._invoker.services.logger.error(error_traceback)
+ pass
+ finally:
+ stop_event.clear()
+ poll_now_event.clear()
+ self._queue_item = None
+ self._thread_semaphore.release()
+
+ def _on_non_fatal_processor_error(
+ self,
+ queue_item: Optional[SessionQueueItem],
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> None:
+ """Called when a non-fatal error occurs in the processor.
+
+ - Log the error.
+ - If a queue item is provided, update the queue item with the completed session & fail it.
+ - Run any callbacks registered for this event.
+ """
+
+ self._invoker.services.logger.error(f"Non-fatal error in session processor {error_type}: {error_message}")
+ self._invoker.services.logger.error(error_traceback)
+
+ if queue_item is not None:
+ # Update the queue item with the completed session & fail it
+ queue_item = self._invoker.services.session_queue.set_queue_item_session(
+ queue_item.item_id, queue_item.session
+ )
+ queue_item = self._invoker.services.session_queue.fail_queue_item(
+ item_id=queue_item.item_id,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+
+ for callback in self._on_non_fatal_processor_error_callbacks:
+ callback(
+ queue_item=queue_item,
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
diff --git a/invokeai/app/services/session_queue/__init__.py b/invokeai/app/services/session_queue/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/session_queue/session_queue_base.py b/invokeai/app/services/session_queue/session_queue_base.py
new file mode 100644
index 00000000000..73acf9c31aa
--- /dev/null
+++ b/invokeai/app/services/session_queue/session_queue_base.py
@@ -0,0 +1,207 @@
+from abc import ABC, abstractmethod
+from typing import Any, Coroutine, Optional
+
+from invokeai.app.services.session_queue.session_queue_common import (
+ QUEUE_ITEM_STATUS,
+ Batch,
+ BatchStatus,
+ CancelAllExceptCurrentResult,
+ CancelByBatchIDsResult,
+ CancelByDestinationResult,
+ CancelByQueueIDResult,
+ ClearResult,
+ DeleteAllExceptCurrentResult,
+ DeleteByDestinationResult,
+ EnqueueBatchResult,
+ IsEmptyResult,
+ IsFullResult,
+ ItemIdsResult,
+ PruneResult,
+ RetryItemsResult,
+ SessionQueueCountsByDestination,
+ SessionQueueItem,
+ SessionQueueStatus,
+)
+from invokeai.app.services.shared.graph import GraphExecutionState
+from invokeai.app.services.shared.pagination import CursorPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+
+
+class SessionQueueBase(ABC):
+ """Base class for session queue"""
+
+ @abstractmethod
+ def dequeue(self) -> Optional[SessionQueueItem]:
+ """Dequeues the next session queue item."""
+ pass
+
+ @abstractmethod
+ def enqueue_batch(
+ self, queue_id: str, batch: Batch, prepend: bool, user_id: str = "system"
+ ) -> Coroutine[Any, Any, EnqueueBatchResult]:
+ """Enqueues all permutations of a batch for execution for a specific user."""
+ pass
+
+ @abstractmethod
+ def get_current(self, queue_id: str) -> Optional[SessionQueueItem]:
+ """Gets the currently-executing session queue item"""
+ pass
+
+ @abstractmethod
+ def get_next(self, queue_id: str) -> Optional[SessionQueueItem]:
+ """Gets the next session queue item (does not dequeue it)"""
+ pass
+
+ @abstractmethod
+ def clear(self, queue_id: str, user_id: Optional[str] = None) -> ClearResult:
+ """Deletes all session queue items. If user_id is provided, only clears items owned by that user."""
+ pass
+
+ @abstractmethod
+ def prune(self, queue_id: str, user_id: Optional[str] = None) -> PruneResult:
+ """Deletes all completed and errored session queue items. If user_id is provided, only prunes items owned by that user."""
+ pass
+
+ @abstractmethod
+ def is_empty(self, queue_id: str) -> IsEmptyResult:
+ """Checks if the queue is empty"""
+ pass
+
+ @abstractmethod
+ def is_full(self, queue_id: str) -> IsFullResult:
+ """Checks if the queue is empty"""
+ pass
+
+ @abstractmethod
+ def get_queue_status(
+ self,
+ queue_id: str,
+ user_id: Optional[str] = None,
+ acting_user_id: Optional[str] = None,
+ ) -> SessionQueueStatus:
+ """Gets the status of the queue. If user_id is provided, also includes user-specific counts.
+
+ acting_user_id is independent of user_id and controls only current-item redaction:
+ when set, the returned status omits item_id/session_id/batch_id unless the
+ currently-running item belongs to acting_user_id. The redaction is decided from the
+ same get_current() snapshot used to embed those identifiers, so it cannot race against
+ a concurrent state change.
+ """
+ pass
+
+ @abstractmethod
+ def get_counts_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> SessionQueueCountsByDestination:
+ """Gets the counts of queue items by destination. If user_id is provided, only counts that user's items."""
+ pass
+
+ @abstractmethod
+ def get_batch_status(self, queue_id: str, batch_id: str, user_id: Optional[str] = None) -> BatchStatus:
+ """Gets the status of a batch. If user_id is provided, only counts that user's items."""
+ pass
+
+ @abstractmethod
+ def complete_queue_item(self, item_id: int) -> SessionQueueItem:
+ """Completes a session queue item"""
+ pass
+
+ @abstractmethod
+ def cancel_queue_item(self, item_id: int) -> SessionQueueItem:
+ """Cancels a session queue item"""
+ pass
+
+ @abstractmethod
+ def delete_queue_item(self, item_id: int) -> None:
+ """Deletes a session queue item"""
+ pass
+
+ @abstractmethod
+ def fail_queue_item(
+ self, item_id: int, error_type: str, error_message: str, error_traceback: str
+ ) -> SessionQueueItem:
+ """Fails a session queue item"""
+ pass
+
+ @abstractmethod
+ def cancel_by_batch_ids(
+ self, queue_id: str, batch_ids: list[str], user_id: Optional[str] = None
+ ) -> CancelByBatchIDsResult:
+ """Cancels all queue items with matching batch IDs. If user_id is provided, only cancels items owned by that user."""
+ pass
+
+ @abstractmethod
+ def cancel_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> CancelByDestinationResult:
+ """Cancels all queue items with the given batch destination. If user_id is provided, only cancels items owned by that user."""
+ pass
+
+ @abstractmethod
+ def delete_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> DeleteByDestinationResult:
+ """Deletes all queue items with the given batch destination. If user_id is provided, only deletes items owned by that user."""
+ pass
+
+ @abstractmethod
+ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
+ """Cancels all queue items with matching queue ID"""
+ pass
+
+ @abstractmethod
+ def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> CancelAllExceptCurrentResult:
+ """Cancels all queue items except in-progress items. If user_id is provided, only cancels items owned by that user."""
+ pass
+
+ @abstractmethod
+ def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> DeleteAllExceptCurrentResult:
+ """Deletes all queue items except in-progress items. If user_id is provided, only deletes items owned by that user."""
+ pass
+
+ @abstractmethod
+ def list_queue_items(
+ self,
+ queue_id: str,
+ limit: int,
+ priority: int,
+ cursor: Optional[int] = None,
+ status: Optional[QUEUE_ITEM_STATUS] = None,
+ destination: Optional[str] = None,
+ ) -> CursorPaginatedResults[SessionQueueItem]:
+ """Gets a page of session queue items. Do not remove."""
+ pass
+
+ @abstractmethod
+ def list_all_queue_items(
+ self,
+ queue_id: str,
+ destination: Optional[str] = None,
+ ) -> list[SessionQueueItem]:
+ """Gets all queue items that match the given parameters"""
+ pass
+
+ @abstractmethod
+ def get_queue_item_ids(
+ self,
+ queue_id: str,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ user_id: Optional[str] = None,
+ ) -> ItemIdsResult:
+ """Gets all queue item ids that match the given parameters. If user_id is provided, only returns items for that user."""
+ pass
+
+ @abstractmethod
+ def get_queue_item(self, item_id: int) -> SessionQueueItem:
+ """Gets a session queue item by ID for a given queue"""
+ pass
+
+ @abstractmethod
+ def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> SessionQueueItem:
+ """Sets the session for a session queue item. Use this to update the session state."""
+ pass
+
+ @abstractmethod
+ def retry_items_by_id(self, queue_id: str, item_ids: list[int]) -> RetryItemsResult:
+ """Retries the given queue items"""
+ pass
diff --git a/invokeai/app/services/session_queue/session_queue_common.py b/invokeai/app/services/session_queue/session_queue_common.py
new file mode 100644
index 00000000000..d87221fbbae
--- /dev/null
+++ b/invokeai/app/services/session_queue/session_queue_common.py
@@ -0,0 +1,654 @@
+import datetime
+import json
+from itertools import chain, product
+from typing import Generator, Literal, Optional, TypeAlias, Union
+
+from pydantic import (
+ AliasChoices,
+ BaseModel,
+ ConfigDict,
+ Field,
+ StrictStr,
+ TypeAdapter,
+ field_validator,
+ model_validator,
+)
+from pydantic_core import to_jsonable_python
+
+from invokeai.app.invocations.fields import ImageField
+from invokeai.app.services.shared.graph import Graph, GraphExecutionState, NodeNotFoundError
+from invokeai.app.services.workflow_records.workflow_records_common import (
+ WorkflowWithoutID,
+ WorkflowWithoutIDValidator,
+)
+from invokeai.app.util.misc import uuid_string
+
+# region Errors
+
+
+class BatchZippedLengthError(ValueError):
+ """Raise when a batch has items of different lengths."""
+
+
+class BatchItemsTypeError(ValueError): # this cannot be a TypeError in pydantic v2
+ """Raise when a batch has items of different types."""
+
+
+class BatchDuplicateNodeFieldError(ValueError):
+ """Raise when a batch has duplicate node_path and field_name."""
+
+
+class TooManySessionsError(ValueError):
+ """Raise when too many sessions are requested."""
+
+
+class SessionQueueItemNotFoundError(ValueError):
+ """Raise when a queue item is not found."""
+
+
+# endregion
+
+
+# region Batch
+
+BatchDataType = Union[StrictStr, float, int, ImageField]
+
+
+class NodeFieldValue(BaseModel):
+ node_path: str = Field(description="The node into which this batch data item will be substituted.")
+ field_name: str = Field(description="The field into which this batch data item will be substituted.")
+ value: BatchDataType = Field(description="The value to substitute into the node/field.")
+
+
+class BatchDatum(BaseModel):
+ node_path: str = Field(description="The node into which this batch data collection will be substituted.")
+ field_name: str = Field(description="The field into which this batch data collection will be substituted.")
+ items: list[BatchDataType] = Field(
+ default_factory=list, description="The list of items to substitute into the node/field."
+ )
+
+
+BatchDataCollection: TypeAlias = list[list[BatchDatum]]
+
+
+class Batch(BaseModel):
+ batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
+ origin: str | None = Field(
+ default=None,
+ description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
+ )
+ destination: str | None = Field(
+ default=None,
+ description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
+ )
+ data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
+ graph: Graph = Field(description="The graph to initialize the session with")
+ workflow: Optional[WorkflowWithoutID] = Field(
+ default=None, description="The workflow to initialize the session with"
+ )
+ runs: int = Field(
+ default=1, ge=1, description="Int stating how many times to iterate through all possible batch indices"
+ )
+
+ @field_validator("data")
+ def validate_lengths(cls, v: Optional[BatchDataCollection]):
+ if v is None:
+ return v
+ for batch_data_list in v:
+ first_item_length = len(batch_data_list[0].items) if batch_data_list and batch_data_list[0].items else 0
+ for i in batch_data_list:
+ if len(i.items) != first_item_length:
+ raise BatchZippedLengthError("Zipped batch items must all have the same length")
+ return v
+
+ @field_validator("data")
+ def validate_types(cls, v: Optional[BatchDataCollection]):
+ if v is None:
+ return v
+ for batch_data_list in v:
+ for datum in batch_data_list:
+ if not datum.items:
+ continue
+
+ # Special handling for numbers - they can be mixed
+ # TODO(psyche): Update BatchDatum to have a `type` field to specify the type of the items, then we can have strict float and int fields
+ if all(isinstance(item, (int, float)) for item in datum.items):
+ continue
+
+ # Get the type of the first item in the list
+ first_item_type = type(datum.items[0])
+ for item in datum.items:
+ if type(item) is not first_item_type:
+ raise BatchItemsTypeError("All items in a batch must have the same type")
+ return v
+
+ @field_validator("data")
+ def validate_unique_field_mappings(cls, v: Optional[BatchDataCollection]):
+ if v is None:
+ return v
+ paths: set[tuple[str, str]] = set()
+ for batch_data_list in v:
+ for datum in batch_data_list:
+ pair = (datum.node_path, datum.field_name)
+ if pair in paths:
+ raise BatchDuplicateNodeFieldError("Each batch data must have unique node_id and field_name")
+ paths.add(pair)
+ return v
+
+ @model_validator(mode="after")
+ def validate_batch_nodes_and_edges(self):
+ if self.data is None:
+ return self
+ for batch_data_list in self.data:
+ for batch_data in batch_data_list:
+ try:
+ node = self.graph.get_node(batch_data.node_path)
+ except NodeNotFoundError:
+ raise NodeNotFoundError(f"Node {batch_data.node_path} not found in graph")
+ if batch_data.field_name not in type(node).model_fields:
+ raise NodeNotFoundError(f"Field {batch_data.field_name} not found in node {batch_data.node_path}")
+ return self
+
+ @field_validator("graph")
+ def validate_graph(cls, v: Graph):
+ v.validate_self()
+ return v
+
+ model_config = ConfigDict(
+ json_schema_extra={
+ "required": [
+ "graph",
+ "runs",
+ ]
+ }
+ )
+
+
+# endregion Batch
+
+
+# region Queue Items
+
+DEFAULT_QUEUE_ID = "default"
+SYSTEM_USER_ID = "system" # Default user_id for system-generated queue items
+
+QUEUE_ITEM_STATUS = Literal["pending", "in_progress", "completed", "failed", "canceled"]
+
+
+class ItemIdsResult(BaseModel):
+ """Response containing ordered item ids with metadata for optimistic updates."""
+
+ item_ids: list[int] = Field(description="Ordered list of item ids")
+ total_count: int = Field(description="Total number of queue items matching the query")
+
+
+NodeFieldValueValidator = TypeAdapter(list[NodeFieldValue])
+
+
+def get_field_values(queue_item_dict: dict) -> Optional[list[NodeFieldValue]]:
+ field_values_raw = queue_item_dict.get("field_values", None)
+ return NodeFieldValueValidator.validate_json(field_values_raw) if field_values_raw is not None else None
+
+
+GraphExecutionStateValidator = TypeAdapter(GraphExecutionState)
+
+
+def get_session(queue_item_dict: dict) -> GraphExecutionState:
+ session_raw = queue_item_dict.get("session", "{}")
+ session = GraphExecutionStateValidator.validate_json(session_raw, strict=False)
+ return session
+
+
+def get_workflow(queue_item_dict: dict) -> Optional[WorkflowWithoutID]:
+ workflow_raw = queue_item_dict.get("workflow", None)
+ if workflow_raw is not None:
+ workflow = WorkflowWithoutIDValidator.validate_json(workflow_raw, strict=False)
+ return workflow
+ return None
+
+
+class FieldIdentifier(BaseModel):
+ kind: Literal["input", "output"] = Field(description="The kind of field")
+ node_id: str = Field(description="The ID of the node")
+ field_name: str = Field(description="The name of the field")
+ user_label: str | None = Field(description="The user label of the field, if any")
+
+
+class SessionQueueItem(BaseModel):
+ """Session queue item without the full graph. Used for serialization."""
+
+ item_id: int = Field(description="The identifier of the session queue item")
+ status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
+ status_sequence: int | None = Field(
+ default=None,
+ # Fallback for rows serialized before migration_28 added the DB-level default of 0.
+ description="A monotonically increasing version for this queue item's visible status lifecycle",
+ )
+ priority: int = Field(default=0, description="The priority of this queue item")
+ batch_id: str = Field(description="The ID of the batch associated with this queue item")
+ origin: str | None = Field(
+ default=None,
+ description="The origin of this queue item. This data is used by the frontend to determine how to handle results.",
+ )
+ destination: str | None = Field(
+ default=None,
+ description="The origin of this queue item. This data is used by the frontend to determine how to handle results",
+ )
+ session_id: str = Field(
+ description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
+ )
+ error_type: Optional[str] = Field(default=None, description="The error type if this queue item errored")
+ error_message: Optional[str] = Field(default=None, description="The error message if this queue item errored")
+ error_traceback: Optional[str] = Field(
+ default=None,
+ description="The error traceback if this queue item errored",
+ validation_alias=AliasChoices("error_traceback", "error"),
+ )
+ created_at: Union[datetime.datetime, str] = Field(description="When this queue item was created")
+ updated_at: Union[datetime.datetime, str] = Field(description="When this queue item was updated")
+ started_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was started")
+ completed_at: Optional[Union[datetime.datetime, str]] = Field(description="When this queue item was completed")
+ queue_id: str = Field(description="The id of the queue with which this item is associated")
+ user_id: str = Field(default="system", description="The id of the user who created this queue item")
+ user_display_name: Optional[str] = Field(
+ default=None, description="The display name of the user who created this queue item, if available"
+ )
+ user_email: Optional[str] = Field(
+ default=None, description="The email of the user who created this queue item, if available"
+ )
+ field_values: Optional[list[NodeFieldValue]] = Field(
+ default=None, description="The field values that were used for this queue item"
+ )
+ retried_from_item_id: Optional[int] = Field(
+ default=None, description="The item_id of the queue item that this item was retried from"
+ )
+ session: GraphExecutionState = Field(description="The fully-populated session to be executed")
+ workflow: Optional[WorkflowWithoutID] = Field(
+ default=None, description="The workflow associated with this queue item"
+ )
+
+ @classmethod
+ def queue_item_from_dict(cls, queue_item_dict: dict) -> "SessionQueueItem":
+ # must parse these manually
+ queue_item_dict["field_values"] = get_field_values(queue_item_dict)
+ queue_item_dict["session"] = get_session(queue_item_dict)
+ queue_item_dict["workflow"] = get_workflow(queue_item_dict)
+ return SessionQueueItem(**queue_item_dict)
+
+ model_config = ConfigDict(
+ json_schema_extra={
+ "required": [
+ "item_id",
+ "status",
+ "batch_id",
+ "queue_id",
+ "session_id",
+ "session",
+ "priority",
+ "session_id",
+ "created_at",
+ "updated_at",
+ ]
+ }
+ )
+
+
+# endregion Queue Items
+
+# region Query Results
+
+
+class SessionQueueStatus(BaseModel):
+ queue_id: str = Field(..., description="The ID of the queue")
+ item_id: Optional[int] = Field(description="The current queue item id")
+ batch_id: Optional[str] = Field(description="The current queue item's batch id")
+ session_id: Optional[str] = Field(description="The current queue item's session id")
+ pending: int = Field(..., description="Number of queue items with status 'pending'")
+ in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
+ completed: int = Field(..., description="Number of queue items with status 'complete'")
+ failed: int = Field(..., description="Number of queue items with status 'error'")
+ canceled: int = Field(..., description="Number of queue items with status 'canceled'")
+ total: int = Field(..., description="Total number of queue items")
+
+
+class SessionQueueCountsByDestination(BaseModel):
+ queue_id: str = Field(..., description="The ID of the queue")
+ destination: str = Field(..., description="The destination of queue items included in this status")
+ pending: int = Field(..., description="Number of queue items with status 'pending' for the destination")
+ in_progress: int = Field(..., description="Number of queue items with status 'in_progress' for the destination")
+ completed: int = Field(..., description="Number of queue items with status 'complete' for the destination")
+ failed: int = Field(..., description="Number of queue items with status 'error' for the destination")
+ canceled: int = Field(..., description="Number of queue items with status 'canceled' for the destination")
+ total: int = Field(..., description="Total number of queue items for the destination")
+
+
+class BatchStatus(BaseModel):
+ queue_id: str = Field(..., description="The ID of the queue")
+ batch_id: str = Field(..., description="The ID of the batch")
+ origin: str | None = Field(..., description="The origin of the batch")
+ destination: str | None = Field(..., description="The destination of the batch")
+ pending: int = Field(..., description="Number of queue items with status 'pending'")
+ in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
+ completed: int = Field(..., description="Number of queue items with status 'complete'")
+ failed: int = Field(..., description="Number of queue items with status 'error'")
+ canceled: int = Field(..., description="Number of queue items with status 'canceled'")
+ total: int = Field(..., description="Total number of queue items")
+
+
+class EnqueueBatchResult(BaseModel):
+ queue_id: str = Field(description="The ID of the queue")
+ enqueued: int = Field(description="The total number of queue items enqueued")
+ requested: int = Field(description="The total number of queue items requested to be enqueued")
+ batch: Batch = Field(description="The batch that was enqueued")
+ priority: int = Field(description="The priority of the enqueued batch")
+ item_ids: list[int] = Field(description="The IDs of the queue items that were enqueued")
+
+
+class RetryItemsResult(BaseModel):
+ queue_id: str = Field(description="The ID of the queue")
+ retried_item_ids: list[int] = Field(description="The IDs of the queue items that were retried")
+
+
+class ClearResult(BaseModel):
+ """Result of clearing the session queue"""
+
+ deleted: int = Field(..., description="Number of queue items deleted")
+
+
+class PruneResult(ClearResult):
+ """Result of pruning the session queue"""
+
+ pass
+
+
+class CancelByBatchIDsResult(BaseModel):
+ """Result of canceling by list of batch ids"""
+
+ canceled: int = Field(..., description="Number of queue items canceled")
+
+
+class CancelByDestinationResult(CancelByBatchIDsResult):
+ """Result of canceling by a destination"""
+
+ pass
+
+
+class DeleteByDestinationResult(BaseModel):
+ """Result of deleting by a destination"""
+
+ deleted: int = Field(..., description="Number of queue items deleted")
+
+
+class DeleteAllExceptCurrentResult(DeleteByDestinationResult):
+ """Result of deleting all except current"""
+
+ pass
+
+
+class CancelByQueueIDResult(CancelByBatchIDsResult):
+ """Result of canceling by queue id"""
+
+ pass
+
+
+class CancelAllExceptCurrentResult(CancelByBatchIDsResult):
+ """Result of canceling all except current"""
+
+ pass
+
+
+class IsEmptyResult(BaseModel):
+ """Result of checking if the session queue is empty"""
+
+ is_empty: bool = Field(..., description="Whether the session queue is empty")
+
+
+class IsFullResult(BaseModel):
+ """Result of checking if the session queue is full"""
+
+ is_full: bool = Field(..., description="Whether the session queue is full")
+
+
+# endregion Query Results
+
+
+# region Util
+
+
+def create_session_nfv_tuples(batch: Batch, maximum: int) -> Generator[tuple[str, str, str], None, None]:
+ """
+ Given a batch and a maximum number of sessions to create, generate a tuple of session_id, session_json, and
+ field_values_json for each session.
+
+ The batch has a "source" graph and a data property. The data property is a list of lists of BatchDatum objects.
+ Each BatchDatum has a field identifier (e.g. a node id and field name), and a list of values to substitute into
+ the field.
+
+ This structure allows us to create a new graph for every possible permutation of BatchDatum objects:
+ - Each BatchDatum can be "expanded" into a dict of node-field-value tuples - one for each item in the BatchDatum.
+ - Zip each inner list of expanded BatchDatum objects together. Call this a "batch_data_list".
+ - Take the cartesian product of all zipped batch_data_lists, resulting in a list of permutations of BatchDatum
+ - Take the cartesian product of all zipped batch_data_lists, resulting in a list of lists of BatchDatum objects.
+ Each inner list now represents the substitution values for a single permutation (session).
+ - For each permutation, substitute the values into the graph
+
+ This function is optimized for performance, as it is used to generate a large number of sessions at once.
+
+ Args:
+ batch: The batch to generate sessions from
+ maximum: The maximum number of sessions to generate
+
+ Returns:
+ A generator that yields tuples of session_id, session_json, and field_values_json for each session. The
+ generator will stop early if the maximum number of sessions is reached.
+ """
+
+ # TODO: Should this be a class method on Batch?
+
+ data: list[list[tuple[dict]]] = []
+ batch_data_collection = batch.data if batch.data is not None else []
+
+ for batch_datum_list in batch_data_collection:
+ node_field_values_to_zip: list[list[dict]] = []
+ # Expand each BatchDatum into a list of dicts - one for each item in the BatchDatum
+ for batch_datum in batch_datum_list:
+ node_field_values = [
+ # Note: A tuple here is slightly faster than a dict, but we need the object in dict form to be inserted
+ # in the session_queue table anyways. So, overall creating NFVs as dicts is faster.
+ {"node_path": batch_datum.node_path, "field_name": batch_datum.field_name, "value": item}
+ for item in batch_datum.items
+ ]
+ node_field_values_to_zip.append(node_field_values)
+ # Zip the dicts together to create a list of dicts for each permutation
+ data.append(list(zip(*node_field_values_to_zip, strict=True))) # type: ignore [arg-type]
+
+ # We serialize the graph and session once, then mutate the graph dict in place for each session.
+ #
+ # This sounds scary, but it's actually fine.
+ #
+ # The batch prep logic injects field values into the same fields for each generated session.
+ #
+ # For example, after the product operation, we'll end up with a list of node-field-value tuples like this:
+ # [
+ # (
+ # {"node_path": "1", "field_name": "a", "value": 1},
+ # {"node_path": "2", "field_name": "b", "value": 2},
+ # {"node_path": "3", "field_name": "c", "value": 3},
+ # ),
+ # (
+ # {"node_path": "1", "field_name": "a", "value": 4},
+ # {"node_path": "2", "field_name": "b", "value": 5},
+ # {"node_path": "3", "field_name": "c", "value": 6},
+ # )
+ # ]
+ #
+ # Note that each tuple has the same length, and each tuple substitutes values in for exactly the same node fields.
+ # No matter the complexity of the batch, this property holds true.
+ #
+ # This means each permutation's substitution can be done in-place on the same graph dict, because it overwrites the
+ # previous mutation. We only need to serialize the graph once, and then we can mutate it in place for each session.
+ #
+ # Previously, we had created new Graph objects for each session, but this was very slow for large (1k+ session
+ # batches). We then tried dumping the graph to dict and using deep-copy to create a new dict for each session,
+ # but this was also slow.
+ #
+ # Overall, we achieved a 100x speedup by mutating the graph dict in place for each session over creating new Graph
+ # objects for each session.
+ #
+ # We will also mutate the session dict in place, setting a new ID for each session and setting the mutated graph
+ # dict as the session's graph.
+
+ # Dump the batch's graph to a dict once
+ graph_as_dict = batch.graph.model_dump(warnings=False, exclude_none=True)
+
+ # We must provide a Graph object when creating the "dummy" session dict, but we don't actually use it. It will be
+ # overwritten for each session by the mutated graph_as_dict.
+ session_dict = GraphExecutionState(graph=Graph()).model_dump(warnings=False, exclude_none=True)
+
+ # Now we can create a generator that yields the session_id, session_json, and field_values_json for each session.
+ count = 0
+
+ # Each batch may have multiple runs, so we need to generate the same number of sessions for each run. The total is
+ # still limited by the maximum number of sessions.
+ for _ in range(batch.runs):
+ for d in product(*data):
+ if count >= maximum:
+ # We've reached the maximum number of sessions we may generate
+ return
+
+ # Flatten the list of lists of dicts into a single list of dicts
+ # TODO(psyche): Is the a more efficient way to do this?
+ flat_node_field_values = list(chain.from_iterable(d))
+
+ # Need a fresh ID for each session
+ session_id = uuid_string()
+
+ # Mutate the session dict in place
+ session_dict["id"] = session_id
+
+ # Substitute the values into the graph
+ for nfv in flat_node_field_values:
+ graph_as_dict["nodes"][nfv["node_path"]][nfv["field_name"]] = nfv["value"]
+
+ # Mutate the session dict in place
+ session_dict["graph"] = graph_as_dict
+
+ # Serialize the session and field values
+ # Note the use of pydantic's to_jsonable_python to handle serialization of any python object, including sets.
+ session_json = json.dumps(session_dict, default=to_jsonable_python)
+ field_values_json = json.dumps(flat_node_field_values, default=to_jsonable_python)
+
+ # Yield the session_id, session_json, and field_values_json
+ yield (session_id, session_json, field_values_json)
+
+ # Increment the count so we know when to stop
+ count += 1
+
+
+def calc_session_count(batch: Batch) -> int:
+ """
+ Calculates the number of sessions that would be created by the batch, without incurring the overhead of actually
+ creating them, as is done in `create_session_nfv_tuples()`.
+
+ The count is used to communicate to the user how many sessions were _requested_ to be created, as opposed to how
+ many were _actually_ created (which may be less due to the maximum number of sessions).
+ """
+ # TODO: Should this be a class method on Batch?
+ if not batch.data:
+ return batch.runs
+ data = []
+ for batch_datum_list in batch.data:
+ to_zip = []
+ for batch_datum in batch_datum_list:
+ batch_data_items = range(len(batch_datum.items))
+ to_zip.append(batch_data_items)
+ data.append(list(zip(*to_zip, strict=True)))
+ data_product = list(product(*data))
+ return len(data_product) * batch.runs
+
+
+ValueToInsertTuple: TypeAlias = tuple[
+ str, # queue_id
+ str, # session (as stringified JSON)
+ str, # session_id
+ str, # batch_id
+ str | None, # field_values (optional, as stringified JSON)
+ int, # priority
+ str | None, # workflow (optional, as stringified JSON)
+ str | None, # origin (optional)
+ str | None, # destination (optional)
+ int | None, # retried_from_item_id (optional, this is always None for new items)
+ str, # user_id
+]
+"""A type alias for the tuple of values to insert into the session queue table.
+
+**If you change this, be sure to update the `enqueue_batch` and `retry_items_by_id` methods in the session queue service!**
+"""
+
+
+def prepare_values_to_insert(
+ queue_id: str, batch: Batch, priority: int, max_new_queue_items: int, user_id: str = "system"
+) -> list[ValueToInsertTuple]:
+ """
+ Given a batch, prepare the values to insert into the session queue table. The list of tuples can be used with an
+ `executemany` statement to insert multiple rows at once.
+
+ Args:
+ queue_id: The ID of the queue to insert the items into
+ batch: The batch to prepare the values for
+ priority: The priority of the queue items
+ max_new_queue_items: The maximum number of queue items to insert
+ user_id: The user ID who is creating these queue items
+
+ Returns:
+ A list of tuples to insert into the session queue table. Each tuple contains the following values:
+ - queue_id
+ - session (as stringified JSON)
+ - session_id
+ - batch_id
+ - field_values (optional, as stringified JSON)
+ - priority
+ - workflow (optional, as stringified JSON)
+ - origin (optional)
+ - destination (optional)
+ - retried_from_item_id (optional, this is always None for new items)
+ - user_id
+ """
+
+ # A tuple is a fast and memory-efficient way to store the values to insert. Previously, we used a NamedTuple, but
+ # measured a ~5% performance improvement by using a normal tuple instead. For very large batches (10k+ items), the
+ # this difference becomes noticeable.
+ #
+ # So, despite the inferior DX with normal tuples, we use one here for performance reasons.
+
+ values_to_insert: list[ValueToInsertTuple] = []
+
+ # pydantic's to_jsonable_python handles serialization of any python object, including sets, which json.dumps does
+ # not support by default. Apparently there are sets somewhere in the graph.
+
+ # The same workflow is used for all sessions in the batch - serialize it once
+ workflow_json = json.dumps(batch.workflow, default=to_jsonable_python) if batch.workflow else None
+
+ for session_id, session_json, field_values_json in create_session_nfv_tuples(batch, max_new_queue_items):
+ values_to_insert.append(
+ (
+ queue_id,
+ session_json,
+ session_id,
+ batch.batch_id,
+ field_values_json,
+ priority,
+ workflow_json,
+ batch.origin,
+ batch.destination,
+ None,
+ user_id,
+ )
+ )
+ return values_to_insert
+
+
+# endregion Util
+
+Batch.model_rebuild(force=True)
+SessionQueueItem.model_rebuild(force=True)
diff --git a/invokeai/app/services/session_queue/session_queue_sqlite.py b/invokeai/app/services/session_queue/session_queue_sqlite.py
new file mode 100644
index 00000000000..a05ed468857
--- /dev/null
+++ b/invokeai/app/services/session_queue/session_queue_sqlite.py
@@ -0,0 +1,1036 @@
+import asyncio
+import json
+import sqlite3
+from typing import Optional, Union, cast
+
+from pydantic_core import to_jsonable_python
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.session_queue.session_queue_base import SessionQueueBase
+from invokeai.app.services.session_queue.session_queue_common import (
+ DEFAULT_QUEUE_ID,
+ QUEUE_ITEM_STATUS,
+ Batch,
+ BatchStatus,
+ CancelAllExceptCurrentResult,
+ CancelByBatchIDsResult,
+ CancelByDestinationResult,
+ CancelByQueueIDResult,
+ ClearResult,
+ DeleteAllExceptCurrentResult,
+ DeleteByDestinationResult,
+ EnqueueBatchResult,
+ IsEmptyResult,
+ IsFullResult,
+ ItemIdsResult,
+ PruneResult,
+ RetryItemsResult,
+ SessionQueueCountsByDestination,
+ SessionQueueItem,
+ SessionQueueItemNotFoundError,
+ SessionQueueStatus,
+ ValueToInsertTuple,
+ calc_session_count,
+ prepare_values_to_insert,
+)
+from invokeai.app.services.shared.graph import GraphExecutionState
+from invokeai.app.services.shared.pagination import CursorPaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+
+
+class SqliteSessionQueue(SessionQueueBase):
+ __invoker: Invoker
+
+ def start(self, invoker: Invoker) -> None:
+ self.__invoker = invoker
+ self._set_in_progress_to_canceled()
+ config = self.__invoker.services.configuration
+ if config.clear_queue_on_startup:
+ clear_result = self.clear(DEFAULT_QUEUE_ID)
+ if clear_result.deleted > 0:
+ self.__invoker.services.logger.info(f"Cleared all {clear_result.deleted} queue items")
+ return
+
+ if config.max_queue_history is not None:
+ deleted = self._prune_terminal_to_limit(DEFAULT_QUEUE_ID, config.max_queue_history)
+ if deleted > 0:
+ self.__invoker.services.logger.info(
+ f"Pruned {deleted} completed/failed/canceled queue items (kept up to {config.max_queue_history})"
+ )
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def _set_in_progress_to_canceled(self) -> None:
+ """
+ Sets all in_progress queue items to canceled. Run on app startup, not associated with any queue.
+ This is necessary because the invoker may have been killed while processing a queue item.
+ """
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ UPDATE session_queue
+ SET status = 'canceled',
+ status_sequence = COALESCE(status_sequence, 0) + 1
+ WHERE status = 'in_progress';
+ """
+ )
+
+ def _prune_terminal_to_limit(self, queue_id: str, keep: int) -> int:
+ """Prune terminal items (completed/failed/canceled) to keep at most N most-recent items."""
+ with self._db.transaction() as cursor:
+ where = """--sql
+ WHERE
+ queue_id = ?
+ AND (
+ status = 'completed'
+ OR status = 'failed'
+ OR status = 'canceled'
+ )
+ """
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where}
+ AND item_id NOT IN (
+ SELECT item_id
+ FROM session_queue
+ {where}
+ ORDER BY COALESCE(completed_at, updated_at, created_at) DESC, item_id DESC
+ LIMIT ?
+ );
+ """,
+ (queue_id, queue_id, keep),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE
+ FROM session_queue
+ {where}
+ AND item_id NOT IN (
+ SELECT item_id
+ FROM session_queue
+ {where}
+ ORDER BY COALESCE(completed_at, updated_at, created_at) DESC, item_id DESC
+ LIMIT ?
+ );
+ """,
+ (queue_id, queue_id, keep),
+ )
+ return count
+
+ def _get_current_queue_size(self, queue_id: str) -> int:
+ """Gets the current number of pending queue items"""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT count(*)
+ FROM session_queue
+ WHERE
+ queue_id = ?
+ AND status = 'pending'
+ """,
+ (queue_id,),
+ )
+ count = cast(int, cursor.fetchone()[0])
+ return count
+
+ def _get_highest_priority(self, queue_id: str) -> int:
+ """Gets the highest priority value in the queue"""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT MAX(priority)
+ FROM session_queue
+ WHERE
+ queue_id = ?
+ AND status = 'pending'
+ """,
+ (queue_id,),
+ )
+ priority = cast(Union[int, None], cursor.fetchone()[0]) or 0
+ return priority
+
+ async def enqueue_batch(
+ self, queue_id: str, batch: Batch, prepend: bool, user_id: str = "system"
+ ) -> EnqueueBatchResult:
+ current_queue_size = self._get_current_queue_size(queue_id)
+ max_queue_size = self.__invoker.services.configuration.max_queue_size
+ max_new_queue_items = max_queue_size - current_queue_size
+
+ priority = 0
+ if prepend:
+ priority = self._get_highest_priority(queue_id) + 1
+
+ requested_count = await asyncio.to_thread(
+ calc_session_count,
+ batch=batch,
+ )
+ values_to_insert = await asyncio.to_thread(
+ prepare_values_to_insert,
+ queue_id=queue_id,
+ batch=batch,
+ priority=priority,
+ max_new_queue_items=max_new_queue_items,
+ user_id=user_id,
+ )
+ enqueued_count = len(values_to_insert)
+
+ with self._db.transaction() as cursor:
+ cursor.executemany(
+ """--sql
+ INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id, user_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ values_to_insert,
+ )
+ cursor.execute(
+ """--sql
+ SELECT item_id
+ FROM session_queue
+ WHERE batch_id = ?
+ ORDER BY item_id DESC;
+ """,
+ (batch.batch_id,),
+ )
+ item_ids = [row[0] for row in cursor.fetchall()]
+ enqueue_result = EnqueueBatchResult(
+ queue_id=queue_id,
+ requested=requested_count,
+ enqueued=enqueued_count,
+ batch=batch,
+ priority=priority,
+ item_ids=item_ids,
+ )
+ self.__invoker.services.events.emit_batch_enqueued(enqueue_result, user_id=user_id)
+ return enqueue_result
+
+ def dequeue(self) -> Optional[SessionQueueItem]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT
+ sq.*,
+ u.display_name as user_display_name,
+ u.email as user_email
+ FROM session_queue sq
+ LEFT JOIN users u ON sq.user_id = u.user_id
+ WHERE sq.status = 'pending'
+ ORDER BY
+ sq.priority DESC,
+ sq.item_id ASC
+ LIMIT 1
+ """
+ )
+ result = cast(Union[sqlite3.Row, None], cursor.fetchone())
+ if result is None:
+ return None
+ queue_item = SessionQueueItem.queue_item_from_dict(dict(result))
+ queue_item = self._set_queue_item_status(item_id=queue_item.item_id, status="in_progress")
+ return queue_item
+
+ def get_next(self, queue_id: str) -> Optional[SessionQueueItem]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT
+ sq.*,
+ u.display_name as user_display_name,
+ u.email as user_email
+ FROM session_queue sq
+ LEFT JOIN users u ON sq.user_id = u.user_id
+ WHERE
+ sq.queue_id = ?
+ AND sq.status = 'pending'
+ ORDER BY
+ sq.priority DESC,
+ sq.created_at ASC
+ LIMIT 1
+ """,
+ (queue_id,),
+ )
+ result = cast(Union[sqlite3.Row, None], cursor.fetchone())
+ if result is None:
+ return None
+ return SessionQueueItem.queue_item_from_dict(dict(result))
+
+ def get_current(self, queue_id: str) -> Optional[SessionQueueItem]:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT
+ sq.*,
+ u.display_name as user_display_name,
+ u.email as user_email
+ FROM session_queue sq
+ LEFT JOIN users u ON sq.user_id = u.user_id
+ WHERE
+ sq.queue_id = ?
+ AND sq.status = 'in_progress'
+ LIMIT 1
+ """,
+ (queue_id,),
+ )
+ result = cast(Union[sqlite3.Row, None], cursor.fetchone())
+ if result is None:
+ return None
+ return SessionQueueItem.queue_item_from_dict(dict(result))
+
+ def _set_queue_item_status(
+ self,
+ item_id: int,
+ status: QUEUE_ITEM_STATUS,
+ error_type: Optional[str] = None,
+ error_message: Optional[str] = None,
+ error_traceback: Optional[str] = None,
+ ) -> SessionQueueItem:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT status FROM session_queue WHERE item_id = ?
+ """,
+ (item_id,),
+ )
+ row = cursor.fetchone()
+ if row is None:
+ raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}")
+ current_status = row[0]
+
+ # Only update if not already finished (completed, failed or canceled)
+ if current_status in ("completed", "failed", "canceled"):
+ return self.get_queue_item(item_id)
+
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ UPDATE session_queue
+ SET status = ?, status_sequence = COALESCE(status_sequence, 0) + 1, error_type = ?, error_message = ?, error_traceback = ?
+ WHERE item_id = ?
+ """,
+ (status, error_type, error_message, error_traceback, item_id),
+ )
+
+ queue_item = self.get_queue_item(item_id)
+ batch_status = self.get_batch_status(queue_id=queue_item.queue_id, batch_id=queue_item.batch_id)
+ # The QueueItemStatusChangedEvent ships to user:{queue_item.user_id} and admin rooms.
+ # acting_user_id ensures the embedded current-item identifiers are redacted when the
+ # in-progress item belongs to someone else, while leaving aggregate counts global.
+ # Doing this inside get_queue_status guarantees the redaction decision and the
+ # embedded identifiers come from the same get_current() snapshot — eliminating the
+ # race where a second read could find None and skip scrubbing stale identifiers.
+ queue_status = self.get_queue_status(queue_id=queue_item.queue_id, acting_user_id=queue_item.user_id)
+
+ self.__invoker.services.events.emit_queue_item_status_changed(queue_item, batch_status, queue_status)
+ return queue_item
+
+ def is_empty(self, queue_id: str) -> IsEmptyResult:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT count(*)
+ FROM session_queue
+ WHERE queue_id = ?
+ """,
+ (queue_id,),
+ )
+ is_empty = cast(int, cursor.fetchone()[0]) == 0
+ return IsEmptyResult(is_empty=is_empty)
+
+ def is_full(self, queue_id: str) -> IsFullResult:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT count(*)
+ FROM session_queue
+ WHERE queue_id = ?
+ """,
+ (queue_id,),
+ )
+ max_queue_size = self.__invoker.services.configuration.max_queue_size
+ is_full = cast(int, cursor.fetchone()[0]) >= max_queue_size
+ return IsFullResult(is_full=is_full)
+
+ def clear(self, queue_id: str, user_id: Optional[str] = None) -> ClearResult:
+ with self._db.transaction() as cursor:
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE queue_id = ?
+ {user_filter}
+ """
+ params: list[str] = [queue_id]
+ if user_id is not None:
+ params.append(user_id)
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where}
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE
+ FROM session_queue
+ {where}
+ """,
+ tuple(params),
+ )
+ self.__invoker.services.events.emit_queue_cleared(queue_id)
+ return ClearResult(deleted=count)
+
+ def prune(self, queue_id: str, user_id: Optional[str] = None) -> PruneResult:
+ with self._db.transaction() as cursor:
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE
+ queue_id = ?
+ AND (
+ status = 'completed'
+ OR status = 'failed'
+ OR status = 'canceled'
+ )
+ {user_filter}
+ """
+ params = [queue_id]
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ return PruneResult(deleted=count)
+
+ def cancel_queue_item(self, item_id: int) -> SessionQueueItem:
+ queue_item = self._set_queue_item_status(item_id=item_id, status="canceled")
+ return queue_item
+
+ def delete_queue_item(self, item_id: int) -> None:
+ """Deletes a session queue item"""
+ try:
+ self.cancel_queue_item(item_id)
+ except SessionQueueItemNotFoundError:
+ pass
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE
+ FROM session_queue
+ WHERE item_id = ?
+ """,
+ (item_id,),
+ )
+
+ def complete_queue_item(self, item_id: int) -> SessionQueueItem:
+ queue_item = self._set_queue_item_status(item_id=item_id, status="completed")
+ return queue_item
+
+ def fail_queue_item(
+ self,
+ item_id: int,
+ error_type: str,
+ error_message: str,
+ error_traceback: str,
+ ) -> SessionQueueItem:
+ queue_item = self._set_queue_item_status(
+ item_id=item_id,
+ status="failed",
+ error_type=error_type,
+ error_message=error_message,
+ error_traceback=error_traceback,
+ )
+ return queue_item
+
+ def cancel_by_batch_ids(
+ self, queue_id: str, batch_ids: list[str], user_id: Optional[str] = None
+ ) -> CancelByBatchIDsResult:
+ with self._db.transaction() as cursor:
+ current_queue_item = self.get_current(queue_id)
+ placeholders = ", ".join(["?" for _ in batch_ids])
+
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE
+ queue_id == ?
+ AND batch_id IN ({placeholders})
+ AND status != 'canceled'
+ AND status != 'completed'
+ AND status != 'failed'
+ -- We will cancel the current item separately below - skip it here
+ AND status != 'in_progress'
+ {user_filter}
+ """
+ params = [queue_id] + batch_ids
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ UPDATE session_queue
+ SET status = 'canceled',
+ status_sequence = COALESCE(status_sequence, 0) + 1
+ {where};
+ """,
+ tuple(params),
+ )
+
+ # Handle current item separately - check ownership if user_id is provided
+ if current_queue_item is not None and current_queue_item.batch_id in batch_ids:
+ if user_id is None or current_queue_item.user_id == user_id:
+ self._set_queue_item_status(current_queue_item.item_id, "canceled")
+
+ return CancelByBatchIDsResult(canceled=count)
+
+ def cancel_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> CancelByDestinationResult:
+ with self._db.transaction() as cursor:
+ current_queue_item = self.get_current(queue_id)
+
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE
+ queue_id == ?
+ AND destination == ?
+ AND status != 'canceled'
+ AND status != 'completed'
+ AND status != 'failed'
+ -- We will cancel the current item separately below - skip it here
+ AND status != 'in_progress'
+ {user_filter}
+ """
+ params = [queue_id, destination]
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ UPDATE session_queue
+ SET status = 'canceled',
+ status_sequence = COALESCE(status_sequence, 0) + 1
+ {where};
+ """,
+ tuple(params),
+ )
+
+ # Handle current item separately - check ownership if user_id is provided
+ if current_queue_item is not None and current_queue_item.destination == destination:
+ if user_id is None or current_queue_item.user_id == user_id:
+ self._set_queue_item_status(current_queue_item.item_id, "canceled")
+
+ return CancelByDestinationResult(canceled=count)
+
+ def delete_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> DeleteByDestinationResult:
+ with self._db.transaction() as cursor:
+ current_queue_item = self.get_current(queue_id)
+
+ # Handle current item separately - check ownership if user_id is provided
+ if current_queue_item is not None and current_queue_item.destination == destination:
+ if user_id is None or current_queue_item.user_id == user_id:
+ self.cancel_queue_item(current_queue_item.item_id)
+
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ params = [queue_id, destination]
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ WHERE
+ queue_id == ?
+ AND destination == ?
+ {user_filter}
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE FROM session_queue
+ WHERE
+ queue_id == ?
+ AND destination == ?
+ {user_filter}
+ """,
+ tuple(params),
+ )
+ return DeleteByDestinationResult(deleted=count)
+
+ def delete_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> DeleteAllExceptCurrentResult:
+ with self._db.transaction() as cursor:
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE
+ queue_id == ?
+ AND status == 'pending'
+ {user_filter}
+ """
+ params = [queue_id]
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ DELETE
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ return DeleteAllExceptCurrentResult(deleted=count)
+
+ def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
+ with self._db.transaction() as cursor:
+ current_queue_item = self.get_current(queue_id)
+ where = """--sql
+ WHERE
+ queue_id is ?
+ AND status != 'canceled'
+ AND status != 'completed'
+ AND status != 'failed'
+ -- We will cancel the current item separately below - skip it here
+ AND status != 'in_progress'
+ """
+ params = [queue_id]
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ UPDATE session_queue
+ SET status = 'canceled',
+ status_sequence = COALESCE(status_sequence, 0) + 1
+ {where};
+ """,
+ tuple(params),
+ )
+
+ if current_queue_item is not None and current_queue_item.queue_id == queue_id:
+ self._set_queue_item_status(current_queue_item.item_id, "canceled")
+ return CancelByQueueIDResult(canceled=count)
+
+ def cancel_all_except_current(self, queue_id: str, user_id: Optional[str] = None) -> CancelAllExceptCurrentResult:
+ with self._db.transaction() as cursor:
+ # Build WHERE clause with optional user_id filter
+ user_filter = "AND user_id = ?" if user_id is not None else ""
+ where = f"""--sql
+ WHERE
+ queue_id == ?
+ AND status == 'pending'
+ {user_filter}
+ """
+ params = [queue_id]
+ if user_id is not None:
+ params.append(user_id)
+
+ cursor.execute(
+ f"""--sql
+ SELECT COUNT(*)
+ FROM session_queue
+ {where};
+ """,
+ tuple(params),
+ )
+ count = cursor.fetchone()[0]
+ cursor.execute(
+ f"""--sql
+ UPDATE session_queue
+ SET status = 'canceled',
+ status_sequence = COALESCE(status_sequence, 0) + 1
+ {where};
+ """,
+ tuple(params),
+ )
+ return CancelAllExceptCurrentResult(canceled=count)
+
+ def get_queue_item(self, item_id: int) -> SessionQueueItem:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT
+ sq.*,
+ u.display_name as user_display_name,
+ u.email as user_email
+ FROM session_queue sq
+ LEFT JOIN users u ON sq.user_id = u.user_id
+ WHERE sq.item_id = ?
+ """,
+ (item_id,),
+ )
+ result = cast(Union[sqlite3.Row, None], cursor.fetchone())
+ if result is None:
+ raise SessionQueueItemNotFoundError(f"No queue item with id {item_id}")
+ return SessionQueueItem.queue_item_from_dict(dict(result))
+
+ def set_queue_item_session(self, item_id: int, session: GraphExecutionState) -> SessionQueueItem:
+ with self._db.transaction() as cursor:
+ # Use exclude_none so we don't end up with a bunch of nulls in the graph - this can cause validation errors
+ # when the graph is loaded. Graph execution occurs purely in memory - the session saved here is not referenced
+ # during execution.
+ session_json = session.model_dump_json(warnings=False, exclude_none=True)
+ cursor.execute(
+ """--sql
+ UPDATE session_queue
+ SET session = ?
+ WHERE item_id = ?
+ """,
+ (session_json, item_id),
+ )
+ return self.get_queue_item(item_id)
+
+ def list_queue_items(
+ self,
+ queue_id: str,
+ limit: int,
+ priority: int,
+ cursor: Optional[int] = None,
+ status: Optional[QUEUE_ITEM_STATUS] = None,
+ destination: Optional[str] = None,
+ ) -> CursorPaginatedResults[SessionQueueItem]:
+ with self._db.transaction() as cursor_:
+ item_id = cursor
+ query = """--sql
+ SELECT *
+ FROM session_queue
+ WHERE queue_id = ?
+ """
+ params: list[Union[str, int]] = [queue_id]
+
+ if status is not None:
+ query += """--sql
+ AND status = ?
+ """
+ params.append(status)
+
+ if destination is not None:
+ query += """---sql
+ AND destination = ?
+ """
+ params.append(destination)
+
+ if item_id is not None:
+ query += """--sql
+ AND (priority < ?) OR (priority = ? AND item_id > ?)
+ """
+ params.extend([priority, priority, item_id])
+
+ query += """--sql
+ ORDER BY
+ priority DESC,
+ item_id ASC
+ LIMIT ?
+ """
+ params.append(limit + 1)
+ cursor_.execute(query, params)
+ results = cast(list[sqlite3.Row], cursor_.fetchall())
+ items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
+ has_more = False
+ if len(items) > limit:
+ # remove the extra item
+ items.pop()
+ has_more = True
+ return CursorPaginatedResults(items=items, limit=limit, has_more=has_more)
+
+ def list_all_queue_items(
+ self,
+ queue_id: str,
+ destination: Optional[str] = None,
+ ) -> list[SessionQueueItem]:
+ """Gets all queue items that match the given parameters"""
+ with self._db.transaction() as cursor:
+ query = """--sql
+ SELECT
+ sq.*,
+ u.display_name as user_display_name,
+ u.email as user_email
+ FROM session_queue sq
+ LEFT JOIN users u ON sq.user_id = u.user_id
+ WHERE sq.queue_id = ?
+ """
+ params: list[Union[str, int]] = [queue_id]
+
+ if destination is not None:
+ query += """---sql
+ AND sq.destination = ?
+ """
+ params.append(destination)
+
+ query += """--sql
+ ORDER BY
+ sq.priority DESC,
+ sq.item_id ASC
+ ;
+ """
+ cursor.execute(query, params)
+ results = cast(list[sqlite3.Row], cursor.fetchall())
+ items = [SessionQueueItem.queue_item_from_dict(dict(result)) for result in results]
+ return items
+
+ def get_queue_item_ids(
+ self,
+ queue_id: str,
+ order_dir: SQLiteDirection = SQLiteDirection.Descending,
+ user_id: Optional[str] = None,
+ ) -> ItemIdsResult:
+ with self._db.transaction() as cursor_:
+ query = """--sql
+ SELECT item_id
+ FROM session_queue
+ WHERE queue_id = ?
+ """
+ query_params: list[str] = [queue_id]
+
+ if user_id is not None:
+ query += " AND user_id = ?"
+ query_params.append(user_id)
+
+ query += f" ORDER BY created_at {order_dir.value}"
+
+ cursor_.execute(query, query_params)
+ result = cast(list[sqlite3.Row], cursor_.fetchall())
+ item_ids = [row[0] for row in result]
+
+ return ItemIdsResult(item_ids=item_ids, total_count=len(item_ids))
+
+ def get_queue_status(
+ self,
+ queue_id: str,
+ user_id: Optional[str] = None,
+ acting_user_id: Optional[str] = None,
+ ) -> SessionQueueStatus:
+ with self._db.transaction() as cursor:
+ # When user_id is provided (non-admin), only count that user's items
+ if user_id is not None:
+ cursor.execute(
+ """--sql
+ SELECT status, count(*)
+ FROM session_queue
+ WHERE queue_id = ? AND user_id = ?
+ GROUP BY status
+ """,
+ (queue_id, user_id),
+ )
+ else:
+ cursor.execute(
+ """--sql
+ SELECT status, count(*)
+ FROM session_queue
+ WHERE queue_id = ?
+ GROUP BY status
+ """,
+ (queue_id,),
+ )
+ counts_result = cast(list[sqlite3.Row], cursor.fetchall())
+
+ current_item = self.get_current(queue_id=queue_id)
+ total = sum(row[1] or 0 for row in counts_result)
+ counts: dict[str, int] = {row[0]: row[1] for row in counts_result}
+
+ # Redaction is decided from the same current_item snapshot used to embed identifiers,
+ # so a concurrent transition (e.g. B finishing while A's status changes) cannot leave
+ # stale identifiers in the result. user_id (count filter) and acting_user_id
+ # (redaction) are independent: callers that need global counts but per-user redaction
+ # pass only acting_user_id; non-admin API callers pass user_id and inherit the same
+ # redaction by default.
+ owner_user_id = user_id if acting_user_id is None else acting_user_id
+ show_current_item = current_item is not None and (
+ owner_user_id is None or current_item.user_id == owner_user_id
+ )
+
+ return SessionQueueStatus(
+ queue_id=queue_id,
+ item_id=current_item.item_id if show_current_item else None,
+ session_id=current_item.session_id if show_current_item else None,
+ batch_id=current_item.batch_id if show_current_item else None,
+ pending=counts.get("pending", 0),
+ in_progress=counts.get("in_progress", 0),
+ completed=counts.get("completed", 0),
+ failed=counts.get("failed", 0),
+ canceled=counts.get("canceled", 0),
+ total=total,
+ )
+
+ def get_batch_status(self, queue_id: str, batch_id: str, user_id: Optional[str] = None) -> BatchStatus:
+ with self._db.transaction() as cursor:
+ query = """--sql
+ SELECT status, count(*), origin, destination
+ FROM session_queue
+ WHERE queue_id = ? AND batch_id = ?
+ """
+ params: list[str] = [queue_id, batch_id]
+ if user_id is not None:
+ query += " AND user_id = ?"
+ params.append(user_id)
+ query += " GROUP BY status"
+ cursor.execute(query, params)
+ result = cast(list[sqlite3.Row], cursor.fetchall())
+ total = sum(row[1] or 0 for row in result)
+ counts: dict[str, int] = {row[0]: row[1] for row in result}
+ origin = result[0]["origin"] if result else None
+ destination = result[0]["destination"] if result else None
+
+ return BatchStatus(
+ batch_id=batch_id,
+ origin=origin,
+ destination=destination,
+ queue_id=queue_id,
+ pending=counts.get("pending", 0),
+ in_progress=counts.get("in_progress", 0),
+ completed=counts.get("completed", 0),
+ failed=counts.get("failed", 0),
+ canceled=counts.get("canceled", 0),
+ total=total,
+ )
+
+ def get_counts_by_destination(
+ self, queue_id: str, destination: str, user_id: Optional[str] = None
+ ) -> SessionQueueCountsByDestination:
+ with self._db.transaction() as cursor:
+ query = """--sql
+ SELECT status, count(*)
+ FROM session_queue
+ WHERE queue_id = ? AND destination = ?
+ """
+ params: list[str] = [queue_id, destination]
+ if user_id is not None:
+ query += " AND user_id = ?"
+ params.append(user_id)
+ query += " GROUP BY status"
+ cursor.execute(query, params)
+ counts_result = cast(list[sqlite3.Row], cursor.fetchall())
+
+ total = sum(row[1] or 0 for row in counts_result)
+ counts: dict[str, int] = {row[0]: row[1] for row in counts_result}
+
+ return SessionQueueCountsByDestination(
+ queue_id=queue_id,
+ destination=destination,
+ pending=counts.get("pending", 0),
+ in_progress=counts.get("in_progress", 0),
+ completed=counts.get("completed", 0),
+ failed=counts.get("failed", 0),
+ canceled=counts.get("canceled", 0),
+ total=total,
+ )
+
+ def retry_items_by_id(self, queue_id: str, item_ids: list[int]) -> RetryItemsResult:
+ """Retries the given queue items"""
+ with self._db.transaction() as cursor:
+ values_to_insert: list[ValueToInsertTuple] = []
+ retried_item_ids: list[int] = []
+
+ for item_id in item_ids:
+ queue_item = self.get_queue_item(item_id)
+
+ if queue_item.status not in ("failed", "canceled"):
+ continue
+
+ retried_item_ids.append(item_id)
+
+ field_values_json = (
+ json.dumps(queue_item.field_values, default=to_jsonable_python) if queue_item.field_values else None
+ )
+ workflow_json = (
+ json.dumps(queue_item.workflow, default=to_jsonable_python) if queue_item.workflow else None
+ )
+ cloned_session = GraphExecutionState(graph=queue_item.session.graph)
+ cloned_session_json = cloned_session.model_dump_json(warnings=False, exclude_none=True)
+
+ retried_from_item_id = (
+ queue_item.retried_from_item_id
+ if queue_item.retried_from_item_id is not None
+ else queue_item.item_id
+ )
+
+ value_to_insert: ValueToInsertTuple = (
+ queue_item.queue_id,
+ cloned_session_json,
+ cloned_session.id,
+ queue_item.batch_id,
+ field_values_json,
+ queue_item.priority,
+ workflow_json,
+ queue_item.origin,
+ queue_item.destination,
+ retried_from_item_id,
+ queue_item.user_id,
+ )
+ values_to_insert.append(value_to_insert)
+
+ # TODO(psyche): Handle max queue size?
+
+ cursor.executemany(
+ """--sql
+ INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin, destination, retried_from_item_id, user_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ values_to_insert,
+ )
+
+ retry_result = RetryItemsResult(
+ queue_id=queue_id,
+ retried_item_ids=retried_item_ids,
+ )
+ self.__invoker.services.events.emit_queue_items_retried(retry_result)
+ return retry_result
diff --git a/invokeai/app/services/shared/README.md b/invokeai/app/services/shared/README.md
new file mode 100644
index 00000000000..f92b1f1ea2e
--- /dev/null
+++ b/invokeai/app/services/shared/README.md
@@ -0,0 +1,257 @@
+# InvokeAI Graph - Design Overview
+
+High-level design for the graph module. Focuses on responsibilities, data flow, and how traversal works.
+
+## 1) Purpose
+
+Provide a typed, acyclic workflow model (**Graph**) plus a runtime scheduler (**GraphExecutionState**) that expands
+iterator patterns, tracks readiness via indegree (the number of incoming edges to a node in the directed graph), and
+executes nodes in class-grouped batches. In normal execution, runtime expansion happens in a separate execution graph
+instead of mutating the source graph.
+
+## 2) Major Data Types
+
+### EdgeConnection
+
+- Fields: `node_id: str`, `field: str`.
+- Hashable; printed as `node.field` for readable diagnostics.
+
+### Edge
+
+- Fields: `source: EdgeConnection`, `destination: EdgeConnection`.
+- One directed connection from a specific output port to a specific input port.
+
+### AnyInvocation / AnyInvocationOutput
+
+- Pydantic wrappers that carry concrete invocation models and outputs.
+- No registry logic in this file; they are permissive containers for heterogeneous nodes.
+
+### IterateInvocation / CollectInvocation
+
+- Control nodes used by validation and execution:
+
+ - **IterateInvocation**: input `collection`, outputs include `item` (and index/total).
+ - **CollectInvocation**: many `item` inputs aggregated to one `collection` output.
+
+## 3) Graph (author-time model)
+
+A container for declared nodes and edges. Does **not** perform iteration expansion.
+
+### 3.1 Data
+
+- `nodes: dict[str, AnyInvocation]` - key must equal `node.id`.
+- `edges: list[Edge]` - zero or more.
+- Utility: `_get_input_edges(node_id, field?)`, `_get_output_edges(node_id, field?)` These scan `self.edges` (no
+ adjacency indices in the current code).
+
+### 3.2 Validation (`validate_self`)
+
+Runs a sequence of checks:
+
+1. **Node ID uniqueness** No duplicate IDs; map key equals `node.id`.
+
+1. **Endpoint existence** Source and destination node IDs must exist.
+
+1. **Port existence** Input ports must exist on the node class; output ports on the node's output model.
+
+1. **DAG constraint** Build a *flat* `DiGraph` (no runtime expansion) and assert acyclicity.
+
+1. **Type compatibility** `get_output_field_type` vs `get_input_field_type` and `are_connection_types_compatible`.
+
+1. **Iterator / collector structure** Enforce special rules:
+
+ - Iterator's input must be `collection`; its outgoing edges use `item`.
+ - Collector accepts many `item` inputs; outputs a single `collection`.
+ - Edge fan-in to a non-collector input is rejected.
+
+### 3.3 Edge admission (`_validate_edge`)
+
+Checks a single prospective edge before insertion:
+
+- Endpoints/ports exist.
+- Destination port is not already occupied unless it's a collector `item`.
+- Adding the edge to the flat DAG must keep it acyclic.
+- Iterator/collector constraints re-checked when the edge creates relevant patterns.
+
+### 3.4 Topology utilities
+
+- `nx_graph()` - DiGraph of declared nodes and edges.
+- `nx_graph_flat()` - "flattened" DAG (still author-time; no runtime copies). Used in validation and in `_prepare()`
+ during execution planning.
+
+### 3.5 Mutation helpers
+
+- `add_node`, `update_node` (preserve edges, rewrite endpoints if id changes), `delete_node`.
+- `add_edge`, `delete_edge` (with validation).
+
+## 4) GraphExecutionState (runtime)
+
+Holds the state for a single run. Keeps the source graph intact and materializes a separate execution graph.
+`GraphExecutionState` is still the public runtime entry point, but most execution behavior is now delegated to a small
+set of internal helper classes.
+
+The source graph is treated as stable during normal execution, but the runtime object still exposes guarded graph
+mutation helpers. Those helpers reject changes once the affected nodes have already been prepared or executed.
+
+### 4.1 Data
+
+- `graph: Graph` - source graph for the run; treated as stable during normal execution.
+- `execution_graph: Graph` - materialized runtime nodes/edges. This is mutable runtime state, not an immutable audit
+ log. Lazy `If` pruning may remove unselected input edges during execution, so persisted failed/completed session
+ snapshots can contain a structurally pruned execution graph. Retry paths rebuild from `graph`, not from a previously
+ persisted `execution_graph`.
+- `executed: set[str]`, `executed_history: list[str]`.
+- `results: dict[str, AnyInvocationOutput]`, `errors: dict[str, str]`.
+- `prepared_source_mapping: dict[str, str]` - exec id -> source id.
+- `source_prepared_mapping: dict[str, set[str]]` - source id -> exec ids.
+- `indegree: dict[str, int]` - unmet inputs per exec node.
+- Prepared exec metadata caches:
+ - source node id
+ - iteration path
+ - runtime state such as pending, ready, executed, or skipped
+- **Ready queues grouped by class** (private attrs): `_ready_queues: dict[class_name, deque[str]]`,
+ `_active_class: Optional[str]`. Optional `ready_order: list[str]` to prioritize classes.
+
+### 4.2 Core methods
+
+- `next()` Returns the next ready exec node. If none are ready, it asks the materializer to expand more source nodes and
+ then retries. Before returning a node, the runtime helper deep-copies inbound values into the node fields.
+- `complete(node_id, output)` Records the result, marks the exec node executed, marks the source node executed once all
+ of its prepared exec copies are done, then decrements downstream indegrees and enqueues newly ready nodes.
+
+### 4.3 Runtime helper classes
+
+`GraphExecutionState` now delegates most runtime behavior to internal helpers:
+
+- `_PreparedExecRegistry` Owns the relationship between source graph nodes and prepared execution graph nodes, plus
+ cached metadata such as iteration path and runtime state.
+- `_ExecutionMaterializer` Expands source graph nodes into concrete execution graph nodes when the scheduler runs out of
+ ready work. When matching prepared parents for a downstream exec node, skipped prepared exec nodes are ignored and
+ cannot be selected as live inputs.
+- `_ExecutionScheduler` Owns indegree transitions, ready queues, class batching, and downstream release on completion.
+- `_ExecutionRuntime` Owns iteration-path lookup and input hydration for prepared exec nodes.
+- `_IfBranchScheduler` Applies lazy `If` semantics by deferring branch-local work until the condition is known, then
+ releasing the selected branch and skipping the unselected branch.
+
+### 4.4 Preparation (`_prepare()`)
+
+- Build a flat DAG from the **source** graph.
+
+- Choose the **next source node** in topological order that:
+
+ 1. has not been prepared,
+ 1. if it is an iterator, *its inputs are already executed*,
+ 1. it has *no unexecuted iterator ancestors*.
+
+- If the node is a **CollectInvocation**: collapse all prepared parents into one mapping and create **one** exec node.
+
+- Otherwise: compute all combinations of prepared iterator ancestors. For each combination, choose the prepared parent
+ for each upstream by matching iterator ancestry, then create **one** exec node.
+
+- For each new exec node:
+
+ - Deep-copy the source node; assign a fresh ID (and `index` for iterators).
+ - Wire edges from chosen prepared parents.
+ - Set `indegree = number of unmet inputs` (i.e., parents not yet executed).
+ - Try to resolve any `If`-specific scheduling state.
+ - If the node is ready and not deferred by an unresolved `If`, enqueue it into its class queue.
+
+### 4.5 Readiness and batching
+
+- `_enqueue_if_ready(nid)` enqueues by class name only when `indegree == 0`, the node has not already executed, and the
+ node is not deferred by an unresolved `If`.
+- `_get_next_node()` drains the `_active_class` queue FIFO; when empty, selects the next nonempty class queue (by
+ `ready_order` if set, else alphabetical), and continues. Optional fairness knobs can limit batch size per class;
+ default is drain fully.
+
+#### 4.5.1 Indegree (what it is and how it's used)
+
+**Indegree** is the number of incoming edges to a node in the execution graph that are still unmet. In this engine:
+
+- For every materialized exec node, `indegree[node]` equals the count of its prerequisite parents that have **not**
+ finished yet.
+- A node is "ready" exactly when `indegree[node] == 0`; only then is it enqueued.
+- When a node completes, the scheduler decrements `indegree[child]` for each outgoing edge. Any child that reaches 0 is
+ enqueued.
+
+Example: edges `A->C`, `B->C`, `C->D`. Start: `A:0, B:0, C:2, D:1`. Run `A` -> `C:1`. Run `B` -> `C:0` -> enqueue `C`.
+Run `C` -> `D:0` -> enqueue `D`. Run `D` -> done.
+
+### 4.6 Input hydration (`_prepare_inputs()`)
+
+- For **CollectInvocation**: gather all incoming `item` values into `collection`, sorting inputs by iteration path so
+ collected results are stable across expanded iterations. Incoming `collection` values are merged first, then incoming
+ `item` values are appended.
+- For **IfInvocation**: hydrate only `condition` and the selected branch input. As a defensive guard against
+ inconsistent runtime or deserialized session state, the runtime raises if the selected input edge points at an exec
+ node with no stored runtime output. In normal scheduling this path should be unreachable.
+- For all others: deep-copy each incoming edge's value into the destination field. This prevents cross-node mutation
+ through shared references.
+
+### 4.7 Lazy `If` semantics
+
+`IfInvocation` now acts as a lazy branch boundary rather than a simple value multiplexer.
+
+- The `condition` input must resolve first.
+- Nodes that are exclusive to the true or false branch can remain deferred even when their indegree is zero.
+- Once the prepared `If` node resolves its condition:
+ - the selected branch is released
+ - the unselected branch is marked skipped
+ - unselected input edges on the prepared `If` exec node are pruned from the execution graph so they no longer
+ participate in downstream indegree accounting
+ - branch-exclusive ancestors of the unselected branch are never executed
+- Skipped branch-local exec nodes may still be treated as executed for scheduling purposes, but they do not create
+ entries in `results`.
+- Shared ancestors still execute if they are required by the selected branch or by any other live path in the graph.
+
+This behavior is implemented in the runtime scheduler, not in the invocation body itself.
+
+## 5) Traversal Summary
+
+1. Author builds a valid **Graph**.
+
+1. Create **GraphExecutionState** with that graph.
+
+1. Loop:
+
+ - `node = state.next()` -> may trigger `_prepare()` expansion.
+ - Execute node externally -> `output`.
+ - `state.complete(node.id, output)` -> updates indegrees, `If` state, and ready queues.
+
+1. Finish when `next()` returns `None`.
+
+In normal execution, all runtime expansion occurs in `execution_graph` with traceability back to source nodes.
+
+## 6) Invariants
+
+- Source **Graph** remains a DAG and type-consistent.
+- `execution_graph` remains a DAG.
+- Nodes are enqueued only when `indegree == 0` and they are not deferred by an unresolved `If`.
+- `results` and `errors` are keyed by **exec node id**.
+- Collectors aggregate `item` inputs and may also merge incoming `collection` inputs during runtime hydration.
+- Branch-exclusive nodes behind an unselected `If` branch are skipped, not failed.
+
+## 7) Extensibility
+
+- **New node types**: implement as Pydantic models with typed fields and outputs. Register per your invocation system;
+ this file accepts them as `AnyInvocation`.
+- **Scheduling policy**: adjust `ready_order` to batch by class; add a batch cap for fairness without changing
+ complexity.
+- **Dynamic behaviors** (future): can be added in `GraphExecutionState` by creating exec nodes and edges at `complete()`
+ time, as long as the DAG invariant holds.
+
+## 8) Error Model (selected)
+
+- `DuplicateNodeIdError`, `NodeAlreadyInGraphError`
+- `NodeNotFoundError`, `NodeFieldNotFoundError`
+- `InvalidEdgeError`, `CyclicalGraphError`
+- `NodeInputError` (raised when preparing inputs for execution)
+
+Messages favor short, precise diagnostics (node id, field, and failing condition).
+
+## 9) Rationale
+
+- **Two-graph approach** isolates authoring from execution expansion and keeps validation simple.
+- **Indegree + queues** gives O(1) scheduling decisions with clear batching semantics.
+- **Iterator/collector separation** keeps fan-out/fan-in explicit and testable.
+- **Deep-copy hydration** avoids incidental aliasing bugs between nodes.
diff --git a/invokeai/app/services/shared/__init__.py b/invokeai/app/services/shared/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py
new file mode 100644
index 00000000000..aa47c3b4bb5
--- /dev/null
+++ b/invokeai/app/services/shared/graph.py
@@ -0,0 +1,2045 @@
+# Copyright (c) 2022 Kyle Schouviller (https://github.com/kyle0654)
+
+import copy
+import itertools
+from collections import deque
+from dataclasses import dataclass
+from typing import Any, Deque, Iterable, Literal, Optional, Type, TypeVar, Union, get_args, get_origin
+
+import networkx as nx
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ GetCoreSchemaHandler,
+ GetJsonSchemaHandler,
+ PrivateAttr,
+ ValidationError,
+ field_validator,
+)
+from pydantic.fields import Field
+from pydantic.json_schema import JsonSchemaValue
+from pydantic_core import core_schema
+
+# Importing * is bad karma but needed here for node detection
+from invokeai.app.invocations import * # noqa: F401 F403
+from invokeai.app.invocations.baseinvocation import (
+ BaseInvocation,
+ BaseInvocationOutput,
+ InvocationRegistry,
+ invocation,
+ invocation_output,
+)
+from invokeai.app.invocations.fields import Input, InputField, OutputField, UIType
+from invokeai.app.invocations.logic import IfInvocation
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.app.util.misc import uuid_string
+
+# in 3.10 this would be "from types import NoneType"
+NoneType = type(None)
+
+# Port name constants
+ITEM_FIELD = "item"
+COLLECTION_FIELD = "collection"
+
+
+class EdgeConnection(BaseModel):
+ node_id: str = Field(description="The id of the node for this edge connection")
+ field: str = Field(description="The field for this connection")
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, self.__class__)
+ and getattr(other, "node_id", None) == self.node_id
+ and getattr(other, "field", None) == self.field
+ )
+
+ def __hash__(self):
+ return hash(f"{self.node_id}.{self.field}")
+
+
+class Edge(BaseModel):
+ source: EdgeConnection = Field(description="The connection for the edge's from node and field")
+ destination: EdgeConnection = Field(description="The connection for the edge's to node and field")
+
+ def __str__(self):
+ return f"{self.source.node_id}.{self.source.field} -> {self.destination.node_id}.{self.destination.field}"
+
+
+PreparedExecState = Literal["pending", "ready", "executed", "skipped"]
+
+
+@dataclass
+class _PreparedExecNodeMetadata:
+ """Cached metadata for a materialized execution node."""
+
+ source_node_id: str
+ iteration_path: Optional[tuple[int, ...]] = None
+ state: PreparedExecState = "pending"
+
+
+class _PreparedExecRegistry:
+ """Tracks prepared execution nodes and their relationship to source graph nodes."""
+
+ def __init__(
+ self,
+ prepared_source_mapping: dict[str, str],
+ source_prepared_mapping: dict[str, set[str]],
+ metadata: dict[str, _PreparedExecNodeMetadata],
+ ) -> None:
+ self._prepared_source_mapping = prepared_source_mapping
+ self._source_prepared_mapping = source_prepared_mapping
+ self._metadata = metadata
+
+ def register(self, exec_node_id: str, source_node_id: str) -> None:
+ self._prepared_source_mapping[exec_node_id] = source_node_id
+ self._metadata[exec_node_id] = _PreparedExecNodeMetadata(source_node_id=source_node_id)
+ if source_node_id not in self._source_prepared_mapping:
+ self._source_prepared_mapping[source_node_id] = set()
+ self._source_prepared_mapping[source_node_id].add(exec_node_id)
+
+ def get_metadata(self, exec_node_id: str) -> _PreparedExecNodeMetadata:
+ metadata = self._metadata.get(exec_node_id)
+ if metadata is None:
+ metadata = _PreparedExecNodeMetadata(source_node_id=self._prepared_source_mapping[exec_node_id])
+ self._metadata[exec_node_id] = metadata
+ return metadata
+
+ def get_source_node_id(self, exec_node_id: str) -> str:
+ metadata = self._metadata.get(exec_node_id)
+ if metadata is not None:
+ return metadata.source_node_id
+ return self._prepared_source_mapping[exec_node_id]
+
+ def get_prepared_ids(self, source_node_id: str) -> set[str]:
+ return self._source_prepared_mapping.get(source_node_id, set())
+
+ def set_state(self, exec_node_id: str, state: PreparedExecState) -> None:
+ self.get_metadata(exec_node_id).state = state
+
+ def get_iteration_path(self, exec_node_id: str) -> Optional[tuple[int, ...]]:
+ metadata = self._metadata.get(exec_node_id)
+ return metadata.iteration_path if metadata is not None else None
+
+ def set_iteration_path(self, exec_node_id: str, iteration_path: tuple[int, ...]) -> None:
+ self.get_metadata(exec_node_id).iteration_path = iteration_path
+
+
+class _IfBranchScheduler:
+ """Applies lazy `If` semantics by deferring, releasing, and skipping branch-local exec nodes."""
+
+ def __init__(self, state: "GraphExecutionState") -> None:
+ self._state = state
+
+ def _get_branch_input_sources(self, if_node_id: str, branch_field: str) -> set[str]:
+ return {e.source.node_id for e in self._state.graph._get_input_edges(if_node_id, branch_field)}
+
+ def _expand_with_ancestors(self, node_ids: set[str]) -> set[str]:
+ expanded = set(node_ids)
+ source_graph = self._state.graph.nx_graph_flat()
+ for node_id in list(expanded):
+ expanded.update(nx.ancestors(source_graph, node_id))
+ return expanded
+
+ def _node_outputs_stay_in_branch(
+ self, node_id: str, if_node_id: str, branch_field: str, branch_nodes: set[str]
+ ) -> bool:
+ output_edges = self._state.graph._get_output_edges(node_id)
+ return all(
+ edge.destination.node_id in branch_nodes
+ or (edge.destination.node_id == if_node_id and edge.destination.field == branch_field)
+ for edge in output_edges
+ )
+
+ def _prune_nonexclusive_branch_nodes(
+ self, if_node_id: str, branch_field: str, candidate_nodes: set[str]
+ ) -> set[str]:
+ exclusive_nodes = set(candidate_nodes)
+ changed = True
+ while changed:
+ changed = False
+ for node_id in list(exclusive_nodes):
+ if self._node_outputs_stay_in_branch(node_id, if_node_id, branch_field, exclusive_nodes):
+ continue
+ exclusive_nodes.remove(node_id)
+ changed = True
+ return exclusive_nodes
+
+ def _get_matching_prepared_if_ids(self, if_node_id: str, iteration_path: tuple[int, ...]) -> list[str]:
+ prepared_if_ids = self._state._prepared_registry().get_prepared_ids(if_node_id)
+ return [pid for pid in prepared_if_ids if self._state._get_iteration_path(pid) == iteration_path]
+
+ def _has_unresolved_matching_if(self, if_node_id: str, iteration_path: tuple[int, ...]) -> bool:
+ matching_prepared_if_ids = self._get_matching_prepared_if_ids(if_node_id, iteration_path)
+ if not matching_prepared_if_ids:
+ return True
+ return not all(pid in self._state._resolved_if_exec_branches for pid in matching_prepared_if_ids)
+
+ def _apply_condition_inputs(self, exec_node_id: str, node: IfInvocation) -> bool:
+ return self._state._apply_if_condition_inputs(exec_node_id, node)
+
+ def _get_selected_branch_fields(self, node: IfInvocation) -> tuple[str, str]:
+ selected_field = "true_input" if node.condition else "false_input"
+ unselected_field = "false_input" if node.condition else "true_input"
+ return selected_field, unselected_field
+
+ def _prune_unselected_if_inputs(self, exec_node_id: str, unselected_field: str) -> None:
+ for edge in self._state.execution_graph._get_input_edges(exec_node_id, unselected_field):
+ if edge.source.node_id not in self._state.executed:
+ if self._state.indegree[exec_node_id] == 0:
+ raise RuntimeError(f"indegree underflow for {exec_node_id} when pruning {unselected_field}")
+ self._state.indegree[exec_node_id] -= 1
+ self._state.execution_graph.delete_edge(edge)
+
+ def _apply_branch_resolution(
+ self,
+ exec_node_id: str,
+ iteration_path: tuple[int, ...],
+ exclusive_sources: dict[str, set[str]],
+ selected_field: str,
+ unselected_field: str,
+ ) -> None:
+ # This iterates over the stable prepared-source mapping while mutating per-exec runtime state such as ready
+ # queues, execution state, and prepared metadata. Branch resolution never adds or removes prepared exec nodes.
+ for prepared_id, prepared_source in self._state.prepared_source_mapping.items():
+ if prepared_id in self._state.executed:
+ continue
+ if self._state._get_iteration_path(prepared_id) != iteration_path:
+ continue
+ if prepared_source in exclusive_sources[selected_field]:
+ self._state._enqueue_if_ready(prepared_id)
+ elif prepared_source in exclusive_sources[unselected_field]:
+ self.mark_exec_node_skipped(prepared_id)
+
+ def get_branch_exclusive_sources(self, if_node_id: str) -> dict[str, set[str]]:
+ cached = self._state._if_branch_exclusive_sources.get(if_node_id)
+ if cached is not None:
+ return cached
+
+ branch_sources: dict[str, set[str]] = {}
+ for branch_field in ("true_input", "false_input"):
+ direct_inputs = self._get_branch_input_sources(if_node_id, branch_field)
+ candidate_nodes = self._expand_with_ancestors(direct_inputs)
+ branch_sources[branch_field] = self._prune_nonexclusive_branch_nodes(
+ if_node_id, branch_field, candidate_nodes
+ )
+
+ self._state._if_branch_exclusive_sources[if_node_id] = branch_sources
+ return branch_sources
+
+ def is_deferred_by_unresolved_if(self, exec_node_id: str) -> bool:
+ source_node_id = self._state._prepared_registry().get_source_node_id(exec_node_id)
+ iteration_path = self._state._get_iteration_path(exec_node_id)
+
+ for source_if_id, source_if_node in self._state.graph.nodes.items():
+ if not isinstance(source_if_node, IfInvocation):
+ continue
+
+ branches = self.get_branch_exclusive_sources(source_if_id)
+ if source_node_id not in branches["true_input"] and source_node_id not in branches["false_input"]:
+ continue
+
+ if self._has_unresolved_matching_if(source_if_id, iteration_path):
+ return True
+ return False
+
+ def mark_exec_node_skipped(self, exec_node_id: str) -> None:
+ state = self._state._get_prepared_exec_metadata(exec_node_id).state
+ if state in ("executed", "skipped"):
+ return
+
+ self._state._remove_from_ready_queues(exec_node_id)
+ self._state._set_prepared_exec_state(exec_node_id, "skipped")
+ self._state.executed.add(exec_node_id)
+
+ registry = self._state._prepared_registry()
+ source_node_id = registry.get_source_node_id(exec_node_id)
+ prepared_nodes = registry.get_prepared_ids(source_node_id)
+ if all(n in self._state.executed for n in prepared_nodes):
+ if source_node_id not in self._state.executed:
+ self._state.executed.add(source_node_id)
+ self._state.executed_history.append(source_node_id)
+
+ def try_resolve_if_node(self, exec_node_id: str) -> None:
+ if exec_node_id in self._state._resolved_if_exec_branches:
+ return
+ node = self._state.execution_graph.get_node(exec_node_id)
+ if not isinstance(node, IfInvocation):
+ return
+
+ if not self._apply_condition_inputs(exec_node_id, node):
+ return
+
+ selected_field, unselected_field = self._get_selected_branch_fields(node)
+ self._state._resolved_if_exec_branches[exec_node_id] = selected_field
+
+ source_if_node_id = self._state._prepared_registry().get_source_node_id(exec_node_id)
+ exclusive_sources = self.get_branch_exclusive_sources(source_if_node_id)
+
+ iteration_path = self._state._get_iteration_path(exec_node_id)
+ self._prune_unselected_if_inputs(exec_node_id, unselected_field)
+ self._apply_branch_resolution(exec_node_id, iteration_path, exclusive_sources, selected_field, unselected_field)
+ self._state._enqueue_if_ready(exec_node_id)
+
+
+class _ExecutionMaterializer:
+ """Expands source-graph nodes into concrete execution-graph nodes for the current runtime state.
+
+ `GraphExecutionState.next()` calls into this helper when no prepared exec node is ready. The materializer chooses
+ the next source node that can be expanded, creates the corresponding exec nodes in the execution graph, wires their
+ inputs, and initializes their scheduler state.
+ """
+
+ def __init__(self, state: "GraphExecutionState") -> None:
+ self._state = state
+
+ def _get_iterator_iteration_count(self, node_id: str, iteration_node_map: list[tuple[str, str]]) -> int:
+ input_collection_edge = next(iter(self._state.graph._get_input_edges(node_id, COLLECTION_FIELD)))
+ input_collection_prepared_node_id = next(
+ prepared_id
+ for source_id, prepared_id in iteration_node_map
+ if source_id == input_collection_edge.source.node_id
+ )
+ input_collection_output = self._state.results[input_collection_prepared_node_id]
+ input_collection = getattr(input_collection_output, input_collection_edge.source.field)
+ return len(input_collection)
+
+ def _get_new_node_iterations(
+ self, node: BaseInvocation, node_id: str, iteration_node_map: list[tuple[str, str]]
+ ) -> list[int]:
+ if not isinstance(node, IterateInvocation):
+ return [-1]
+
+ iteration_count = self._get_iterator_iteration_count(node_id, iteration_node_map)
+ if iteration_count == 0:
+ return []
+ return list(range(iteration_count))
+
+ def _build_execution_edges(self, node_id: str, iteration_node_map: list[tuple[str, str]]) -> list[Edge]:
+ input_edges = self._state.graph._get_input_edges(node_id)
+ new_edges: list[Edge] = []
+ for edge in input_edges:
+ matching_inputs = [
+ prepared_id for source_id, prepared_id in iteration_node_map if source_id == edge.source.node_id
+ ]
+ for input_node_id in matching_inputs:
+ new_edges.append(
+ Edge(
+ source=EdgeConnection(node_id=input_node_id, field=edge.source.field),
+ destination=EdgeConnection(node_id="", field=edge.destination.field),
+ )
+ )
+ return new_edges
+
+ def _create_execution_node_copy(self, node: BaseInvocation, node_id: str, iteration_index: int) -> BaseInvocation:
+ new_node = node.model_copy(deep=True)
+ new_node.id = uuid_string()
+
+ if isinstance(new_node, IterateInvocation):
+ new_node.index = iteration_index
+
+ self._state.execution_graph.add_node(new_node)
+ self._state._register_prepared_exec_node(new_node.id, node_id)
+ return new_node
+
+ def _attach_execution_edges(self, exec_node_id: str, new_edges: list[Edge]) -> None:
+ for edge in new_edges:
+ self._state.execution_graph.add_edge(
+ Edge(
+ source=edge.source,
+ destination=EdgeConnection(node_id=exec_node_id, field=edge.destination.field),
+ )
+ )
+
+ def _initialize_execution_node(self, exec_node_id: str) -> None:
+ inputs = self._state.execution_graph._get_input_edges(exec_node_id)
+ unmet = sum(1 for edge in inputs if edge.source.node_id not in self._state.executed)
+ self._state.indegree[exec_node_id] = unmet
+ self._state._try_resolve_if_node(exec_node_id)
+ self._state._enqueue_if_ready(exec_node_id)
+
+ def _get_collect_iteration_mappings(self, parent_node_ids: list[str]) -> list[tuple[str, str]]:
+ all_iteration_mappings: list[tuple[str, str]] = []
+ for source_node_id in parent_node_ids:
+ prepared_nodes = self._get_prepared_nodes_for_source(source_node_id)
+ all_iteration_mappings.extend((source_node_id, prepared_id) for prepared_id in prepared_nodes)
+ return all_iteration_mappings
+
+ def _get_parent_iteration_mappings(self, next_node_id: str, graph: nx.DiGraph) -> list[list[tuple[str, str]]]:
+ parent_node_ids = [source_id for source_id, _ in graph.in_edges(next_node_id)]
+ iterator_graph = self.iterator_graph(graph)
+ iterator_nodes = self.get_node_iterators(next_node_id, iterator_graph)
+ iterator_nodes_prepared = [list(self._state.source_prepared_mapping[node_id]) for node_id in iterator_nodes]
+ iterator_node_prepared_combinations = list(itertools.product(*iterator_nodes_prepared))
+
+ execution_graph = self._state.execution_graph.nx_graph_flat()
+ prepared_parent_mappings = [
+ [
+ (node_id, self.get_iteration_node(node_id, graph, execution_graph, prepared_iterators))
+ for node_id in parent_node_ids
+ ]
+ for prepared_iterators in iterator_node_prepared_combinations
+ ]
+ return [
+ mapping
+ for mapping in prepared_parent_mappings
+ if all(prepared_id is not None for _, prepared_id in mapping)
+ ]
+
+ def create_execution_node(self, node_id: str, iteration_node_map: list[tuple[str, str]]) -> list[str]:
+ """Prepares an iteration node and connects all edges, returning the new node id"""
+
+ node = self._state.graph.get_node(node_id)
+ iteration_indexes = self._get_new_node_iterations(node, node_id, iteration_node_map)
+ if not iteration_indexes:
+ return []
+
+ new_edges = self._build_execution_edges(node_id, iteration_node_map)
+ new_nodes: list[str] = []
+ for iteration_index in iteration_indexes:
+ new_node = self._create_execution_node_copy(node, node_id, iteration_index)
+ self._attach_execution_edges(new_node.id, new_edges)
+ self._initialize_execution_node(new_node.id)
+ new_nodes.append(new_node.id)
+
+ return new_nodes
+
+ def iterator_graph(self, base: Optional[nx.DiGraph] = None) -> nx.DiGraph:
+ """Gets a DiGraph with edges to collectors removed so an ancestor search produces all active iterators for any node"""
+ g = base.copy() if base is not None else self._state.graph.nx_graph_flat()
+ collectors = (
+ n for n in self._state.graph.nodes if isinstance(self._state.graph.get_node(n), CollectInvocation)
+ )
+ for c in collectors:
+ g.remove_edges_from(list(g.in_edges(c)))
+ return g
+
+ def get_node_iterators(self, node_id: str, it_graph: Optional[nx.DiGraph] = None) -> list[str]:
+ g = it_graph or self.iterator_graph()
+ return [n for n in nx.ancestors(g, node_id) if isinstance(self._state.graph.get_node(n), IterateInvocation)]
+
+ def _get_prepared_nodes_for_source(self, source_node_id: str) -> set[str]:
+ return {
+ exec_node_id
+ for exec_node_id in self._state.source_prepared_mapping[source_node_id]
+ if self._state._get_prepared_exec_metadata(exec_node_id).state != "skipped"
+ }
+
+ def _get_parent_iterator_exec_nodes(
+ self, source_node_id: str, graph: nx.DiGraph, prepared_iterator_nodes: list[str]
+ ) -> list[tuple[str, str]]:
+ iterator_source_node_mapping = [
+ (prepared_exec_node_id, self._state.prepared_source_mapping[prepared_exec_node_id])
+ for prepared_exec_node_id in prepared_iterator_nodes
+ ]
+ return [
+ iterator_mapping
+ for iterator_mapping in iterator_source_node_mapping
+ if nx.has_path(graph, iterator_mapping[1], source_node_id)
+ ]
+
+ def _matches_parent_iterators(
+ self, candidate_exec_node_id: str, parent_iterators: list[tuple[str, str]], execution_graph: nx.DiGraph
+ ) -> bool:
+ return all(
+ nx.has_path(execution_graph, parent_iterator_exec_id, candidate_exec_node_id)
+ for parent_iterator_exec_id, _ in parent_iterators
+ )
+
+ def _get_direct_prepared_iterator_match(
+ self,
+ prepared_nodes: set[str],
+ prepared_iterator_nodes: list[str],
+ parent_iterators: list[tuple[str, str]],
+ execution_graph: nx.DiGraph,
+ ) -> Optional[str]:
+ prepared_iterator = next((node_id for node_id in prepared_nodes if node_id in prepared_iterator_nodes), None)
+ if prepared_iterator is None:
+ return None
+ if self._matches_parent_iterators(prepared_iterator, parent_iterators, execution_graph):
+ return prepared_iterator
+ return None
+
+ def _find_prepared_node_matching_iterators(
+ self, prepared_nodes: set[str], parent_iterators: list[tuple[str, str]], execution_graph: nx.DiGraph
+ ) -> Optional[str]:
+ return next(
+ (
+ node_id
+ for node_id in prepared_nodes
+ if self._matches_parent_iterators(node_id, parent_iterators, execution_graph)
+ ),
+ None,
+ )
+
+ def get_iteration_node(
+ self,
+ source_node_id: str,
+ graph: nx.DiGraph,
+ execution_graph: nx.DiGraph,
+ prepared_iterator_nodes: list[str],
+ ) -> Optional[str]:
+ prepared_nodes = self._get_prepared_nodes_for_source(source_node_id)
+ if len(prepared_nodes) == 1 and not prepared_iterator_nodes:
+ return next(iter(prepared_nodes))
+
+ parent_iterators = self._get_parent_iterator_exec_nodes(source_node_id, graph, prepared_iterator_nodes)
+ if len(prepared_nodes) == 1:
+ prepared_node_id = next(iter(prepared_nodes))
+ if self._matches_parent_iterators(prepared_node_id, parent_iterators, execution_graph):
+ return prepared_node_id
+ return None
+
+ direct_iterator_match = self._get_direct_prepared_iterator_match(
+ prepared_nodes, prepared_iterator_nodes, parent_iterators, execution_graph
+ )
+ if direct_iterator_match is not None:
+ return direct_iterator_match
+
+ return self._find_prepared_node_matching_iterators(prepared_nodes, parent_iterators, execution_graph)
+
+ def prepare(self, base_g: Optional[nx.DiGraph] = None) -> Optional[str]:
+ g = base_g or self._state.graph.nx_graph_flat()
+ next_node_id = next(
+ (
+ node_id
+ for node_id in nx.topological_sort(g)
+ if node_id not in self._state.source_prepared_mapping
+ and (
+ not isinstance(self._state.graph.get_node(node_id), IterateInvocation)
+ or all(source_id in self._state.executed for source_id, _ in g.in_edges(node_id))
+ )
+ and not any(
+ isinstance(self._state.graph.get_node(ancestor_id), IterateInvocation)
+ and ancestor_id not in self._state.executed
+ for ancestor_id in nx.ancestors(g, node_id)
+ )
+ ),
+ None,
+ )
+
+ if next_node_id is None:
+ return None
+
+ next_node = self._state.graph.get_node(next_node_id)
+ new_node_ids: list[str] = []
+
+ if isinstance(next_node, CollectInvocation):
+ next_node_parents = [source_id for source_id, _ in g.in_edges(next_node_id)]
+ create_results = self.create_execution_node(
+ next_node_id, self._get_collect_iteration_mappings(next_node_parents)
+ )
+ if create_results is not None:
+ new_node_ids.extend(create_results)
+ else:
+ for iteration_mappings in self._get_parent_iteration_mappings(next_node_id, g):
+ create_results = self.create_execution_node(next_node_id, iteration_mappings)
+ if create_results is not None:
+ new_node_ids.extend(create_results)
+
+ return next(iter(new_node_ids), None)
+
+
+class _ExecutionScheduler:
+ """Owns ready-queue ordering and indegree-driven execution transitions."""
+
+ def __init__(self, state: "GraphExecutionState") -> None:
+ self._state = state
+
+ def _validate_exec_node_ready_state(self, exec_node_id: str) -> None:
+ if exec_node_id not in self._state.execution_graph.nodes:
+ raise KeyError(f"exec node {exec_node_id} missing from execution_graph")
+ if exec_node_id not in self._state.indegree:
+ raise KeyError(f"indegree missing for exec node {exec_node_id}")
+
+ def _should_skip_ready_enqueue(self, exec_node_id: str) -> bool:
+ return (
+ self._state.indegree[exec_node_id] != 0
+ or exec_node_id in self._state.executed
+ or self._state._is_deferred_by_unresolved_if(exec_node_id)
+ )
+
+ def _get_ready_queue(self, exec_node_id: str) -> Deque[str]:
+ node_obj = self._state.execution_graph.nodes[exec_node_id]
+ return self.queue_for(self._state._type_key(node_obj))
+
+ def _insert_ready_node(self, queue: Deque[str], exec_node_id: str) -> None:
+ exec_node_path = self._state._get_iteration_path(exec_node_id)
+ for i, existing in enumerate(queue):
+ if self._state._get_iteration_path(existing) > exec_node_path:
+ queue.insert(i, exec_node_id)
+ return
+ queue.append(exec_node_id)
+
+ def _record_completed_node(self, exec_node_id: str, output: BaseInvocationOutput) -> None:
+ self._state._set_prepared_exec_state(exec_node_id, "executed")
+ self._state.executed.add(exec_node_id)
+ self._state.results[exec_node_id] = output
+
+ def _mark_source_node_complete(self, exec_node_id: str) -> None:
+ registry = self._state._prepared_registry()
+ source_node_id = registry.get_source_node_id(exec_node_id)
+ prepared_nodes = registry.get_prepared_ids(source_node_id)
+ if all(node_id in self._state.executed for node_id in prepared_nodes):
+ self._state.executed.add(source_node_id)
+ self._state.executed_history.append(source_node_id)
+
+ def _decrement_child_indegree(self, child_exec_node_id: str, parent_exec_node_id: str) -> None:
+ if child_exec_node_id not in self._state.indegree:
+ raise KeyError(f"indegree missing for exec node {child_exec_node_id}")
+ if self._state.indegree[child_exec_node_id] == 0:
+ raise RuntimeError(f"indegree underflow for {child_exec_node_id} from parent {parent_exec_node_id}")
+ self._state.indegree[child_exec_node_id] -= 1
+
+ def _release_downstream_nodes(self, exec_node_id: str) -> None:
+ for edge in self._state.execution_graph._get_output_edges(exec_node_id):
+ child = edge.destination.node_id
+ self._decrement_child_indegree(child, exec_node_id)
+ self._state._try_resolve_if_node(child)
+ if self._state.indegree[child] == 0:
+ self.enqueue_if_ready(child)
+
+ def queue_for(self, cls_name: str) -> Deque[str]:
+ q = self._state._ready_queues.get(cls_name)
+ if q is None:
+ q = deque()
+ self._state._ready_queues[cls_name] = q
+ return q
+
+ def remove_from_ready_queues(self, exec_node_id: str) -> None:
+ for q in self._state._ready_queues.values():
+ try:
+ q.remove(exec_node_id)
+ except ValueError:
+ continue
+
+ def enqueue_if_ready(self, exec_node_id: str) -> None:
+ """Push exec_node_id to its class queue if unmet inputs == 0."""
+ self._validate_exec_node_ready_state(exec_node_id)
+ if self._should_skip_ready_enqueue(exec_node_id):
+ return
+ queue = self._get_ready_queue(exec_node_id)
+ if exec_node_id in queue:
+ return
+ self._state._set_prepared_exec_state(exec_node_id, "ready")
+ self._insert_ready_node(queue, exec_node_id)
+
+ def get_next_node(self) -> Optional[BaseInvocation]:
+ """Gets the next ready node: FIFO within class, drain class before switching."""
+ while True:
+ if self._state._active_class:
+ q = self._state._ready_queues.get(self._state._active_class)
+ while q:
+ exec_node_id = q.popleft()
+ if exec_node_id not in self._state.executed:
+ return self._state.execution_graph.nodes[exec_node_id]
+ self._state._active_class = None
+ continue
+
+ seen = set(self._state.ready_order)
+ next_class = next(
+ (cls_name for cls_name in self._state.ready_order if self._state._ready_queues.get(cls_name)),
+ None,
+ )
+ if next_class is None:
+ next_class = next(
+ (
+ cls_name
+ for cls_name in sorted(k for k in self._state._ready_queues.keys() if k not in seen)
+ if self._state._ready_queues[cls_name]
+ ),
+ None,
+ )
+ if next_class is None:
+ return None
+
+ self._state._active_class = next_class
+
+ def complete(self, exec_node_id: str, output: BaseInvocationOutput) -> None:
+ if exec_node_id not in self._state.execution_graph.nodes:
+ return
+
+ self._record_completed_node(exec_node_id, output)
+ self._mark_source_node_complete(exec_node_id)
+ self._release_downstream_nodes(exec_node_id)
+
+
+class _ExecutionRuntime:
+ """Provides runtime-only helpers such as iteration-path lookup and input hydration."""
+
+ def __init__(self, state: "GraphExecutionState") -> None:
+ self._state = state
+
+ def _get_cached_iteration_path(self, exec_node_id: str) -> Optional[tuple[int, ...]]:
+ registry = self._state._prepared_registry()
+ metadata_iteration_path = registry.get_iteration_path(exec_node_id)
+ if metadata_iteration_path is not None:
+ return metadata_iteration_path
+
+ return self._state._iteration_path_cache.get(exec_node_id)
+
+ def _get_iteration_source_node_id(self, exec_node_id: str) -> Optional[str]:
+ if exec_node_id not in self._state.prepared_source_mapping:
+ return None
+ return self._state._prepared_registry().get_source_node_id(exec_node_id)
+
+ def _get_ordered_iterator_sources(self, source_node_id: str) -> list[str]:
+ iterator_graph = self._state._iterator_graph(self._state.graph.nx_graph())
+ iterator_sources = [
+ node_id
+ for node_id in nx.ancestors(iterator_graph, source_node_id)
+ if isinstance(self._state.graph.get_node(node_id), IterateInvocation)
+ ]
+
+ topo = list(nx.topological_sort(iterator_graph))
+ topo_index = {node_id: i for i, node_id in enumerate(topo)}
+ iterator_sources.sort(key=lambda node_id: topo_index.get(node_id, 0))
+ return iterator_sources
+
+ def _get_iterator_exec_id(
+ self, iterator_source_id: str, exec_node_id: str, execution_graph: nx.DiGraph
+ ) -> Optional[str]:
+ prepared = self._state.source_prepared_mapping.get(iterator_source_id)
+ if not prepared:
+ return None
+ return next((pid for pid in prepared if nx.has_path(execution_graph, pid, exec_node_id)), None)
+
+ def _build_iteration_path(self, exec_node_id: str, source_node_id: str) -> tuple[int, ...]:
+ iterator_sources = self._get_ordered_iterator_sources(source_node_id)
+ execution_graph = self._state.execution_graph.nx_graph()
+ path: list[int] = []
+ for iterator_source_id in iterator_sources:
+ iterator_exec_id = self._get_iterator_exec_id(iterator_source_id, exec_node_id, execution_graph)
+ if iterator_exec_id is None:
+ continue
+ iterator_node = self._state.execution_graph.nodes.get(iterator_exec_id)
+ if isinstance(iterator_node, IterateInvocation):
+ path.append(iterator_node.index)
+
+ node_obj = self._state.execution_graph.nodes.get(exec_node_id)
+ if isinstance(node_obj, IterateInvocation):
+ path.append(node_obj.index)
+
+ return tuple(path)
+
+ def _cache_iteration_path(self, exec_node_id: str, iteration_path: tuple[int, ...]) -> tuple[int, ...]:
+ self._state._iteration_path_cache[exec_node_id] = iteration_path
+ self._state._prepared_registry().set_iteration_path(exec_node_id, iteration_path)
+ return iteration_path
+
+ def get_iteration_path(self, exec_node_id: str) -> tuple[int, ...]:
+ """Best-effort outer->inner iteration indices for an execution node, stopping at collectors."""
+ cached = self._get_cached_iteration_path(exec_node_id)
+ if cached is not None:
+ return cached
+
+ source_node_id = self._get_iteration_source_node_id(exec_node_id)
+ if source_node_id is None:
+ return self._cache_iteration_path(exec_node_id, ())
+
+ return self._cache_iteration_path(exec_node_id, self._build_iteration_path(exec_node_id, source_node_id))
+
+ def _sort_collect_input_edges(self, input_edges: list[Edge], field_name: str) -> list[Edge]:
+ matching_edges = [edge for edge in input_edges if edge.destination.field == field_name]
+ matching_edges.sort(key=lambda edge: (self.get_iteration_path(edge.source.node_id), edge.source.node_id))
+ return matching_edges
+
+ def _get_copied_result_value(self, edge: Edge) -> Any:
+ return copydeep(getattr(self._state.results[edge.source.node_id], edge.source.field))
+
+ def _try_get_copied_result_value(self, edge: Edge) -> tuple[bool, Any]:
+ source_output = self._state.results.get(edge.source.node_id)
+ if source_output is None:
+ return False, None
+ return True, copydeep(getattr(source_output, edge.source.field))
+
+ def _build_collect_collection(self, input_edges: list[Edge]) -> list[Any]:
+ item_edges = self._sort_collect_input_edges(input_edges, ITEM_FIELD)
+ collection_edges = self._sort_collect_input_edges(input_edges, COLLECTION_FIELD)
+
+ output_collection = []
+ for edge in collection_edges:
+ source_value = self._get_copied_result_value(edge)
+ if isinstance(source_value, list):
+ output_collection.extend(source_value)
+ else:
+ output_collection.append(source_value)
+ output_collection.extend(self._get_copied_result_value(edge) for edge in item_edges)
+ return output_collection
+
+ def _set_node_inputs(
+ self, node: BaseInvocation, input_edges: list[Edge], allowed_fields: Optional[set[str]] = None
+ ) -> None:
+ for edge in input_edges:
+ if allowed_fields is not None and edge.destination.field not in allowed_fields:
+ continue
+ setattr(node, edge.destination.field, self._get_copied_result_value(edge))
+
+ def _prepare_collect_inputs(self, node: "CollectInvocation", input_edges: list[Edge]) -> None:
+ node.collection = self._build_collect_collection(input_edges)
+
+ def _prepare_if_inputs(self, node: IfInvocation, input_edges: list[Edge]) -> None:
+ selected_field = self._state._resolved_if_exec_branches.get(node.id)
+ allowed_fields = {"condition", selected_field} if selected_field is not None else {"condition"}
+
+ for edge in input_edges:
+ if edge.destination.field not in allowed_fields:
+ continue
+
+ found_value, copied_value = self._try_get_copied_result_value(edge)
+ if not found_value:
+ iteration_path = self._state._get_iteration_path(node.id)
+ raise RuntimeError(
+ "IfInvocation selected input edge points at an exec node with no stored result output: "
+ f"if_exec_id={node.id}, source_exec_id={edge.source.node_id}, iteration_path={iteration_path}"
+ )
+
+ setattr(node, edge.destination.field, copied_value)
+
+ def _prepare_default_inputs(self, node: BaseInvocation, input_edges: list[Edge]) -> None:
+ self._set_node_inputs(node, input_edges)
+
+ def prepare_inputs(self, node: BaseInvocation) -> None:
+ input_edges = self._state.execution_graph._get_input_edges(node.id)
+
+ if isinstance(node, CollectInvocation):
+ self._prepare_collect_inputs(node, input_edges)
+ return
+
+ if isinstance(node, IfInvocation):
+ self._prepare_if_inputs(node, input_edges)
+ return
+
+ self._prepare_default_inputs(node, input_edges)
+
+
+def get_output_field_type(node: BaseInvocation, field: str) -> Any:
+ # TODO(psyche): This is awkward - if field_info is None, it means the field is not defined in the output, which
+ # really should raise. The consumers of this utility expect it to never raise, and return None instead. Fixing this
+ # would require some fairly significant changes and I don't want risk breaking anything.
+ try:
+ invocation_class = type(node)
+ invocation_output_class = invocation_class.get_output_annotation()
+ field_info = invocation_output_class.model_fields.get(field)
+ assert field_info is not None, f"Output field '{field}' not found in {invocation_output_class.get_type()}"
+ output_field_type = field_info.annotation
+ return output_field_type
+ except Exception:
+ return None
+
+
+def get_input_field_type(node: BaseInvocation, field: str) -> Any:
+ # TODO(psyche): This is awkward - if field_info is None, it means the field is not defined in the output, which
+ # really should raise. The consumers of this utility expect it to never raise, and return None instead. Fixing this
+ # would require some fairly significant changes and I don't want risk breaking anything.
+ try:
+ invocation_class = type(node)
+ field_info = invocation_class.model_fields.get(field)
+ assert field_info is not None, f"Input field '{field}' not found in {invocation_class.get_type()}"
+ input_field_type = field_info.annotation
+ return input_field_type
+ except Exception:
+ return None
+
+
+def is_union_subtype(t1, t2):
+ t1_args = get_args(t1)
+ t2_args = get_args(t2)
+ if not t1_args:
+ # t1 is a single type
+ return t1 in t2_args
+ else:
+ # t1 is a Union, check that all of its types are in t2_args
+ return all(arg in t2_args for arg in t1_args)
+
+
+def is_list_or_contains_list(t):
+ t_args = get_args(t)
+
+ # If the type is a List
+ if get_origin(t) is list:
+ return True
+
+ # If the type is a Union
+ elif t_args:
+ # Check if any of the types in the Union is a List
+ for arg in t_args:
+ if get_origin(arg) is list:
+ return True
+ return False
+
+
+def is_any(t: Any) -> bool:
+ return t == Any or Any in get_args(t)
+
+
+def extract_collection_item_types(t: Any) -> set[Any]:
+ """Extracts list item types from a collection annotation, including unions containing list branches."""
+ if is_any(t):
+ return {Any}
+
+ if get_origin(t) is list:
+ return {arg for arg in get_args(t) if arg != NoneType}
+
+ item_types: set[Any] = set()
+ for arg in get_args(t):
+ if is_any(arg):
+ item_types.add(Any)
+ elif get_origin(arg) is list:
+ item_types.update(item_arg for item_arg in get_args(arg) if item_arg != NoneType)
+ return item_types
+
+
+def are_connection_types_compatible(from_type: Any, to_type: Any) -> bool:
+ if not from_type or not to_type:
+ return False
+
+ # Ports are compatible
+ if from_type == to_type or is_any(from_type) or is_any(to_type):
+ return True
+
+ if from_type in get_args(to_type):
+ return True
+
+ if to_type in get_args(from_type):
+ return True
+
+ # allow int -> float, pydantic will cast for us
+ if from_type is int and to_type is float:
+ return True
+
+ # allow int|float -> str, pydantic will cast for us
+ if (from_type is int or from_type is float) and to_type is str:
+ return True
+
+ # Prefer issubclass when both are real classes
+ try:
+ if isinstance(from_type, type) and isinstance(to_type, type):
+ return issubclass(from_type, to_type)
+ except TypeError:
+ pass
+
+ # Union-to-Union (or Union-to-non-Union) handling
+ return is_union_subtype(from_type, to_type)
+
+
+def are_connections_compatible(
+ from_node: BaseInvocation, from_field: str, to_node: BaseInvocation, to_field: str
+) -> bool:
+ """Determines if a connection between fields of two nodes is compatible."""
+
+ # TODO: handle iterators and collectors
+ from_type = get_output_field_type(from_node, from_field)
+ to_type = get_input_field_type(to_node, to_field)
+
+ return are_connection_types_compatible(from_type, to_type)
+
+
+T = TypeVar("T")
+
+
+def copydeep(obj: T) -> T:
+ """Deep-copies an object. If it is a pydantic model, use the model's copy method."""
+ if isinstance(obj, BaseModel):
+ return obj.model_copy(deep=True)
+ return copy.deepcopy(obj)
+
+
+class NodeAlreadyInGraphError(ValueError):
+ pass
+
+
+class InvalidEdgeError(ValueError):
+ pass
+
+
+class NodeNotFoundError(ValueError):
+ pass
+
+
+class NodeAlreadyExecutedError(ValueError):
+ pass
+
+
+class DuplicateNodeIdError(ValueError):
+ pass
+
+
+class NodeFieldNotFoundError(ValueError):
+ pass
+
+
+class NodeIdMismatchError(ValueError):
+ pass
+
+
+class CyclicalGraphError(ValueError):
+ pass
+
+
+class UnknownGraphValidationError(ValueError):
+ pass
+
+
+class NodeInputError(ValueError):
+ """Raised when a node fails preparation. This occurs when a node's inputs are being set from its incomers, but an
+ input fails validation.
+
+ Attributes:
+ node: The node that failed preparation. Note: only successfully set fields will be accurate. Review the error to
+ determine which field caused the failure.
+ """
+
+ def __init__(self, node: BaseInvocation, e: ValidationError):
+ self.original_error = e
+ self.node = node
+ # When preparing a node, we set each input one-at-a-time. We may thus safely assume that the first error
+ # represents the first input that failed.
+ self.failed_input = loc_to_dot_sep(e.errors()[0]["loc"])
+ super().__init__(f"Node {node.id} has invalid incoming input for {self.failed_input}")
+
+
+def loc_to_dot_sep(loc: tuple[Union[str, int], ...]) -> str:
+ """Helper to pretty-print pydantic error locations as dot-separated strings.
+ Taken from https://docs.pydantic.dev/latest/errors/errors/#customize-error-messages
+ """
+ path = ""
+ for i, x in enumerate(loc):
+ if isinstance(x, str):
+ if i > 0:
+ path += "."
+ path += x
+ else:
+ path += f"[{x}]"
+ return path
+
+
+@invocation_output("iterate_output")
+class IterateInvocationOutput(BaseInvocationOutput):
+ """Used to connect iteration outputs. Will be expanded to a specific output."""
+
+ item: Any = OutputField(
+ description="The item being iterated over", title="Collection Item", ui_type=UIType._CollectionItem
+ )
+ index: int = OutputField(description="The index of the item", title="Index")
+ total: int = OutputField(description="The total number of items", title="Total")
+
+
+# TODO: Fill this out and move to invocations
+@invocation("iterate", version="1.1.0")
+class IterateInvocation(BaseInvocation):
+ """Iterates over a list of items"""
+
+ collection: list[Any] = InputField(
+ description="The list of items to iterate over", default=[], ui_type=UIType._Collection
+ )
+ index: int = InputField(description="The index, will be provided on executed iterators", default=0, ui_hidden=True)
+
+ def invoke(self, context: InvocationContext) -> IterateInvocationOutput:
+ """Produces the outputs as values"""
+ return IterateInvocationOutput(item=self.collection[self.index], index=self.index, total=len(self.collection))
+
+
+@invocation_output("collect_output")
+class CollectInvocationOutput(BaseInvocationOutput):
+ collection: list[Any] = OutputField(
+ description="The collection of input items", title="Collection", ui_type=UIType._Collection
+ )
+
+
+@invocation("collect", version="1.1.0")
+class CollectInvocation(BaseInvocation):
+ """Collects values into a collection"""
+
+ item: Optional[Any] = InputField(
+ default=None,
+ description="The item to collect (all inputs must be of the same type)",
+ ui_type=UIType._CollectionItem,
+ title="Collection Item",
+ input=Input.Connection,
+ )
+ collection: list[Any] = InputField(
+ description="An optional collection to append to",
+ default=[],
+ ui_type=UIType._Collection,
+ input=Input.Connection,
+ )
+
+ def invoke(self, context: InvocationContext) -> CollectInvocationOutput:
+ """Invoke with provided services and return outputs."""
+ return CollectInvocationOutput(collection=copy.copy(self.collection))
+
+
+class AnyInvocation(BaseInvocation):
+ @classmethod
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
+ def validate_invocation(v: Any) -> "AnyInvocation":
+ return InvocationRegistry.get_invocation_typeadapter().validate_python(v)
+
+ return core_schema.no_info_plain_validator_function(validate_invocation)
+
+ @classmethod
+ def __get_pydantic_json_schema__(
+ cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
+ ) -> JsonSchemaValue:
+ # Nodes are too powerful, we have to make our own OpenAPI schema manually
+ # No but really, because the schema is dynamic depending on loaded nodes, we need to generate it manually
+ oneOf: list[dict[str, str]] = []
+ names = [i.__name__ for i in InvocationRegistry.get_invocation_classes()]
+ for name in sorted(names):
+ oneOf.append({"$ref": f"#/components/schemas/{name}"})
+ return {"oneOf": oneOf}
+
+
+class AnyInvocationOutput(BaseInvocationOutput):
+ @classmethod
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler):
+ def validate_invocation_output(v: Any) -> "AnyInvocationOutput":
+ return InvocationRegistry.get_output_typeadapter().validate_python(v)
+
+ return core_schema.no_info_plain_validator_function(validate_invocation_output)
+
+ @classmethod
+ def __get_pydantic_json_schema__(
+ cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
+ ) -> JsonSchemaValue:
+ # Nodes are too powerful, we have to make our own OpenAPI schema manually
+ # No but really, because the schema is dynamic depending on loaded nodes, we need to generate it manually
+
+ oneOf: list[dict[str, str]] = []
+ names = [i.__name__ for i in InvocationRegistry.get_output_classes()]
+ for name in sorted(names):
+ oneOf.append({"$ref": f"#/components/schemas/{name}"})
+ return {"oneOf": oneOf}
+
+
+class Graph(BaseModel):
+ """A validated invocation graph made of nodes and typed edges."""
+
+ id: str = Field(description="The id of this graph", default_factory=uuid_string)
+ # TODO: use a list (and never use dict in a BaseModel) because pydantic/fastapi hates me
+ nodes: dict[str, AnyInvocation] = Field(description="The nodes in this graph", default_factory=dict)
+ edges: list[Edge] = Field(
+ description="The connections between nodes and their fields in this graph",
+ default_factory=list,
+ )
+
+ def add_node(self, node: BaseInvocation) -> None:
+ """Adds a node to a graph
+
+ :raises NodeAlreadyInGraphError: the node is already present in the graph.
+ """
+
+ if node.id in self.nodes:
+ raise NodeAlreadyInGraphError()
+
+ self.nodes[node.id] = node
+
+ def delete_node(self, node_id: str) -> None:
+ """Deletes a node from a graph"""
+
+ try:
+ # Delete edges for this node
+ input_edges = self._get_input_edges(node_id)
+ output_edges = self._get_output_edges(node_id)
+
+ for edge in input_edges:
+ self.delete_edge(edge)
+
+ for edge in output_edges:
+ self.delete_edge(edge)
+
+ del self.nodes[node_id]
+
+ except NodeNotFoundError:
+ pass # Ignore, not doesn't exist (should this throw?)
+
+ def add_edge(self, edge: Edge) -> None:
+ """Adds an edge to a graph
+
+ :raises InvalidEdgeError: the provided edge is invalid.
+ """
+
+ self._validate_edge(edge)
+ if edge not in self.edges:
+ self.edges.append(edge)
+ else:
+ raise InvalidEdgeError()
+
+ def delete_edge(self, edge: Edge) -> None:
+ """Deletes an edge from a graph"""
+
+ try:
+ self.edges.remove(edge)
+ except ValueError:
+ pass
+
+ def _validate_unique_node_ids(self) -> None:
+ node_ids = [n.id for n in self.nodes.values()]
+ seen = set()
+ duplicate_node_ids = {nid for nid in node_ids if (nid in seen) or seen.add(nid)}
+ if duplicate_node_ids:
+ raise DuplicateNodeIdError(f"Node ids must be unique, found duplicates {duplicate_node_ids}")
+
+ def _validate_node_id_mapping(self) -> None:
+ for node_dict_id, node in self.nodes.items():
+ if node_dict_id != node.id:
+ raise NodeIdMismatchError(f"Node ids must match, got {node_dict_id} and {node.id}")
+
+ def _validate_edge_nodes_and_fields(self) -> None:
+ for edge in self.edges:
+ source_node = self.nodes.get(edge.source.node_id, None)
+ if source_node is None:
+ raise NodeNotFoundError(f"Edge source node {edge.source.node_id} does not exist in the graph")
+
+ destination_node = self.nodes.get(edge.destination.node_id, None)
+ if destination_node is None:
+ raise NodeNotFoundError(f"Edge destination node {edge.destination.node_id} does not exist in the graph")
+
+ if edge.source.field not in source_node.get_output_annotation().model_fields:
+ raise NodeFieldNotFoundError(
+ f"Edge source field {edge.source.field} does not exist in node {edge.source.node_id}"
+ )
+
+ if edge.destination.field not in type(destination_node).model_fields:
+ raise NodeFieldNotFoundError(
+ f"Edge destination field {edge.destination.field} does not exist in node {edge.destination.node_id}"
+ )
+
+ def _validate_graph_is_acyclic(self) -> None:
+ graph = self.nx_graph_flat()
+ if not nx.is_directed_acyclic_graph(graph):
+ raise CyclicalGraphError("Graph contains cycles")
+
+ def _validate_edge_type_compatibility(self) -> None:
+ for edge in self.edges:
+ if not are_connections_compatible(
+ self.get_node(edge.source.node_id),
+ edge.source.field,
+ self.get_node(edge.destination.node_id),
+ edge.destination.field,
+ ):
+ raise InvalidEdgeError(f"Edge source and target types do not match ({edge})")
+
+ def _validate_special_nodes(self) -> None:
+ # TODO: may need to validate all iterators & collectors in subgraphs so edge connections in parent graphs will be available
+ for node in self.nodes.values():
+ if isinstance(node, IterateInvocation):
+ err = self._is_iterator_connection_valid(node.id)
+ if err is not None:
+ raise InvalidEdgeError(f"Invalid iterator node ({node.id}): {err}")
+ if isinstance(node, CollectInvocation):
+ err = self._is_collector_connection_valid(node.id)
+ if err is not None:
+ raise InvalidEdgeError(f"Invalid collector node ({node.id}): {err}")
+
+ def validate_self(self) -> None:
+ """
+ Validates the graph.
+
+ Raises an exception if the graph is invalid:
+ - `DuplicateNodeIdError`
+ - `NodeIdMismatchError`
+ - `InvalidSubGraphError`
+ - `NodeNotFoundError`
+ - `NodeFieldNotFoundError`
+ - `CyclicalGraphError`
+ - `InvalidEdgeError`
+ """
+
+ self._validate_unique_node_ids()
+ self._validate_node_id_mapping()
+ self._validate_edge_nodes_and_fields()
+ self._validate_graph_is_acyclic()
+ self._validate_edge_type_compatibility()
+ self._validate_special_nodes()
+ return None
+
+ def is_valid(self) -> bool:
+ """
+ Checks if the graph is valid.
+
+ Raises `UnknownGraphValidationError` if there is a problem validating the graph (not a validation error).
+ """
+ try:
+ self.validate_self()
+ return True
+ except (
+ DuplicateNodeIdError,
+ NodeIdMismatchError,
+ NodeNotFoundError,
+ NodeFieldNotFoundError,
+ CyclicalGraphError,
+ InvalidEdgeError,
+ ):
+ return False
+ except Exception as e:
+ raise UnknownGraphValidationError(f"Problem validating graph {e}") from e
+
+ def _is_destination_field_Any(self, edge: Edge) -> bool:
+ """Checks if the destination field for an edge is of type typing.Any"""
+ return get_input_field_type(self.get_node(edge.destination.node_id), edge.destination.field) == Any
+
+ def _is_destination_field_list_of_Any(self, edge: Edge) -> bool:
+ """Checks if the destination field for an edge is of type typing.Any"""
+ return get_input_field_type(self.get_node(edge.destination.node_id), edge.destination.field) == list[Any]
+
+ def _get_edge_nodes(self, edge: Edge) -> tuple[BaseInvocation, BaseInvocation]:
+ try:
+ return self.get_node(edge.source.node_id), self.get_node(edge.destination.node_id)
+ except NodeNotFoundError:
+ raise InvalidEdgeError(f"One or both nodes don't exist ({edge})")
+
+ def _validate_edge_destination_uniqueness(self, edge: Edge, destination_node: BaseInvocation) -> None:
+ input_edges = self._get_input_edges(edge.destination.node_id, edge.destination.field)
+ if len(input_edges) > 0 and (
+ not isinstance(destination_node, CollectInvocation) or edge.destination.field != ITEM_FIELD
+ ):
+ raise InvalidEdgeError(f"Edge already exists ({edge})")
+
+ def _validate_edge_would_not_create_cycle(self, edge: Edge) -> None:
+ graph = self.nx_graph_flat()
+ graph.add_edge(edge.source.node_id, edge.destination.node_id)
+ if not nx.is_directed_acyclic_graph(graph):
+ raise InvalidEdgeError(f"Edge creates a cycle in the graph ({edge})")
+
+ def _validate_edge_field_compatibility(
+ self, edge: Edge, source_node: BaseInvocation, destination_node: BaseInvocation
+ ) -> None:
+ if not are_connections_compatible(source_node, edge.source.field, destination_node, edge.destination.field):
+ raise InvalidEdgeError(f"Field types are incompatible ({edge})")
+
+ def _validate_iterator_edge_rules(
+ self, edge: Edge, source_node: BaseInvocation, destination_node: BaseInvocation
+ ) -> None:
+ if isinstance(destination_node, IterateInvocation) and edge.destination.field == COLLECTION_FIELD:
+ err = self._is_iterator_connection_valid(edge.destination.node_id, new_input=edge.source)
+ if err is not None:
+ raise InvalidEdgeError(f"Iterator input type does not match iterator output type ({edge}): {err}")
+
+ if isinstance(source_node, IterateInvocation) and edge.source.field == ITEM_FIELD:
+ err = self._is_iterator_connection_valid(edge.source.node_id, new_output=edge.destination)
+ if err is not None:
+ raise InvalidEdgeError(f"Iterator output type does not match iterator input type ({edge}): {err}")
+
+ def _validate_collector_edge_rules(
+ self, edge: Edge, source_node: BaseInvocation, destination_node: BaseInvocation
+ ) -> None:
+ if isinstance(destination_node, CollectInvocation) and edge.destination.field in (ITEM_FIELD, COLLECTION_FIELD):
+ err = self._is_collector_connection_valid(
+ edge.destination.node_id, new_input=edge.source, new_input_field=edge.destination.field
+ )
+ if err is not None:
+ raise InvalidEdgeError(f"Collector output type does not match collector input type ({edge}): {err}")
+
+ if (
+ isinstance(source_node, CollectInvocation)
+ and edge.source.field == COLLECTION_FIELD
+ and not self._is_destination_field_list_of_Any(edge)
+ and not self._is_destination_field_Any(edge)
+ ):
+ err = self._is_collector_connection_valid(edge.source.node_id, new_output=edge.destination)
+ if err is not None:
+ raise InvalidEdgeError(f"Collector input type does not match collector output type ({edge}): {err}")
+
+ def _validate_edge(self, edge: Edge):
+ """Validates that a new edge doesn't create a cycle in the graph"""
+ source_node, destination_node = self._get_edge_nodes(edge)
+ self._validate_edge_destination_uniqueness(edge, destination_node)
+ self._validate_edge_would_not_create_cycle(edge)
+ self._validate_edge_field_compatibility(edge, source_node, destination_node)
+ self._validate_iterator_edge_rules(edge, source_node, destination_node)
+ self._validate_collector_edge_rules(edge, source_node, destination_node)
+
+ def has_node(self, node_id: str) -> bool:
+ """Determines whether or not a node exists in the graph."""
+ try:
+ _ = self.get_node(node_id)
+ return True
+ except NodeNotFoundError:
+ return False
+
+ def get_node(self, node_id: str) -> BaseInvocation:
+ """Gets a node from the graph."""
+ try:
+ return self.nodes[node_id]
+ except KeyError as e:
+ raise NodeNotFoundError(f"Node {node_id} not found in graph") from e
+
+ def update_node(self, node_id: str, new_node: BaseInvocation) -> None:
+ """Updates a node in the graph."""
+ node = self.nodes[node_id]
+
+ # Ensure the node type matches the new node
+ if type(node) is not type(new_node):
+ raise TypeError(f"Node {node_id} is type {type(node)} but new node is type {type(new_node)}")
+
+ # Ensure the new id is either the same or is not in the graph
+ if new_node.id != node.id and self.has_node(new_node.id):
+ raise NodeAlreadyInGraphError(f"Node with id {new_node.id} already exists in graph")
+
+ # Set the new node in the graph
+ self.nodes[new_node.id] = new_node
+ if new_node.id != node.id:
+ input_edges = self._get_input_edges(node_id)
+ output_edges = self._get_output_edges(node_id)
+
+ # Delete node and all edges
+ self.delete_node(node_id)
+
+ # Create new edges for each input and output
+ for edge in input_edges:
+ self.add_edge(
+ Edge(
+ source=edge.source,
+ destination=EdgeConnection(node_id=new_node.id, field=edge.destination.field),
+ )
+ )
+
+ for edge in output_edges:
+ self.add_edge(
+ Edge(
+ source=EdgeConnection(node_id=new_node.id, field=edge.source.field),
+ destination=edge.destination,
+ )
+ )
+
+ def _get_input_edges(self, node_id: str, field: Optional[str] = None) -> list[Edge]:
+ """Gets all input edges for a node. If field is provided, only edges to that field are returned."""
+
+ edges = [e for e in self.edges if e.destination.node_id == node_id]
+
+ if field is None:
+ return edges
+
+ filtered_edges = [e for e in edges if e.destination.field == field]
+
+ return filtered_edges
+
+ def _get_output_edges(self, node_id: str, field: Optional[str] = None) -> list[Edge]:
+ """Gets all output edges for a node. If field is provided, only edges from that field are returned."""
+ edges = [e for e in self.edges if e.source.node_id == node_id]
+
+ if field is None:
+ return edges
+
+ filtered_edges = [e for e in edges if e.source.field == field]
+
+ return filtered_edges
+
+ def _is_iterator_connection_valid(
+ self,
+ node_id: str,
+ new_input: Optional[EdgeConnection] = None,
+ new_output: Optional[EdgeConnection] = None,
+ ) -> str | None:
+ inputs = [e.source for e in self._get_input_edges(node_id, COLLECTION_FIELD)]
+ outputs = [e.destination for e in self._get_output_edges(node_id, ITEM_FIELD)]
+
+ if new_input is not None:
+ inputs.append(new_input)
+ if new_output is not None:
+ outputs.append(new_output)
+
+ return self._validate_iterator_connections(inputs, outputs)
+
+ def _validate_iterator_connections(self, inputs: list[EdgeConnection], outputs: list[EdgeConnection]) -> str | None:
+ presence_error = self._validate_iterator_input_presence(inputs)
+ if presence_error is not None:
+ return presence_error
+
+ input_node = self.get_node(inputs[0].node_id)
+ input_field_type = get_output_field_type(input_node, inputs[0].field)
+ output_field_types = self._get_iterator_output_field_types(outputs)
+
+ input_type_error = self._validate_iterator_input_type(input_field_type)
+ if input_type_error is not None:
+ return input_type_error
+
+ output_type_error = self._validate_iterator_output_types(input_field_type, output_field_types)
+ if output_type_error is not None:
+ return output_type_error
+
+ return self._validate_iterator_collector_input(input_node, output_field_types)
+
+ def _validate_iterator_input_presence(self, inputs: list[EdgeConnection]) -> str | None:
+ if len(inputs) == 0:
+ return "Iterator must have a collection input edge"
+ if len(inputs) > 1:
+ return "Iterator may only have one input edge"
+ return None
+
+ def _get_iterator_output_field_types(self, outputs: list[EdgeConnection]) -> list[Any]:
+ return [get_input_field_type(self.get_node(e.node_id), e.field) for e in outputs]
+
+ def _validate_iterator_input_type(self, input_field_type: Any) -> str | None:
+ if get_origin(input_field_type) is not list:
+ return "Iterator input must be a collection"
+ return None
+
+ def _validate_iterator_output_types(self, input_field_type: Any, output_field_types: list[Any]) -> str | None:
+ input_field_item_type = get_args(input_field_type)[0]
+ if not all(are_connection_types_compatible(input_field_item_type, t) for t in output_field_types):
+ return "Iterator outputs must connect to an input with a matching type"
+ return None
+
+ def _validate_iterator_collector_input(
+ self, input_node: BaseInvocation, output_field_types: list[Any]
+ ) -> str | None:
+ if not isinstance(input_node, CollectInvocation):
+ return None
+
+ input_root_type = self._get_collector_input_root_type(input_node.id)
+ if input_root_type is None:
+ return "Iterator input collector must have at least one item or collection input edge"
+ if not all(are_connection_types_compatible(input_root_type, t) for t in output_field_types):
+ return "Iterator collection type must match all iterator output types"
+ return None
+
+ def _resolve_collector_input_types(self, node_id: str, visited: Optional[set[str]] = None) -> set[Any]:
+ """Resolves possible item types for a collector's inputs, recursively following chained collectors."""
+ visited = visited or set()
+ if node_id in visited:
+ return set()
+ visited.add(node_id)
+
+ input_types: set[Any] = set()
+
+ for edge in self._get_input_edges(node_id, ITEM_FIELD):
+ input_field_type = get_output_field_type(self.get_node(edge.source.node_id), edge.source.field)
+ resolved_types = [input_field_type] if get_origin(input_field_type) is None else get_args(input_field_type)
+ input_types.update(t for t in resolved_types if t != NoneType)
+
+ for edge in self._get_input_edges(node_id, COLLECTION_FIELD):
+ source_node = self.get_node(edge.source.node_id)
+ if isinstance(source_node, CollectInvocation) and edge.source.field == COLLECTION_FIELD:
+ input_types.update(self._resolve_collector_input_types(source_node.id, visited.copy()))
+ continue
+
+ input_field_type = get_output_field_type(source_node, edge.source.field)
+ input_types.update(extract_collection_item_types(input_field_type))
+
+ return input_types
+
+ def _get_type_tree_root_types(self, input_types: set[Any]) -> list[Any]:
+ type_tree = nx.DiGraph()
+ type_tree.add_nodes_from(input_types)
+ type_tree.add_edges_from([e for e in itertools.permutations(input_types, 2) if issubclass(e[1], e[0])])
+ type_degrees = type_tree.in_degree(type_tree.nodes)
+ return [t[0] for t in type_degrees if t[1] == 0] # type: ignore
+
+ def _get_collector_input_root_type(self, node_id: str) -> Any | None:
+ input_types = self._resolve_collector_input_types(node_id)
+ non_any_input_types = {t for t in input_types if t != Any}
+ if len(non_any_input_types) == 0 and Any in input_types:
+ return Any
+ if len(non_any_input_types) == 0:
+ return None
+
+ root_types = self._get_type_tree_root_types(non_any_input_types)
+ if len(root_types) != 1:
+ return Any
+ return root_types[0]
+
+ def _get_collector_connections(
+ self,
+ node_id: str,
+ new_input: Optional[EdgeConnection] = None,
+ new_input_field: Optional[str] = None,
+ new_output: Optional[EdgeConnection] = None,
+ ) -> tuple[list[EdgeConnection], list[EdgeConnection], list[EdgeConnection]]:
+ item_inputs = [e.source for e in self._get_input_edges(node_id, ITEM_FIELD)]
+ collection_inputs = [e.source for e in self._get_input_edges(node_id, COLLECTION_FIELD)]
+ outputs = [e.destination for e in self._get_output_edges(node_id, COLLECTION_FIELD)]
+
+ if new_input is not None:
+ field = new_input_field or ITEM_FIELD
+ if field == ITEM_FIELD:
+ item_inputs.append(new_input)
+ elif field == COLLECTION_FIELD:
+ collection_inputs.append(new_input)
+
+ if new_output is not None:
+ outputs.append(new_output)
+
+ return item_inputs, collection_inputs, outputs
+
+ def _get_collector_port_types(
+ self,
+ item_inputs: list[EdgeConnection],
+ collection_inputs: list[EdgeConnection],
+ outputs: list[EdgeConnection],
+ ) -> tuple[list[Any], list[Any], list[Any]]:
+ item_input_field_types = [get_output_field_type(self.get_node(e.node_id), e.field) for e in item_inputs]
+ collection_input_field_types = [
+ get_output_field_type(self.get_node(e.node_id), e.field) for e in collection_inputs
+ ]
+ output_field_types = [get_input_field_type(self.get_node(e.node_id), e.field) for e in outputs]
+ return item_input_field_types, collection_input_field_types, output_field_types
+
+ def _resolve_item_input_types(self, item_input_field_types: list[Any]) -> set[Any]:
+ return {
+ resolved_type
+ for input_field_type in item_input_field_types
+ for resolved_type in (
+ [input_field_type] if get_origin(input_field_type) is None else get_args(input_field_type)
+ )
+ if resolved_type != NoneType
+ }
+
+ def _resolve_collection_input_types(
+ self, collection_inputs: list[EdgeConnection], collection_input_field_types: list[Any]
+ ) -> set[Any]:
+ input_field_types: set[Any] = set()
+ for input_conn, input_field_type in zip(collection_inputs, collection_input_field_types, strict=False):
+ source_node = self.get_node(input_conn.node_id)
+ if isinstance(source_node, CollectInvocation) and input_conn.field == COLLECTION_FIELD:
+ input_field_types.update(self._resolve_collector_input_types(source_node.id))
+ continue
+ input_field_types.update(extract_collection_item_types(input_field_type))
+ return input_field_types
+
+ def _validate_collector_collection_inputs(self, collection_input_field_types: list[Any]) -> str | None:
+ if not all((is_list_or_contains_list(t) or is_any(t) for t in collection_input_field_types)):
+ return "Collector collection input must be a collection"
+ return None
+
+ def _get_collector_input_root_type_from_resolved_types(
+ self, input_field_types: set[Any]
+ ) -> tuple[bool, Any | None]:
+ non_any_input_field_types = {t for t in input_field_types if t != Any}
+ root_types = self._get_type_tree_root_types(non_any_input_field_types)
+ if len(root_types) > 1:
+ return True, None
+ return False, root_types[0] if len(root_types) == 1 else None
+
+ def _validate_collector_output_types(
+ self, output_field_types: list[Any], input_root_type: Any | None
+ ) -> str | None:
+ if not all(is_list_or_contains_list(t) or is_any(t) for t in output_field_types):
+ return "Collector output must connect to a collection input"
+
+ if input_root_type is not None:
+ if not all(
+ is_any(t)
+ or is_union_subtype(input_root_type, get_args(t)[0])
+ or issubclass(input_root_type, get_args(t)[0])
+ for t in output_field_types
+ ):
+ return "Collector outputs must connect to a collection input with a matching type"
+ elif any(not is_any(t) and get_args(t)[0] != Any for t in output_field_types):
+ return "Collector outputs must connect to a collection input with a matching type"
+
+ return None
+
+ def _validate_downstream_collector_outputs(
+ self, outputs: list[EdgeConnection], input_root_type: Any | None
+ ) -> str | None:
+ for output in outputs:
+ output_node = self.get_node(output.node_id)
+ if not isinstance(output_node, CollectInvocation) or output.field != COLLECTION_FIELD:
+ continue
+ output_root_type = self._get_collector_input_root_type(output_node.id)
+ if output_root_type is None:
+ continue
+ if input_root_type is None:
+ if output_root_type != Any:
+ return "Collector outputs must connect to a collection input with a matching type"
+ continue
+ if not are_connection_types_compatible(input_root_type, output_root_type):
+ return "Collector outputs must connect to a collection input with a matching type"
+ return None
+
+ def _is_collector_connection_valid(
+ self,
+ node_id: str,
+ new_input: Optional[EdgeConnection] = None,
+ new_input_field: Optional[str] = None,
+ new_output: Optional[EdgeConnection] = None,
+ ) -> str | None:
+ item_inputs, collection_inputs, outputs = self._get_collector_connections(
+ node_id, new_input=new_input, new_input_field=new_input_field, new_output=new_output
+ )
+
+ if len(item_inputs) == 0 and len(collection_inputs) == 0:
+ return "Collector must have at least one item or collection input edge"
+
+ item_input_field_types, collection_input_field_types, output_field_types = self._get_collector_port_types(
+ item_inputs, collection_inputs, outputs
+ )
+
+ collection_input_error = self._validate_collector_collection_inputs(collection_input_field_types)
+ if collection_input_error is not None:
+ return collection_input_error
+
+ input_field_types = self._resolve_item_input_types(item_input_field_types)
+ input_field_types.update(self._resolve_collection_input_types(collection_inputs, collection_input_field_types))
+
+ has_multiple_root_types, input_root_type = self._get_collector_input_root_type_from_resolved_types(
+ input_field_types
+ )
+ if has_multiple_root_types:
+ return "Collector input collection items must be of a single type"
+
+ output_type_error = self._validate_collector_output_types(output_field_types, input_root_type)
+ if output_type_error is not None:
+ return output_type_error
+
+ downstream_output_error = self._validate_downstream_collector_outputs(outputs, input_root_type)
+ if downstream_output_error is not None:
+ return downstream_output_error
+
+ return None
+
+ def nx_graph(self) -> nx.DiGraph:
+ """Returns a NetworkX DiGraph representing the layout of this graph"""
+ # TODO: Cache this?
+ g = nx.DiGraph()
+ g.add_nodes_from(list(self.nodes.keys()))
+ g.add_edges_from({(e.source.node_id, e.destination.node_id) for e in self.edges})
+ return g
+
+ def nx_graph_flat(self, nx_graph: Optional[nx.DiGraph] = None) -> nx.DiGraph:
+ """Returns a flattened NetworkX DiGraph, including all subgraphs (but not with iterations expanded)"""
+ g = nx_graph or nx.DiGraph()
+
+ # Add all nodes from this graph except graph/iteration nodes
+ g.add_nodes_from([n.id for n in self.nodes.values()])
+
+ unique_edges = {(e.source.node_id, e.destination.node_id) for e in self.edges}
+ g.add_edges_from(unique_edges)
+ return g
+
+
+class GraphExecutionState(BaseModel):
+ """Tracks source-graph expansion, execution progress, and runtime results."""
+
+ id: str = Field(description="The id of the execution state", default_factory=uuid_string)
+ # TODO: Store a reference to the graph instead of the actual graph?
+ graph: Graph = Field(description="The graph being executed")
+
+ # The graph of materialized nodes
+ execution_graph: Graph = Field(
+ description="The expanded graph of activated and executed nodes",
+ default_factory=Graph,
+ )
+
+ # Nodes that have been executed
+ executed: set[str] = Field(description="The set of node ids that have been executed", default_factory=set)
+ executed_history: list[str] = Field(
+ description="The list of node ids that have been executed, in order of execution",
+ default_factory=list,
+ )
+
+ # The results of executed nodes
+ results: dict[str, AnyInvocationOutput] = Field(description="The results of node executions", default_factory=dict)
+
+ # Errors raised when executing nodes
+ errors: dict[str, str] = Field(description="Errors raised when executing nodes", default_factory=dict)
+
+ # Map of prepared/executed nodes to their original nodes
+ prepared_source_mapping: dict[str, str] = Field(
+ description="The map of prepared nodes to original graph nodes",
+ default_factory=dict,
+ )
+
+ # Map of original nodes to prepared nodes
+ source_prepared_mapping: dict[str, set[str]] = Field(
+ description="The map of original graph nodes to prepared nodes",
+ default_factory=dict,
+ )
+ # Ready queues grouped by node class name (internal only)
+ _ready_queues: dict[str, Deque[str]] = PrivateAttr(default_factory=dict)
+ # Current class being drained; stays until its queue empties
+ _active_class: Optional[str] = PrivateAttr(default=None)
+ # Optional priority; others follow in name order
+ ready_order: list[str] = Field(default_factory=list)
+ indegree: dict[str, int] = Field(default_factory=dict, description="Remaining unmet input count for exec nodes")
+ _iteration_path_cache: dict[str, tuple[int, ...]] = PrivateAttr(default_factory=dict)
+ _if_branch_exclusive_sources: dict[str, dict[str, set[str]]] = PrivateAttr(default_factory=dict)
+ _resolved_if_exec_branches: dict[str, str] = PrivateAttr(default_factory=dict)
+ _prepared_exec_metadata: dict[str, _PreparedExecNodeMetadata] = PrivateAttr(default_factory=dict)
+ _prepared_exec_registry: Optional[_PreparedExecRegistry] = PrivateAttr(default=None)
+ _if_branch_scheduler: Optional[_IfBranchScheduler] = PrivateAttr(default=None)
+ _execution_materializer: Optional[_ExecutionMaterializer] = PrivateAttr(default=None)
+ _execution_scheduler: Optional[_ExecutionScheduler] = PrivateAttr(default=None)
+ _execution_runtime: Optional[_ExecutionRuntime] = PrivateAttr(default=None)
+
+ def _type_key(self, node_obj: BaseInvocation) -> str:
+ return node_obj.__class__.__name__
+
+ def _prepared_registry(self) -> _PreparedExecRegistry:
+ if self._prepared_exec_registry is None:
+ self._prepared_exec_registry = _PreparedExecRegistry(
+ prepared_source_mapping=self.prepared_source_mapping,
+ source_prepared_mapping=self.source_prepared_mapping,
+ metadata=self._prepared_exec_metadata,
+ )
+ return self._prepared_exec_registry
+
+ def _if_scheduler(self) -> _IfBranchScheduler:
+ if self._if_branch_scheduler is None:
+ self._if_branch_scheduler = _IfBranchScheduler(self)
+ return self._if_branch_scheduler
+
+ def _materializer(self) -> _ExecutionMaterializer:
+ if self._execution_materializer is None:
+ self._execution_materializer = _ExecutionMaterializer(self)
+ return self._execution_materializer
+
+ def _scheduler(self) -> _ExecutionScheduler:
+ if self._execution_scheduler is None:
+ self._execution_scheduler = _ExecutionScheduler(self)
+ return self._execution_scheduler
+
+ def _runtime(self) -> _ExecutionRuntime:
+ if self._execution_runtime is None:
+ self._execution_runtime = _ExecutionRuntime(self)
+ return self._execution_runtime
+
+ def _register_prepared_exec_node(self, exec_node_id: str, source_node_id: str) -> None:
+ self._prepared_registry().register(exec_node_id, source_node_id)
+
+ def _get_prepared_exec_metadata(self, exec_node_id: str) -> _PreparedExecNodeMetadata:
+ return self._prepared_registry().get_metadata(exec_node_id)
+
+ def _set_prepared_exec_state(self, exec_node_id: str, state: PreparedExecState) -> None:
+ self._prepared_registry().set_state(exec_node_id, state)
+
+ def _get_iteration_path(self, exec_node_id: str) -> tuple[int, ...]:
+ return self._runtime().get_iteration_path(exec_node_id)
+
+ def _queue_for(self, cls_name: str) -> Deque[str]:
+ return self._scheduler().queue_for(cls_name)
+
+ def _is_deferred_by_unresolved_if(self, exec_node_id: str) -> bool:
+ return self._if_scheduler().is_deferred_by_unresolved_if(exec_node_id)
+
+ def _remove_from_ready_queues(self, exec_node_id: str) -> None:
+ self._scheduler().remove_from_ready_queues(exec_node_id)
+
+ def _try_resolve_if_node(self, exec_node_id: str) -> None:
+ self._if_scheduler().try_resolve_if_node(exec_node_id)
+
+ def set_ready_order(self, order: Iterable[Type[BaseInvocation] | str]) -> None:
+ names: list[str] = []
+ for x in order:
+ names.append(x.__name__ if hasattr(x, "__name__") else str(x))
+ self.ready_order = names
+
+ def _enqueue_if_ready(self, nid: str) -> None:
+ self._scheduler().enqueue_if_ready(nid)
+
+ def _prepare_until_node_ready(self) -> Optional[BaseInvocation]:
+ base_graph = self.graph.nx_graph_flat()
+ prepared_id = self._materializer().prepare(base_graph)
+ next_node: Optional[BaseInvocation] = None
+
+ while prepared_id is not None:
+ prepared_id = self._materializer().prepare(base_graph)
+ if next_node is None:
+ next_node = self._get_next_node()
+
+ return next_node
+
+ def _reset_runtime_caches(self) -> None:
+ self._ready_queues = {}
+ self._active_class = None
+ self._iteration_path_cache = {}
+ self._if_branch_exclusive_sources = {}
+ self._resolved_if_exec_branches = {}
+ self._prepared_exec_metadata = {}
+ self._prepared_exec_registry = None
+ self._if_branch_scheduler = None
+ self._execution_materializer = None
+ self._execution_scheduler = None
+ self._execution_runtime = None
+
+ def _rehydrate_prepared_exec_metadata(self) -> None:
+ registry = self._prepared_registry()
+ for exec_node_id, source_node_id in self.prepared_source_mapping.items():
+ metadata = registry.get_metadata(exec_node_id)
+ metadata.source_node_id = source_node_id
+ metadata.iteration_path = self._get_iteration_path(exec_node_id)
+ if exec_node_id in self.executed:
+ metadata.state = "executed" if exec_node_id in self.results else "skipped"
+ elif self.indegree.get(exec_node_id) == 0:
+ metadata.state = "ready"
+ else:
+ metadata.state = "pending"
+
+ def _apply_if_condition_inputs(self, exec_node_id: str, node: IfInvocation) -> bool:
+ condition_edges = self.execution_graph._get_input_edges(exec_node_id, "condition")
+ if any(edge.source.node_id not in self.executed for edge in condition_edges):
+ return False
+
+ for edge in condition_edges:
+ setattr(
+ node,
+ edge.destination.field,
+ copydeep(getattr(self.results[edge.source.node_id], edge.source.field)),
+ )
+ return True
+
+ def _rehydrate_resolved_if_exec_branches(self) -> None:
+ for exec_node_id, node in self.execution_graph.nodes.items():
+ if not isinstance(node, IfInvocation):
+ continue
+
+ if not self._apply_if_condition_inputs(exec_node_id, node):
+ continue
+
+ self._resolved_if_exec_branches[exec_node_id] = "true_input" if node.condition else "false_input"
+
+ def _rehydrate_ready_queues(self) -> None:
+ execution_graph = self.execution_graph.nx_graph_flat()
+ for exec_node_id in nx.topological_sort(execution_graph):
+ if exec_node_id in self.executed:
+ continue
+ if self.indegree.get(exec_node_id) != 0:
+ continue
+ self._enqueue_if_ready(exec_node_id)
+
+ def _rehydrate_runtime_state(self) -> None:
+ self._reset_runtime_caches()
+ self._rehydrate_prepared_exec_metadata()
+ self._rehydrate_resolved_if_exec_branches()
+ self._rehydrate_ready_queues()
+
+ def model_post_init(self, __context: Any) -> None:
+ self._rehydrate_runtime_state()
+
+ model_config = ConfigDict(
+ json_schema_extra={
+ "required": [
+ "id",
+ "graph",
+ "execution_graph",
+ "executed",
+ "executed_history",
+ "results",
+ "errors",
+ "prepared_source_mapping",
+ "source_prepared_mapping",
+ ]
+ }
+ )
+
+ @field_validator("graph")
+ def graph_is_valid(cls, v: Graph):
+ """Validates that the graph is valid"""
+ v.validate_self()
+ return v
+
+ def next(self) -> Optional[BaseInvocation]:
+ """Gets the next node ready to execute."""
+
+ # TODO: enable multiple nodes to execute simultaneously by tracking currently executing nodes
+ # possibly with a timeout?
+
+ # If there are no prepared nodes, prepare some nodes
+ next_node = self._get_next_node()
+ if next_node is None:
+ next_node = self._prepare_until_node_ready()
+
+ # Get values from edges
+ if next_node is not None:
+ try:
+ self._prepare_inputs(next_node)
+ except ValidationError as e:
+ raise NodeInputError(next_node, e)
+
+ # If next is still none, there's no next node, return None
+ return next_node
+
+ def complete(self, node_id: str, output: BaseInvocationOutput) -> None:
+ """Marks a node as complete"""
+ self._scheduler().complete(node_id, output)
+
+ def set_node_error(self, node_id: str, error: str):
+ """Marks a node as errored"""
+ self.errors[node_id] = error
+
+ def is_complete(self) -> bool:
+ """Returns true if the graph is complete"""
+ node_ids = set(self.graph.nx_graph_flat().nodes)
+ return self.has_error() or all((k in self.executed for k in node_ids))
+
+ def has_error(self) -> bool:
+ """Returns true if the graph has any errors"""
+ return len(self.errors) > 0
+
+ def _create_execution_node(self, node_id: str, iteration_node_map: list[tuple[str, str]]) -> list[str]:
+ return self._materializer().create_execution_node(node_id, iteration_node_map)
+
+ def _iterator_graph(self, base: Optional[nx.DiGraph] = None) -> nx.DiGraph:
+ return self._materializer().iterator_graph(base)
+
+ def _get_node_iterators(self, node_id: str, it_graph: Optional[nx.DiGraph] = None) -> list[str]:
+ return self._materializer().get_node_iterators(node_id, it_graph)
+
+ def _prepare(self, base_g: Optional[nx.DiGraph] = None) -> Optional[str]:
+ return self._materializer().prepare(base_g)
+
+ def _get_iteration_node(
+ self,
+ source_node_id: str,
+ graph: nx.DiGraph,
+ execution_graph: nx.DiGraph,
+ prepared_iterator_nodes: list[str],
+ ) -> Optional[str]:
+ return self._materializer().get_iteration_node(source_node_id, graph, execution_graph, prepared_iterator_nodes)
+
+ def _get_next_node(self) -> Optional[BaseInvocation]:
+ return self._scheduler().get_next_node()
+
+ def _prepare_inputs(self, node: BaseInvocation):
+ self._runtime().prepare_inputs(node)
+
+ # TODO: Add API for modifying underlying graph that checks if the change will be valid given the current execution state
+ def _is_edge_valid(self, edge: Edge) -> bool:
+ try:
+ self.graph._validate_edge(edge)
+ except InvalidEdgeError:
+ return False
+
+ # Invalid if destination has already been prepared or executed
+ if edge.destination.node_id in self.source_prepared_mapping:
+ return False
+
+ # Otherwise, the edge is valid
+ return True
+
+ def _is_node_updatable(self, node_id: str) -> bool:
+ # The node is updatable as long as it hasn't been prepared or executed
+ return node_id not in self.source_prepared_mapping
+
+ def add_node(self, node: BaseInvocation) -> None:
+ self.graph.add_node(node)
+
+ def update_node(self, node_id: str, new_node: BaseInvocation) -> None:
+ if not self._is_node_updatable(node_id):
+ raise NodeAlreadyExecutedError(
+ f"Node {node_id} has already been prepared or executed and cannot be updated"
+ )
+ self.graph.update_node(node_id, new_node)
+
+ def delete_node(self, node_id: str) -> None:
+ if not self._is_node_updatable(node_id):
+ raise NodeAlreadyExecutedError(
+ f"Node {node_id} has already been prepared or executed and cannot be deleted"
+ )
+ self.graph.delete_node(node_id)
+
+ def add_edge(self, edge: Edge) -> None:
+ if not self._is_node_updatable(edge.destination.node_id):
+ raise NodeAlreadyExecutedError(
+ f"Destination node {edge.destination.node_id} has already been prepared or executed and cannot be linked to"
+ )
+ self.graph.add_edge(edge)
+
+ def delete_edge(self, edge: Edge) -> None:
+ if not self._is_node_updatable(edge.destination.node_id):
+ raise NodeAlreadyExecutedError(
+ f"Destination node {edge.destination.node_id} has already been prepared or executed and cannot have a source edge deleted"
+ )
+ self.graph.delete_edge(edge)
diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py
new file mode 100644
index 00000000000..e38766d5ba2
--- /dev/null
+++ b/invokeai/app/services/shared/invocation_context.py
@@ -0,0 +1,808 @@
+from copy import deepcopy
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING, Callable, Optional, Union
+
+from PIL.Image import Image
+from pydantic.networks import AnyHttpUrl
+from torch import Tensor
+
+from invokeai.app.invocations.constants import IMAGE_MODES
+from invokeai.app.invocations.fields import MetadataField, WithBoard, WithMetadata
+from invokeai.app.services.board_records.board_records_common import BoardRecordOrderBy
+from invokeai.app.services.boards.boards_common import BoardDTO
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin
+from invokeai.app.services.images.images_common import ImageDTO
+from invokeai.app.services.invocation_services import InvocationServices
+from invokeai.app.services.model_records.model_records_base import UnknownModelException
+from invokeai.app.services.session_processor.session_processor_common import ProgressImage
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.util.step_callback import diffusion_step_callback
+from invokeai.backend.model_manager.configs.base import Config_Base
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig
+from invokeai.backend.model_manager.load.load_base import LoadedModel, LoadedModelWithoutConfig
+from invokeai.backend.model_manager.taxonomy import AnyModel, BaseModelType, ModelFormat, ModelType, SubModelType
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData
+
+if TYPE_CHECKING:
+ from invokeai.app.invocations.baseinvocation import BaseInvocation
+ from invokeai.app.invocations.model import ModelIdentifierField
+ from invokeai.app.services.session_queue.session_queue_common import SessionQueueItem
+
+"""
+The InvocationContext provides access to various services and data about the current invocation.
+
+We do not provide the invocation services directly, as their methods are both dangerous and
+inconvenient to use.
+
+For example:
+- The `images` service allows nodes to delete or unsafely modify existing images.
+- The `configuration` service allows nodes to change the app's config at runtime.
+- The `events` service allows nodes to emit arbitrary events.
+
+Wrapping these services provides a simpler and safer interface for nodes to use.
+
+When a node executes, a fresh `InvocationContext` is built for it, ensuring nodes cannot interfere
+with each other.
+
+Many of the wrappers have the same signature as the methods they wrap. This allows us to write
+user-facing docstrings and not need to go and update the internal services to match.
+
+Note: The docstrings are in weird places, but that's where they must be to get IDEs to see them.
+"""
+
+
+@dataclass
+class InvocationContextData:
+ queue_item: "SessionQueueItem"
+ """The queue item that is being executed."""
+ invocation: "BaseInvocation"
+ """The invocation that is being executed."""
+ source_invocation_id: str
+ """The ID of the invocation from which the currently executing invocation was prepared."""
+
+
+class InvocationContextInterface:
+ def __init__(self, services: InvocationServices, data: InvocationContextData) -> None:
+ self._services = services
+ self._data = data
+
+
+class BoardsInterface(InvocationContextInterface):
+ def create(self, board_name: str) -> BoardDTO:
+ """Creates a board for the current user.
+
+ Args:
+ board_name: The name of the board to create.
+
+ Returns:
+ The created board DTO.
+ """
+ user_id = self._data.queue_item.user_id
+ return self._services.boards.create(board_name, user_id)
+
+ def get_dto(self, board_id: str) -> BoardDTO:
+ """Gets a board DTO.
+
+ Args:
+ board_id: The ID of the board to get.
+
+ Returns:
+ The board DTO.
+ """
+ return self._services.boards.get_dto(board_id)
+
+ def get_all(self) -> list[BoardDTO]:
+ """Gets all boards accessible to the current user.
+
+ Returns:
+ A list of all boards accessible to the current user.
+ """
+ user_id = self._data.queue_item.user_id
+ return self._services.boards.get_all(
+ user_id, order_by=BoardRecordOrderBy.CreatedAt, direction=SQLiteDirection.Descending
+ )
+
+ def add_image_to_board(self, board_id: str, image_name: str) -> None:
+ """Adds an image to a board.
+
+ Args:
+ board_id: The ID of the board to add the image to.
+ image_name: The name of the image to add to the board.
+ """
+ return self._services.board_images.add_image_to_board(board_id, image_name)
+
+ def get_all_image_names_for_board(self, board_id: str) -> list[str]:
+ """Gets all image names for a board.
+
+ Args:
+ board_id: The ID of the board to get the image names for.
+
+ Returns:
+ A list of all image names for the board.
+ """
+ return self._services.board_images.get_all_board_image_names_for_board(
+ board_id,
+ categories=None,
+ is_intermediate=None,
+ )
+
+
+class LoggerInterface(InvocationContextInterface):
+ def debug(self, message: str) -> None:
+ """Logs a debug message.
+
+ Args:
+ message: The message to log.
+ """
+ self._services.logger.debug(message)
+
+ def info(self, message: str) -> None:
+ """Logs an info message.
+
+ Args:
+ message: The message to log.
+ """
+ self._services.logger.info(message)
+
+ def warning(self, message: str) -> None:
+ """Logs a warning message.
+
+ Args:
+ message: The message to log.
+ """
+ self._services.logger.warning(message)
+
+ def error(self, message: str) -> None:
+ """Logs an error message.
+
+ Args:
+ message: The message to log.
+ """
+ self._services.logger.error(message)
+
+
+class ImagesInterface(InvocationContextInterface):
+ def __init__(self, services: InvocationServices, data: InvocationContextData, util: "UtilInterface") -> None:
+ super().__init__(services, data)
+ self._util = util
+
+ def save(
+ self,
+ image: Image,
+ board_id: Optional[str] = None,
+ image_category: ImageCategory = ImageCategory.GENERAL,
+ metadata: Optional[MetadataField] = None,
+ ) -> ImageDTO:
+ """Saves an image, returning its DTO.
+
+ If the current queue item has a workflow or metadata, it is automatically saved with the image.
+
+ Args:
+ image: The image to save, as a PIL image.
+ board_id: The board ID to add the image to, if it should be added. It the invocation \
+ inherits from `WithBoard`, that board will be used automatically. **Use this only if \
+ you want to override or provide a board manually!**
+ image_category: The category of the image. Only the GENERAL category is added \
+ to the gallery.
+ metadata: The metadata to save with the image, if it should have any. If the \
+ invocation inherits from `WithMetadata`, that metadata will be used automatically. \
+ **Use this only if you want to override or provide metadata manually!**
+
+ Returns:
+ The saved image DTO.
+ """
+
+ self._util.signal_progress("Saving image")
+
+ # If `metadata` is provided directly, use that. Else, use the metadata provided by `WithMetadata`, falling back to None.
+ metadata_ = None
+ if metadata:
+ metadata_ = metadata.model_dump_json()
+ elif isinstance(self._data.invocation, WithMetadata) and self._data.invocation.metadata:
+ metadata_ = self._data.invocation.metadata.model_dump_json()
+
+ # If `board_id` is provided directly, use that. Else, use the board provided by `WithBoard`, falling back to None.
+ board_id_ = None
+ if board_id:
+ board_id_ = board_id
+ elif isinstance(self._data.invocation, WithBoard) and self._data.invocation.board:
+ board_id_ = self._data.invocation.board.board_id
+
+ workflow_ = None
+ if self._data.queue_item.workflow:
+ workflow_ = self._data.queue_item.workflow.model_dump_json()
+
+ graph_ = None
+ if self._data.queue_item.session.graph:
+ graph_ = self._data.queue_item.session.graph.model_dump_json()
+
+ return self._services.images.create(
+ image=image,
+ is_intermediate=self._data.invocation.is_intermediate,
+ image_category=image_category,
+ board_id=board_id_,
+ metadata=metadata_,
+ image_origin=ResourceOrigin.INTERNAL,
+ workflow=workflow_,
+ graph=graph_,
+ session_id=self._data.queue_item.session_id,
+ node_id=self._data.invocation.id,
+ user_id=self._data.queue_item.user_id,
+ )
+
+ def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image:
+ """Gets an image as a PIL Image object. This method returns a copy of the image.
+
+ Args:
+ image_name: The name of the image to get.
+ mode: The color mode to convert the image to. If None, the original mode is used.
+
+ Returns:
+ The image as a PIL Image object.
+ """
+ image = self._services.images.get_pil_image(image_name)
+ if mode and mode != image.mode:
+ try:
+ # convert makes a copy!
+ image = image.convert(mode)
+ except ValueError:
+ self._services.logger.warning(
+ f"Could not convert image from {image.mode} to {mode}. Using original mode instead."
+ )
+ else:
+ # copy the image to prevent the user from modifying the original
+ image = image.copy()
+ return image
+
+ def get_metadata(self, image_name: str) -> Optional[MetadataField]:
+ """Gets an image's metadata, if it has any.
+
+ Args:
+ image_name: The name of the image to get the metadata for.
+
+ Returns:
+ The image's metadata, if it has any.
+ """
+ return self._services.images.get_metadata(image_name)
+
+ def get_dto(self, image_name: str) -> ImageDTO:
+ """Gets an image as an ImageDTO object.
+
+ Args:
+ image_name: The name of the image to get.
+
+ Returns:
+ The image as an ImageDTO object.
+ """
+ return self._services.images.get_dto(image_name)
+
+ def get_path(self, image_name: str, thumbnail: bool = False) -> Path:
+ """Gets the internal path to an image or thumbnail.
+
+ Args:
+ image_name: The name of the image to get the path of.
+ thumbnail: Get the path of the thumbnail instead of the full image
+
+ Returns:
+ The local path of the image or thumbnail.
+ """
+ return Path(self._services.images.get_path(image_name, thumbnail))
+
+
+class TensorsInterface(InvocationContextInterface):
+ def save(self, tensor: Tensor) -> str:
+ """Saves a tensor, returning its name.
+
+ Args:
+ tensor: The tensor to save.
+
+ Returns:
+ The name of the saved tensor.
+ """
+
+ name = self._services.tensors.save(obj=tensor)
+ return name
+
+ def load(self, name: str) -> Tensor:
+ """Loads a tensor by name. This method returns a copy of the tensor.
+
+ Args:
+ name: The name of the tensor to load.
+
+ Returns:
+ The tensor.
+ """
+ return self._services.tensors.load(name).clone()
+
+
+class ConditioningInterface(InvocationContextInterface):
+ def save(self, conditioning_data: ConditioningFieldData) -> str:
+ """Saves a conditioning data object, returning its name.
+
+ Args:
+ conditioning_data: The conditioning data to save.
+
+ Returns:
+ The name of the saved conditioning data.
+ """
+
+ name = self._services.conditioning.save(obj=conditioning_data)
+ return name
+
+ def load(self, name: str) -> ConditioningFieldData:
+ """Loads conditioning data by name. This method returns a copy of the conditioning data.
+
+ Args:
+ name: The name of the conditioning data to load.
+
+ Returns:
+ The conditioning data.
+ """
+
+ return deepcopy(self._services.conditioning.load(name))
+
+
+class ModelsInterface(InvocationContextInterface):
+ """Common API for loading, downloading and managing models."""
+
+ def __init__(self, services: InvocationServices, data: InvocationContextData, util: "UtilInterface") -> None:
+ super().__init__(services, data)
+ self._util = util
+
+ def exists(self, identifier: Union[str, "ModelIdentifierField"]) -> bool:
+ """Check if a model exists.
+
+ Args:
+ identifier: The key or ModelField representing the model.
+
+ Returns:
+ True if the model exists, False if not.
+ """
+ if isinstance(identifier, str):
+ return self._services.model_manager.store.exists(identifier)
+ else:
+ return self._services.model_manager.store.exists(identifier.key)
+
+ def load(
+ self, identifier: Union[str, "ModelIdentifierField"], submodel_type: Optional[SubModelType] = None
+ ) -> LoadedModel:
+ """Load a model.
+
+ Args:
+ identifier: The key or ModelField representing the model.
+ submodel_type: The submodel of the model to get.
+
+ Returns:
+ An object representing the loaded model.
+ """
+
+ # The model manager emits events as it loads the model. It needs the context data to build
+ # the event payloads.
+
+ if isinstance(identifier, str):
+ model = self._services.model_manager.store.get_model(identifier)
+ else:
+ submodel_type = submodel_type or identifier.submodel_type
+ model = self._services.model_manager.store.get_model(identifier.key)
+
+ self._raise_if_external(model)
+
+ message = f"Loading model {model.name}"
+ if submodel_type:
+ message += f" ({submodel_type.value})"
+ self._util.signal_progress(message)
+ return self._services.model_manager.load.load_model(model, submodel_type)
+
+ def load_by_attrs(
+ self, name: str, base: BaseModelType, type: ModelType, submodel_type: Optional[SubModelType] = None
+ ) -> LoadedModel:
+ """Load a model by its attributes.
+
+ Args:
+ name: Name of the model.
+ base: The models' base type, e.g. `BaseModelType.StableDiffusion1`, `BaseModelType.StableDiffusionXL`, etc.
+ type: Type of the model, e.g. `ModelType.Main`, `ModelType.Vae`, etc.
+ submodel_type: The type of submodel to load, e.g. `SubModelType.UNet`, `SubModelType.TextEncoder`, etc. Only main
+ models have submodels.
+
+ Returns:
+ An object representing the loaded model.
+ """
+
+ configs = self._services.model_manager.store.search_by_attr(model_name=name, base_model=base, model_type=type)
+ if len(configs) == 0:
+ raise UnknownModelException(f"No model found with name {name}, base {base}, and type {type}")
+
+ if len(configs) > 1:
+ raise ValueError(f"More than one model found with name {name}, base {base}, and type {type}")
+
+ self._raise_if_external(configs[0])
+ message = f"Loading model {name}"
+ if submodel_type:
+ message += f" ({submodel_type.value})"
+ self._util.signal_progress(message)
+ return self._services.model_manager.load.load_model(configs[0], submodel_type)
+
+ @staticmethod
+ def _raise_if_external(model: AnyModelConfig) -> None:
+ if model.base == BaseModelType.External or model.format == ModelFormat.ExternalApi:
+ raise ValueError("External API models cannot be loaded from disk")
+
+ def get_config(self, identifier: Union[str, "ModelIdentifierField"]) -> AnyModelConfig:
+ """Get a model's config.
+
+ Args:
+ identifier: The key or ModelField representing the model.
+
+ Returns:
+ The model's config.
+ """
+ if isinstance(identifier, str):
+ return self._services.model_manager.store.get_model(identifier)
+ else:
+ return self._services.model_manager.store.get_model(identifier.key)
+
+ def search_by_path(self, path: Path) -> list[AnyModelConfig]:
+ """Search for models by path.
+
+ Args:
+ path: The path to search for.
+
+ Returns:
+ A list of models that match the path.
+ """
+ return self._services.model_manager.store.search_by_path(path)
+
+ def search_by_attrs(
+ self,
+ name: Optional[str] = None,
+ base: Optional[BaseModelType] = None,
+ type: Optional[ModelType] = None,
+ format: Optional[ModelFormat] = None,
+ ) -> list[AnyModelConfig]:
+ """Search for models by attributes.
+
+ Args:
+ name: The name to search for (exact match).
+ base: The base to search for, e.g. `BaseModelType.StableDiffusion1`, `BaseModelType.StableDiffusionXL`, etc.
+ type: Type type of model to search for, e.g. `ModelType.Main`, `ModelType.Vae`, etc.
+ format: The format of model to search for, e.g. `ModelFormat.Checkpoint`, `ModelFormat.Diffusers`, etc.
+
+ Returns:
+ A list of models that match the attributes.
+ """
+
+ return self._services.model_manager.store.search_by_attr(
+ model_name=name,
+ base_model=base,
+ model_type=type,
+ model_format=format,
+ )
+
+ def download_and_cache_model(
+ self,
+ source: str | AnyHttpUrl,
+ ) -> Path:
+ """
+ Download the model file located at source to the models cache and return its Path.
+
+ This can be used to single-file install models and other resources of arbitrary types
+ which should not get registered with the database. If the model is already
+ installed, the cached path will be returned. Otherwise it will be downloaded.
+
+ Args:
+ source: A URL that points to the model, or a huggingface repo_id.
+
+ Returns:
+ Path to the downloaded model
+ """
+ self._util.signal_progress(f"Downloading model {source}")
+ return self._services.model_manager.install.download_and_cache_model(source=source)
+
+ def load_local_model(
+ self,
+ model_path: Path,
+ loader: Optional[Callable[[Path], AnyModel]] = None,
+ ) -> LoadedModelWithoutConfig:
+ """
+ Load the model file located at the indicated path
+
+ If a loader callable is provided, it will be invoked to load the model. Otherwise,
+ `safetensors.torch.load_file()` or `torch.load()` will be called to load the model.
+
+ Be aware that the LoadedModelWithoutConfig object has no `config` attribute
+
+ Args:
+ path: A model Path
+ loader: A Callable that expects a Path and returns a dict[str|int, Any]
+
+ Returns:
+ A LoadedModelWithoutConfig object.
+ """
+
+ self._util.signal_progress(f"Loading model {model_path.name}")
+ return self._services.model_manager.load.load_model_from_path(model_path=model_path, loader=loader)
+
+ def load_remote_model(
+ self,
+ source: str | AnyHttpUrl,
+ loader: Optional[Callable[[Path], AnyModel]] = None,
+ ) -> LoadedModelWithoutConfig:
+ """
+ Download, cache, and load the model file located at the indicated URL or repo_id.
+
+ If the model is already downloaded, it will be loaded from the cache.
+
+ If the a loader callable is provided, it will be invoked to load the model. Otherwise,
+ `safetensors.torch.load_file()` or `torch.load()` will be called to load the model.
+
+ Be aware that the LoadedModelWithoutConfig object has no `config` attribute
+
+ Args:
+ source: A URL or huggingface repoid.
+ loader: A Callable that expects a Path and returns a dict[str|int, Any]
+
+ Returns:
+ A LoadedModelWithoutConfig object.
+ """
+ model_path = self._services.model_manager.install.download_and_cache_model(source=str(source))
+
+ self._util.signal_progress(f"Loading model {source}")
+ return self._services.model_manager.load.load_model_from_path(model_path=model_path, loader=loader)
+
+ def get_absolute_path(self, config_or_path: AnyModelConfig | Path | str) -> Path:
+ """Gets the absolute path for a given model config or path.
+
+ For example, if the model's path is `flux/main/FLUX Dev.safetensors`, and the models path is
+ `/home/username/InvokeAI/models`, this method will return
+ `/home/username/InvokeAI/models/flux/main/FLUX Dev.safetensors`.
+
+ Args:
+ config_or_path: The model config or path.
+
+ Returns:
+ The absolute path to the model.
+ """
+
+ model_path = Path(config_or_path.path) if isinstance(config_or_path, Config_Base) else Path(config_or_path)
+
+ if model_path.is_absolute():
+ return model_path.resolve()
+
+ base_models_path = self._services.configuration.models_path
+ joined_path = base_models_path / model_path
+ resolved_path = joined_path.resolve()
+ return resolved_path
+
+
+class ConfigInterface(InvocationContextInterface):
+ def get(self) -> InvokeAIAppConfig:
+ """Gets the app's config.
+
+ Returns:
+ The app's config.
+ """
+
+ return self._services.configuration
+
+
+class UtilInterface(InvocationContextInterface):
+ def __init__(
+ self, services: InvocationServices, data: InvocationContextData, is_canceled: Callable[[], bool]
+ ) -> None:
+ super().__init__(services, data)
+ self._is_canceled = is_canceled
+
+ def is_canceled(self) -> bool:
+ """Checks if the current session has been canceled.
+
+ Returns:
+ True if the current session has been canceled, False if not.
+ """
+ return self._is_canceled()
+
+ def sd_step_callback(self, intermediate_state: PipelineIntermediateState, base_model: BaseModelType) -> None:
+ """
+ The step callback emits a progress event with the current step, the total number of
+ steps, a preview image, and some other internal metadata.
+
+ This should be called after each denoising step.
+
+ Args:
+ intermediate_state: The intermediate state of the diffusion pipeline.
+ base_model: The base model for the current denoising step.
+ """
+
+ diffusion_step_callback(
+ signal_progress=self.signal_progress,
+ intermediate_state=intermediate_state,
+ base_model=base_model,
+ is_canceled=self.is_canceled,
+ )
+
+ def flux_step_callback(self, intermediate_state: PipelineIntermediateState) -> None:
+ """
+ The step callback emits a progress event with the current step, the total number of
+ steps, a preview image, and some other internal metadata.
+
+ This should be called after each denoising step.
+
+ Args:
+ intermediate_state: The intermediate state of the diffusion pipeline.
+ """
+
+ diffusion_step_callback(
+ signal_progress=self.signal_progress,
+ intermediate_state=intermediate_state,
+ base_model=BaseModelType.Flux,
+ is_canceled=self.is_canceled,
+ )
+
+ def flux2_step_callback(self, intermediate_state: PipelineIntermediateState) -> None:
+ """
+ The step callback for FLUX.2 Klein models (32-channel VAE).
+
+ Args:
+ intermediate_state: The intermediate state of the diffusion pipeline.
+ """
+
+ diffusion_step_callback(
+ signal_progress=self.signal_progress,
+ intermediate_state=intermediate_state,
+ base_model=BaseModelType.Flux2,
+ is_canceled=self.is_canceled,
+ )
+
+ def signal_progress(
+ self,
+ message: str,
+ percentage: float | None = None,
+ image: Image | None = None,
+ image_size: tuple[int, int] | None = None,
+ ) -> None:
+ """Signals the progress of some long-running invocation. The progress is displayed in the UI.
+
+ If a percentage is provided, the UI will display a progress bar and automatically append the percentage to the
+ message. You should not include the percentage in the message.
+
+ Example:
+ ```py
+ total_steps = 10
+ for i in range(total_steps):
+ percentage = i / (total_steps - 1)
+ context.util.signal_progress("Doing something cool", percentage)
+ ```
+
+ If an image is provided, the UI will display it. If your image should be displayed at a different size, provide
+ a tuple of `(width, height)` for the `image_size` parameter. The image will be displayed at the specified size
+ in the UI.
+
+ For example, SD denoising progress images are 1/8 the size of the original image, so you'd do this to ensure the
+ image is displayed at the correct size:
+ ```py
+ # Calculate the output size of the image (8x the progress image's size)
+ width = progress_image.width * 8
+ height = progress_image.height * 8
+ # Signal the progress with the image and output size
+ signal_progress("Denoising", percentage, progress_image, (width, height))
+ ```
+
+ If your progress image is very large, consider downscaling it to reduce the payload size and provide the original
+ size to the `image_size` parameter. The PIL `thumbnail` method is useful for this, as it maintains the aspect
+ ratio of the image:
+ ```py
+ # `thumbnail` modifies the image in-place, so we need to first make a copy
+ thumbnail_image = progress_image.copy()
+ # Resize the image to a maximum of 256x256 pixels, maintaining the aspect ratio
+ thumbnail_image.thumbnail((256, 256))
+ # Signal the progress with the thumbnail, passing the original size
+ signal_progress("Denoising", percentage, thumbnail, progress_image.size)
+ ```
+
+ Args:
+ message: A message describing the current status. Do not include the percentage in this message.
+ percentage: The current percentage completion for the process. Omit for indeterminate progress.
+ image: An optional image to display.
+ image_size: The optional size of the image to display. If omitted, the image will be displayed at its
+ original size.
+ """
+
+ self._services.events.emit_invocation_progress(
+ queue_item=self._data.queue_item,
+ invocation=self._data.invocation,
+ message=message,
+ percentage=percentage,
+ image=ProgressImage.build(image, image_size) if image else None,
+ )
+
+
+class InvocationContext:
+ """Provides access to various services and data for the current invocation.
+
+ Attributes:
+ images (ImagesInterface): Methods to save, get and update images and their metadata.
+ tensors (TensorsInterface): Methods to save and get tensors, including image, noise, masks, and masked images.
+ conditioning (ConditioningInterface): Methods to save and get conditioning data.
+ models (ModelsInterface): Methods to check if a model exists, get a model, and get a model's info.
+ logger (LoggerInterface): The app logger.
+ config (ConfigInterface): The app config.
+ util (UtilInterface): Utility methods, including a method to check if an invocation was canceled and step callbacks.
+ boards (BoardsInterface): Methods to interact with boards.
+ """
+
+ def __init__(
+ self,
+ images: ImagesInterface,
+ tensors: TensorsInterface,
+ conditioning: ConditioningInterface,
+ models: ModelsInterface,
+ logger: LoggerInterface,
+ config: ConfigInterface,
+ util: UtilInterface,
+ boards: BoardsInterface,
+ data: InvocationContextData,
+ services: InvocationServices,
+ ) -> None:
+ self.images = images
+ """Methods to save, get and update images and their metadata."""
+ self.tensors = tensors
+ """Methods to save and get tensors, including image, noise, masks, and masked images."""
+ self.conditioning = conditioning
+ """Methods to save and get conditioning data."""
+ self.models = models
+ """Methods to check if a model exists, get a model, and get a model's info."""
+ self.logger = logger
+ """The app logger."""
+ self.config = config
+ """The app config."""
+ self.util = util
+ """Utility methods, including a method to check if an invocation was canceled and step callbacks."""
+ self.boards = boards
+ """Methods to interact with boards."""
+ self._data = data
+ """An internal API providing access to data about the current queue item and invocation. You probably shouldn't use this. It may change without warning."""
+ self._services = services
+ """An internal API providing access to all application services. You probably shouldn't use this. It may change without warning."""
+
+
+def build_invocation_context(
+ services: InvocationServices,
+ data: InvocationContextData,
+ is_canceled: Callable[[], bool],
+) -> InvocationContext:
+ """Builds the invocation context for a specific invocation execution.
+
+ Args:
+ services: The invocation services to wrap.
+ data: The invocation context data.
+
+ Returns:
+ The invocation context.
+ """
+
+ logger = LoggerInterface(services=services, data=data)
+ tensors = TensorsInterface(services=services, data=data)
+ config = ConfigInterface(services=services, data=data)
+ util = UtilInterface(services=services, data=data, is_canceled=is_canceled)
+ conditioning = ConditioningInterface(services=services, data=data)
+ models = ModelsInterface(services=services, data=data, util=util)
+ images = ImagesInterface(services=services, data=data, util=util)
+ boards = BoardsInterface(services=services, data=data)
+
+ ctx = InvocationContext(
+ images=images,
+ logger=logger,
+ config=config,
+ tensors=tensors,
+ models=models,
+ data=data,
+ util=util,
+ conditioning=conditioning,
+ services=services,
+ boards=boards,
+ )
+
+ return ctx
diff --git a/invokeai/app/services/shared/pagination.py b/invokeai/app/services/shared/pagination.py
new file mode 100644
index 00000000000..ea342b11013
--- /dev/null
+++ b/invokeai/app/services/shared/pagination.py
@@ -0,0 +1,41 @@
+from typing import Generic, TypeVar
+
+from pydantic import BaseModel, Field
+
+GenericBaseModel = TypeVar("GenericBaseModel", bound=BaseModel)
+
+
+class CursorPaginatedResults(BaseModel, Generic[GenericBaseModel]):
+ """
+ Cursor-paginated results
+ Generic must be a Pydantic model
+ """
+
+ limit: int = Field(..., description="Limit of items to get")
+ has_more: bool = Field(..., description="Whether there are more items available")
+ items: list[GenericBaseModel] = Field(..., description="Items")
+
+
+class OffsetPaginatedResults(BaseModel, Generic[GenericBaseModel]):
+ """
+ Offset-paginated results
+ Generic must be a Pydantic model
+ """
+
+ limit: int = Field(description="Limit of items to get")
+ offset: int = Field(description="Offset from which to retrieve items")
+ total: int = Field(description="Total number of items in result")
+ items: list[GenericBaseModel] = Field(description="Items")
+
+
+class PaginatedResults(BaseModel, Generic[GenericBaseModel]):
+ """
+ Paginated results
+ Generic must be a Pydantic model
+ """
+
+ page: int = Field(description="Current Page")
+ pages: int = Field(description="Total number of pages")
+ per_page: int = Field(description="Number of items per page")
+ total: int = Field(description="Total number of items in result")
+ items: list[GenericBaseModel] = Field(description="Items")
diff --git a/invokeai/app/services/shared/sqlite/__init__.py b/invokeai/app/services/shared/sqlite/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/shared/sqlite/sqlite_common.py b/invokeai/app/services/shared/sqlite/sqlite_common.py
new file mode 100644
index 00000000000..25206952011
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite/sqlite_common.py
@@ -0,0 +1,10 @@
+from enum import Enum
+
+from invokeai.app.util.metaenum import MetaEnum
+
+sqlite_memory = ":memory:"
+
+
+class SQLiteDirection(str, Enum, metaclass=MetaEnum):
+ Ascending = "ASC"
+ Descending = "DESC"
diff --git a/invokeai/app/services/shared/sqlite/sqlite_database.py b/invokeai/app/services/shared/sqlite/sqlite_database.py
new file mode 100644
index 00000000000..e67aab0ea58
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite/sqlite_database.py
@@ -0,0 +1,93 @@
+import sqlite3
+import threading
+from collections.abc import Generator
+from contextlib import contextmanager
+from logging import Logger
+from pathlib import Path
+
+from invokeai.app.services.shared.sqlite.sqlite_common import sqlite_memory
+
+
+class SqliteDatabase:
+ """
+ Manages a connection to an SQLite database.
+
+ :param db_path: Path to the database file. If None, an in-memory database is used.
+ :param logger: Logger to use for logging.
+ :param verbose: Whether to log SQL statements. Provides `logger.debug` as the SQLite trace callback.
+
+ This is a light wrapper around the `sqlite3` module, providing a few conveniences:
+ - The database file is written to disk if it does not exist.
+ - Foreign key constraints are enabled by default.
+ - The connection is configured to use the `sqlite3.Row` row factory.
+
+ In addition to the constructor args, the instance provides the following attributes and methods:
+ - `conn`: A `sqlite3.Connection` object. Note that the connection must never be closed if the database is in-memory.
+ - `lock`: A shared re-entrant lock, used to approximate thread safety.
+ - `clean()`: Runs the SQL `VACUUM;` command and reports on the freed space.
+ """
+
+ def __init__(self, db_path: Path | None, logger: Logger, verbose: bool = False) -> None:
+ """Initializes the database. This is used internally by the class constructor."""
+ self._logger = logger
+ self._db_path = db_path
+ self._verbose = verbose
+ self._lock = threading.RLock()
+
+ if not self._db_path:
+ logger.info("Initializing in-memory database")
+ else:
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
+ self._logger.info(f"Initializing database at {self._db_path}")
+
+ self._conn = sqlite3.connect(database=self._db_path or sqlite_memory, check_same_thread=False)
+ self._conn.row_factory = sqlite3.Row
+
+ if self._verbose:
+ self._conn.set_trace_callback(self._logger.debug)
+
+ # Enable foreign key constraints
+ self._conn.execute("PRAGMA foreign_keys = ON;")
+
+ # Enable Write-Ahead Logging (WAL) mode for better concurrency
+ self._conn.execute("PRAGMA journal_mode = WAL;")
+
+ # Set a busy timeout to prevent database lockups during writes
+ self._conn.execute("PRAGMA busy_timeout = 5000;") # 5 seconds
+
+ def clean(self) -> None:
+ """
+ Cleans the database by running the VACUUM command, reporting on the freed space.
+ """
+ # No need to clean in-memory database
+ if not self._db_path:
+ return
+ try:
+ with self._conn as conn:
+ initial_db_size = Path(self._db_path).stat().st_size
+ conn.execute("VACUUM;")
+ conn.commit()
+ final_db_size = Path(self._db_path).stat().st_size
+ freed_space_in_mb = round((initial_db_size - final_db_size) / 1024 / 1024, 2)
+ if freed_space_in_mb > 0:
+ self._logger.info(f"Cleaned database (freed {freed_space_in_mb}MB)")
+ except Exception as e:
+ self._logger.error(f"Error cleaning database: {e}")
+ raise
+
+ @contextmanager
+ def transaction(self) -> Generator[sqlite3.Cursor, None, None]:
+ """
+ Thread-safe context manager for DB work.
+ Acquires the RLock, yields a Cursor, then commits or rolls back.
+ """
+ with self._lock:
+ cursor = self._conn.cursor()
+ try:
+ yield cursor
+ self._conn.commit()
+ except Exception:
+ self._conn.rollback()
+ raise
+ finally:
+ cursor.close()
diff --git a/invokeai/app/services/shared/sqlite/sqlite_util.py b/invokeai/app/services/shared/sqlite/sqlite_util.py
new file mode 100644
index 00000000000..12642610c8c
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite/sqlite_util.py
@@ -0,0 +1,90 @@
+from logging import Logger
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_1 import build_migration_1
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2 import build_migration_2
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_3 import build_migration_3
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_4 import build_migration_4
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_5 import build_migration_5
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_6 import build_migration_6
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_7 import build_migration_7
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_8 import build_migration_8
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_9 import build_migration_9
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_10 import build_migration_10
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import build_migration_11
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_16 import build_migration_16
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_17 import build_migration_17
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_18 import build_migration_18
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_19 import build_migration_19
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_20 import build_migration_20
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_21 import build_migration_21
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_22 import build_migration_22
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_23 import build_migration_23
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_24 import build_migration_24
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_25 import build_migration_25
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_26 import build_migration_26
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_27 import build_migration_27
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_28 import build_migration_28
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_29 import build_migration_29
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_30 import build_migration_30
+from invokeai.app.services.shared.sqlite_migrator.migrations.migration_31 import build_migration_31
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
+
+
+def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileStorageBase) -> SqliteDatabase:
+ """
+ Initializes the SQLite database.
+
+ :param config: The app config
+ :param logger: The logger
+ :param image_files: The image files service (used by migration 2)
+
+ This function:
+ - Instantiates a :class:`SqliteDatabase`
+ - Instantiates a :class:`SqliteMigrator` and registers all migrations
+ - Runs all migrations
+ """
+ db_path = None if config.use_memory_db else config.db_path
+ db = SqliteDatabase(db_path=db_path, logger=logger, verbose=config.log_sql)
+
+ migrator = SqliteMigrator(db=db)
+ migrator.register_migration(build_migration_1())
+ migrator.register_migration(build_migration_2(image_files=image_files, logger=logger))
+ migrator.register_migration(build_migration_3(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_4())
+ migrator.register_migration(build_migration_5())
+ migrator.register_migration(build_migration_6())
+ migrator.register_migration(build_migration_7())
+ migrator.register_migration(build_migration_8(app_config=config))
+ migrator.register_migration(build_migration_9())
+ migrator.register_migration(build_migration_10())
+ migrator.register_migration(build_migration_11(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_12(app_config=config))
+ migrator.register_migration(build_migration_13())
+ migrator.register_migration(build_migration_14())
+ migrator.register_migration(build_migration_15())
+ migrator.register_migration(build_migration_16())
+ migrator.register_migration(build_migration_17())
+ migrator.register_migration(build_migration_18())
+ migrator.register_migration(build_migration_19(app_config=config))
+ migrator.register_migration(build_migration_20())
+ migrator.register_migration(build_migration_21())
+ migrator.register_migration(build_migration_22(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_23(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_24(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_25(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_26(app_config=config, logger=logger))
+ migrator.register_migration(build_migration_27())
+ migrator.register_migration(build_migration_28())
+ migrator.register_migration(build_migration_29())
+ migrator.register_migration(build_migration_30())
+ migrator.register_migration(build_migration_31())
+ migrator.run_migrations()
+
+ return db
diff --git a/invokeai/app/services/shared/sqlite_migrator/__init__.py b/invokeai/app/services/shared/sqlite_migrator/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/__init__.py b/invokeai/app/services/shared/sqlite_migrator/migrations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_1.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_1.py
new file mode 100644
index 00000000000..574afb472f6
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_1.py
@@ -0,0 +1,372 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration1Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ """Migration callback for database version 1."""
+
+ self._create_board_images(cursor)
+ self._create_boards(cursor)
+ self._create_images(cursor)
+ self._create_model_config(cursor)
+ self._create_session_queue(cursor)
+ self._create_workflow_images(cursor)
+ self._create_workflows(cursor)
+
+ def _create_board_images(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the `board_images` table, indices and triggers."""
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS board_images (
+ board_id TEXT NOT NULL,
+ image_name TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME,
+ -- enforce one-to-many relationship between boards and images using PK
+ -- (we can extend this to many-to-many later)
+ PRIMARY KEY (image_name),
+ FOREIGN KEY (board_id) REFERENCES boards (board_id) ON DELETE CASCADE,
+ FOREIGN KEY (image_name) REFERENCES images (image_name) ON DELETE CASCADE
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE INDEX IF NOT EXISTS idx_board_images_board_id ON board_images (board_id);",
+ "CREATE INDEX IF NOT EXISTS idx_board_images_board_id_created_at ON board_images (board_id, created_at);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_board_images_updated_at
+ AFTER UPDATE
+ ON board_images FOR EACH ROW
+ BEGIN
+ UPDATE board_images SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE board_id = old.board_id AND image_name = old.image_name;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_boards(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the `boards` table, indices and triggers."""
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS boards (
+ board_id TEXT NOT NULL PRIMARY KEY,
+ board_name TEXT NOT NULL,
+ cover_image_name TEXT,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME,
+ FOREIGN KEY (cover_image_name) REFERENCES images (image_name) ON DELETE SET NULL
+ );
+ """
+ ]
+
+ indices = ["CREATE INDEX IF NOT EXISTS idx_boards_created_at ON boards (created_at);"]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_boards_updated_at
+ AFTER UPDATE
+ ON boards FOR EACH ROW
+ BEGIN
+ UPDATE boards SET updated_at = current_timestamp
+ WHERE board_id = old.board_id;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_images(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the `images` table, indices and triggers. Adds the `starred` column."""
+
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS images (
+ image_name TEXT NOT NULL PRIMARY KEY,
+ -- This is an enum in python, unrestricted string here for flexibility
+ image_origin TEXT NOT NULL,
+ -- This is an enum in python, unrestricted string here for flexibility
+ image_category TEXT NOT NULL,
+ width INTEGER NOT NULL,
+ height INTEGER NOT NULL,
+ session_id TEXT,
+ node_id TEXT,
+ metadata TEXT,
+ is_intermediate BOOLEAN DEFAULT FALSE,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_images_image_name ON images(image_name);",
+ "CREATE INDEX IF NOT EXISTS idx_images_image_origin ON images(image_origin);",
+ "CREATE INDEX IF NOT EXISTS idx_images_image_category ON images(image_category);",
+ "CREATE INDEX IF NOT EXISTS idx_images_created_at ON images(created_at);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_images_updated_at
+ AFTER UPDATE
+ ON images FOR EACH ROW
+ BEGIN
+ UPDATE images SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE image_name = old.image_name;
+ END;
+ """
+ ]
+
+ # Add the 'starred' column to `images` if it doesn't exist
+ cursor.execute("PRAGMA table_info(images)")
+ columns = [column[1] for column in cursor.fetchall()]
+
+ if "starred" not in columns:
+ tables.append("ALTER TABLE images ADD COLUMN starred BOOLEAN DEFAULT FALSE;")
+ indices.append("CREATE INDEX IF NOT EXISTS idx_images_starred ON images(starred);")
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_model_config(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the `model_config` table, `model_manager_metadata` table, indices and triggers."""
+
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS model_config (
+ id TEXT NOT NULL PRIMARY KEY,
+ -- The next 3 fields are enums in python, unrestricted string here
+ base TEXT NOT NULL,
+ type TEXT NOT NULL,
+ name TEXT NOT NULL,
+ path TEXT NOT NULL,
+ original_hash TEXT, -- could be null
+ -- Serialized JSON representation of the whole config object,
+ -- which will contain additional fields from subclasses
+ config TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- unique constraint on combo of name, base and type
+ UNIQUE(name, base, type)
+ );
+ """,
+ """--sql
+ CREATE TABLE IF NOT EXISTS model_manager_metadata (
+ metadata_key TEXT NOT NULL PRIMARY KEY,
+ metadata_value TEXT NOT NULL
+ );
+ """,
+ ]
+
+ # Add trigger for `updated_at`.
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS model_config_updated_at
+ AFTER UPDATE
+ ON model_config FOR EACH ROW
+ BEGIN
+ UPDATE model_config SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ ]
+
+ # Add indexes for searchable fields
+ indices = [
+ "CREATE INDEX IF NOT EXISTS base_index ON model_config(base);",
+ "CREATE INDEX IF NOT EXISTS type_index ON model_config(type);",
+ "CREATE INDEX IF NOT EXISTS name_index ON model_config(name);",
+ "CREATE UNIQUE INDEX IF NOT EXISTS path_index ON model_config(path);",
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_session_queue(self, cursor: sqlite3.Cursor) -> None:
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS session_queue (
+ item_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used for ordering, cursor pagination
+ batch_id TEXT NOT NULL, -- identifier of the batch this queue item belongs to
+ queue_id TEXT NOT NULL, -- identifier of the queue this queue item belongs to
+ session_id TEXT NOT NULL UNIQUE, -- duplicated data from the session column, for ease of access
+ field_values TEXT, -- NULL if no values are associated with this queue item
+ session TEXT NOT NULL, -- the session to be executed
+ status TEXT NOT NULL DEFAULT 'pending', -- the status of the queue item, one of 'pending', 'in_progress', 'completed', 'failed', 'canceled'
+ priority INTEGER NOT NULL DEFAULT 0, -- the priority, higher is more important
+ error TEXT, -- any errors associated with this queue item
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), -- updated via trigger
+ started_at DATETIME, -- updated via trigger
+ completed_at DATETIME -- updated via trigger, completed items are cleaned up on application startup
+ -- Ideally this is a FK, but graph_executions uses INSERT OR REPLACE, and REPLACE triggers the ON DELETE CASCADE...
+ -- FOREIGN KEY (session_id) REFERENCES graph_executions (id) ON DELETE CASCADE
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_session_queue_item_id ON session_queue(item_id);",
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_session_queue_session_id ON session_queue(session_id);",
+ "CREATE INDEX IF NOT EXISTS idx_session_queue_batch_id ON session_queue(batch_id);",
+ "CREATE INDEX IF NOT EXISTS idx_session_queue_created_priority ON session_queue(priority);",
+ "CREATE INDEX IF NOT EXISTS idx_session_queue_created_status ON session_queue(status);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_session_queue_completed_at
+ AFTER UPDATE OF status ON session_queue
+ FOR EACH ROW
+ WHEN
+ NEW.status = 'completed'
+ OR NEW.status = 'failed'
+ OR NEW.status = 'canceled'
+ BEGIN
+ UPDATE session_queue
+ SET completed_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE item_id = NEW.item_id;
+ END;
+ """,
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_session_queue_started_at
+ AFTER UPDATE OF status ON session_queue
+ FOR EACH ROW
+ WHEN
+ NEW.status = 'in_progress'
+ BEGIN
+ UPDATE session_queue
+ SET started_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE item_id = NEW.item_id;
+ END;
+ """,
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_session_queue_updated_at
+ AFTER UPDATE
+ ON session_queue FOR EACH ROW
+ BEGIN
+ UPDATE session_queue
+ SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE item_id = old.item_id;
+ END;
+ """,
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_workflow_images(self, cursor: sqlite3.Cursor) -> None:
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS workflow_images (
+ workflow_id TEXT NOT NULL,
+ image_name TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Soft delete, currently unused
+ deleted_at DATETIME,
+ -- enforce one-to-many relationship between workflows and images using PK
+ -- (we can extend this to many-to-many later)
+ PRIMARY KEY (image_name),
+ FOREIGN KEY (workflow_id) REFERENCES workflows (workflow_id) ON DELETE CASCADE,
+ FOREIGN KEY (image_name) REFERENCES images (image_name) ON DELETE CASCADE
+ );
+ """
+ ]
+
+ indices = [
+ "CREATE INDEX IF NOT EXISTS idx_workflow_images_workflow_id ON workflow_images (workflow_id);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_images_workflow_id_created_at ON workflow_images (workflow_id, created_at);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_workflow_images_updated_at
+ AFTER UPDATE
+ ON workflow_images FOR EACH ROW
+ BEGIN
+ UPDATE workflow_images SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE workflow_id = old.workflow_id AND image_name = old.image_name;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _create_workflows(self, cursor: sqlite3.Cursor) -> None:
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS workflows (
+ workflow TEXT NOT NULL,
+ workflow_id TEXT GENERATED ALWAYS AS (json_extract(workflow, '$.id')) VIRTUAL NOT NULL UNIQUE, -- gets implicit index
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) -- updated via trigger
+ );
+ """
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_workflows_updated_at
+ AFTER UPDATE
+ ON workflows FOR EACH ROW
+ BEGIN
+ UPDATE workflows
+ SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE workflow_id = old.workflow_id;
+ END;
+ """
+ ]
+
+ for stmt in tables + triggers:
+ cursor.execute(stmt)
+
+
+def build_migration_1() -> Migration:
+ """
+ Builds the migration from database version 0 (init) to 1.
+
+ This migration represents the state of the database circa InvokeAI v3.4.0, which was the last
+ version to not use migrations to manage the database.
+
+ As such, this migration does include some ALTER statements, and the SQL statements are written
+ to be idempotent.
+
+ - Create `board_images` junction table
+ - Create `boards` table
+ - Create `images` table, add `starred` column
+ - Create `model_config` table
+ - Create `session_queue` table
+ - Create `workflow_images` junction table
+ - Create `workflows` table
+ """
+
+ migration_1 = Migration(
+ from_version=0,
+ to_version=1,
+ callback=Migration1Callback(),
+ )
+
+ return migration_1
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py
new file mode 100644
index 00000000000..ce2cd2e965e
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_10.py
@@ -0,0 +1,35 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration10Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._update_error_cols(cursor)
+
+ def _update_error_cols(self, cursor: sqlite3.Cursor) -> None:
+ """
+ - Adds `error_type` and `error_message` columns to the session queue table.
+ - Renames the `error` column to `error_traceback`.
+ """
+
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN error_type TEXT;")
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN error_message TEXT;")
+ cursor.execute("ALTER TABLE session_queue RENAME COLUMN error TO error_traceback;")
+
+
+def build_migration_10() -> Migration:
+ """
+ Build the migration from database version 9 to 10.
+
+ This migration does the following:
+ - Adds `error_type` and `error_message` columns to the session queue table.
+ - Renames the `error` column to `error_traceback`.
+ """
+ migration_10 = Migration(
+ from_version=9,
+ to_version=10,
+ callback=Migration10Callback(),
+ )
+
+ return migration_10
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py
new file mode 100644
index 00000000000..17e61334f09
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_11.py
@@ -0,0 +1,75 @@
+import shutil
+import sqlite3
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+LEGACY_CORE_MODELS = [
+ # OpenPose
+ "any/annotators/dwpose/yolox_l.onnx",
+ "any/annotators/dwpose/dw-ll_ucoco_384.onnx",
+ # DepthAnything
+ "any/annotators/depth_anything/depth_anything_vitl14.pth",
+ "any/annotators/depth_anything/depth_anything_vitb14.pth",
+ "any/annotators/depth_anything/depth_anything_vits14.pth",
+ # Lama inpaint
+ "core/misc/lama/lama.pt",
+ # RealESRGAN upscale
+ "core/upscaling/realesrgan/RealESRGAN_x4plus.pth",
+ "core/upscaling/realesrgan/RealESRGAN_x4plus_anime_6B.pth",
+ "core/upscaling/realesrgan/ESRGAN_SRx4_DF2KOST_official-ff704c30.pth",
+ "core/upscaling/realesrgan/RealESRGAN_x2plus.pth",
+]
+
+
+class Migration11Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._remove_convert_cache()
+ self._remove_downloaded_models()
+ self._remove_unused_core_models()
+
+ def _remove_convert_cache(self) -> None:
+ """Rename models/.cache to models/.convert_cache."""
+ self._logger.info("Removing models/.cache directory. Converted models will now be cached in .convert_cache.")
+ legacy_convert_path = self._app_config.root_path / "models" / ".cache"
+ shutil.rmtree(legacy_convert_path, ignore_errors=True)
+
+ def _remove_downloaded_models(self) -> None:
+ """Remove models from their old locations; they will re-download when needed."""
+ self._logger.info(
+ "Removing legacy just-in-time models. Downloaded models will now be cached in .download_cache."
+ )
+ for model_path in LEGACY_CORE_MODELS:
+ legacy_dest_path = self._app_config.models_path / model_path
+ legacy_dest_path.unlink(missing_ok=True)
+
+ def _remove_unused_core_models(self) -> None:
+ """Remove unused core models and their directories."""
+ self._logger.info("Removing defunct core models.")
+ for dir in ["face_restoration", "misc", "upscaling"]:
+ path_to_remove = self._app_config.models_path / "core" / dir
+ shutil.rmtree(path_to_remove, ignore_errors=True)
+ shutil.rmtree(self._app_config.models_path / "any" / "annotators", ignore_errors=True)
+
+
+def build_migration_11(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """
+ Build the migration from database version 10 to 11.
+
+ This migration does the following:
+ - Moves "core" models previously downloaded with download_with_progress_bar() into new
+ "models/.download_cache" directory.
+ - Renames "models/.cache" to "models/.convert_cache".
+ """
+ migration_11 = Migration(
+ from_version=10,
+ to_version=11,
+ callback=Migration11Callback(app_config=app_config, logger=logger),
+ )
+
+ return migration_11
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_12.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_12.py
new file mode 100644
index 00000000000..f81632445c8
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_12.py
@@ -0,0 +1,35 @@
+import shutil
+import sqlite3
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration12Callback:
+ def __init__(self, app_config: InvokeAIAppConfig) -> None:
+ self._app_config = app_config
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._remove_model_convert_cache_dir()
+
+ def _remove_model_convert_cache_dir(self) -> None:
+ """
+ Removes unused model convert cache directory
+ """
+ convert_cache = self._app_config.convert_cache_path
+ shutil.rmtree(convert_cache, ignore_errors=True)
+
+
+def build_migration_12(app_config: InvokeAIAppConfig) -> Migration:
+ """
+ Build the migration from database version 11 to 12.
+
+ This migration removes the now-unused model convert cache directory.
+ """
+ migration_12 = Migration(
+ from_version=11,
+ to_version=12,
+ callback=Migration12Callback(app_config),
+ )
+
+ return migration_12
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_13.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_13.py
new file mode 100644
index 00000000000..401c0a4866a
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_13.py
@@ -0,0 +1,31 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration13Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_archived_col(cursor)
+
+ def _add_archived_col(self, cursor: sqlite3.Cursor) -> None:
+ """
+ - Adds `archived` columns to the board table.
+ """
+
+ cursor.execute("ALTER TABLE boards ADD COLUMN archived BOOLEAN DEFAULT FALSE;")
+
+
+def build_migration_13() -> Migration:
+ """
+ Build the migration from database version 12 to 13..
+
+ This migration does the following:
+ - Adds `archived` columns to the board table.
+ """
+ migration_13 = Migration(
+ from_version=12,
+ to_version=13,
+ callback=Migration13Callback(),
+ )
+
+ return migration_13
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_14.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_14.py
new file mode 100644
index 00000000000..399f5a71d20
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_14.py
@@ -0,0 +1,61 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration14Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._create_style_presets(cursor)
+
+ def _create_style_presets(self, cursor: sqlite3.Cursor) -> None:
+ """Create the table used to store style presets."""
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS style_presets (
+ id TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ preset_data TEXT NOT NULL,
+ type TEXT NOT NULL DEFAULT "user",
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
+ );
+ """
+ ]
+
+ # Add trigger for `updated_at`.
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS style_presets
+ AFTER UPDATE
+ ON style_presets FOR EACH ROW
+ BEGIN
+ UPDATE style_presets SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ ]
+
+ # Add indexes for searchable fields
+ indices = [
+ "CREATE INDEX IF NOT EXISTS idx_style_presets_name ON style_presets(name);",
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+
+def build_migration_14() -> Migration:
+ """
+ Build the migration from database version 13 to 14..
+
+ This migration does the following:
+ - Create the table used to store style presets.
+ """
+ migration_14 = Migration(
+ from_version=13,
+ to_version=14,
+ callback=Migration14Callback(),
+ )
+
+ return migration_14
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py
new file mode 100644
index 00000000000..455ff71ab5b
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_15.py
@@ -0,0 +1,34 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration15Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_origin_col(cursor)
+
+ def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
+ """
+ - Adds `origin` column to the session queue table.
+ - Adds `destination` column to the session queue table.
+ """
+
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN destination TEXT;")
+
+
+def build_migration_15() -> Migration:
+ """
+ Build the migration from database version 14 to 15.
+
+ This migration does the following:
+ - Adds `origin` column to the session queue table.
+ - Adds `destination` column to the session queue table.
+ """
+ migration_15 = Migration(
+ from_version=14,
+ to_version=15,
+ callback=Migration15Callback(),
+ )
+
+ return migration_15
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_16.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_16.py
new file mode 100644
index 00000000000..d401247b923
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_16.py
@@ -0,0 +1,31 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration16Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_retried_from_item_id_col(cursor)
+
+ def _add_retried_from_item_id_col(self, cursor: sqlite3.Cursor) -> None:
+ """
+ - Adds `retried_from_item_id` column to the session queue table.
+ """
+
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN retried_from_item_id INTEGER;")
+
+
+def build_migration_16() -> Migration:
+ """
+ Build the migration from database version 15 to 16.
+
+ This migration does the following:
+ - Adds `retried_from_item_id` column to the session queue table.
+ """
+ migration_16 = Migration(
+ from_version=15,
+ to_version=16,
+ callback=Migration16Callback(),
+ )
+
+ return migration_16
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_17.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_17.py
new file mode 100644
index 00000000000..8e2e788a8f5
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_17.py
@@ -0,0 +1,35 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration17Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_workflows_tags_col(cursor)
+
+ def _add_workflows_tags_col(self, cursor: sqlite3.Cursor) -> None:
+ """
+ - Adds `tags` column to the workflow_library table. It is a generated column that extracts the tags from the
+ workflow JSON.
+ """
+
+ cursor.execute(
+ "ALTER TABLE workflow_library ADD COLUMN tags TEXT GENERATED ALWAYS AS (json_extract(workflow, '$.tags')) VIRTUAL;"
+ )
+
+
+def build_migration_17() -> Migration:
+ """
+ Build the migration from database version 16 to 17.
+
+ This migration does the following:
+ - Adds `tags` column to the workflow_library table. It is a generated column that extracts the tags from the
+ workflow JSON.
+ """
+ migration_17 = Migration(
+ from_version=16,
+ to_version=17,
+ callback=Migration17Callback(),
+ )
+
+ return migration_17
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_18.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_18.py
new file mode 100644
index 00000000000..7879ddc378f
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_18.py
@@ -0,0 +1,47 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration18Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._make_workflow_opened_at_nullable(cursor)
+
+ def _make_workflow_opened_at_nullable(self, cursor: sqlite3.Cursor) -> None:
+ """
+ Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
+ - Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
+ - Dropping the existing `opened_at` column
+ - Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
+ - Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
+ """
+ # For index renaming in SQLite, we need to drop and recreate
+ cursor.execute("DROP INDEX IF EXISTS idx_workflow_library_opened_at;")
+ # Rename existing column to deprecated
+ cursor.execute("ALTER TABLE workflow_library DROP COLUMN opened_at;")
+ # Add new nullable column - all values will be NULL - no migration of data needed
+ cursor.execute("ALTER TABLE workflow_library ADD COLUMN opened_at DATETIME;")
+ # Create new index on the new column
+ cursor.execute(
+ "CREATE INDEX idx_workflow_library_opened_at ON workflow_library(opened_at);",
+ )
+
+
+def build_migration_18() -> Migration:
+ """
+ Build the migration from database version 17 to 18.
+
+ This migration does the following:
+ - Make the `opened_at` column nullable in the `workflow_library` table. This is accomplished by:
+ - Dropping the existing `idx_workflow_library_opened_at` index (must be done before dropping the column)
+ - Dropping the existing `opened_at` column
+ - Adding a new nullable column `opened_at` (no data migration needed, all values will be NULL)
+ - Adding a new `idx_workflow_library_opened_at` index on the `opened_at` column
+ """
+ migration_18 = Migration(
+ from_version=17,
+ to_version=18,
+ callback=Migration18Callback(),
+ )
+
+ return migration_18
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_19.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_19.py
new file mode 100644
index 00000000000..363cd2e8d83
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_19.py
@@ -0,0 +1,37 @@
+import sqlite3
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
+
+
+class Migration19Callback:
+ def __init__(self, app_config: InvokeAIAppConfig):
+ self.models_path = app_config.models_path
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._populate_size(cursor)
+ self._add_size_column(cursor)
+
+ def _add_size_column(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ "ALTER TABLE models ADD COLUMN file_size INTEGER "
+ "GENERATED ALWAYS as (json_extract(config, '$.file_size')) VIRTUAL NOT NULL"
+ )
+
+ def _populate_size(self, cursor: sqlite3.Cursor) -> None:
+ all_models = cursor.execute("SELECT id, path FROM models;").fetchall()
+
+ for model_id, model_path in all_models:
+ mod = ModelOnDisk(self.models_path / model_path)
+ cursor.execute(
+ "UPDATE models SET config = json_set(config, '$.file_size', ?) WHERE id = ?", (mod.size(), model_id)
+ )
+
+
+def build_migration_19(app_config: InvokeAIAppConfig) -> Migration:
+ return Migration(
+ from_version=18,
+ to_version=19,
+ callback=Migration19Callback(app_config),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2.py
new file mode 100644
index 00000000000..f290fe61594
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_2.py
@@ -0,0 +1,166 @@
+import sqlite3
+from logging import Logger
+
+from pydantic import ValidationError
+from tqdm import tqdm
+
+from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
+from invokeai.app.services.image_files.image_files_common import ImageFileNotFoundException
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.app.services.workflow_records.workflow_records_common import (
+ UnsafeWorkflowWithVersionValidator,
+)
+
+
+class Migration2Callback:
+ def __init__(self, image_files: ImageFileStorageBase, logger: Logger):
+ self._image_files = image_files
+ self._logger = logger
+
+ def __call__(self, cursor: sqlite3.Cursor):
+ self._add_images_has_workflow(cursor)
+ self._add_session_queue_workflow(cursor)
+ self._drop_old_workflow_tables(cursor)
+ self._add_workflow_library(cursor)
+ self._drop_model_manager_metadata(cursor)
+ self._migrate_embedded_workflows(cursor)
+
+ def _add_images_has_workflow(self, cursor: sqlite3.Cursor) -> None:
+ """Add the `has_workflow` column to `images` table."""
+ cursor.execute("PRAGMA table_info(images)")
+ columns = [column[1] for column in cursor.fetchall()]
+
+ if "has_workflow" not in columns:
+ cursor.execute("ALTER TABLE images ADD COLUMN has_workflow BOOLEAN DEFAULT FALSE;")
+
+ def _add_session_queue_workflow(self, cursor: sqlite3.Cursor) -> None:
+ """Add the `workflow` column to `session_queue` table."""
+
+ cursor.execute("PRAGMA table_info(session_queue)")
+ columns = [column[1] for column in cursor.fetchall()]
+
+ if "workflow" not in columns:
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN workflow TEXT;")
+
+ def _drop_old_workflow_tables(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the `workflows` and `workflow_images` tables."""
+ cursor.execute("DROP TABLE IF EXISTS workflow_images;")
+ cursor.execute("DROP TABLE IF EXISTS workflows;")
+
+ def _add_workflow_library(self, cursor: sqlite3.Cursor) -> None:
+ """Adds the `workflow_library` table and drops the `workflows` and `workflow_images` tables."""
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS workflow_library (
+ workflow_id TEXT NOT NULL PRIMARY KEY,
+ workflow TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- updated manually when retrieving workflow
+ opened_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Generated columns, needed for indexing and searching
+ category TEXT GENERATED ALWAYS as (json_extract(workflow, '$.meta.category')) VIRTUAL NOT NULL,
+ name TEXT GENERATED ALWAYS as (json_extract(workflow, '$.name')) VIRTUAL NOT NULL,
+ description TEXT GENERATED ALWAYS as (json_extract(workflow, '$.description')) VIRTUAL NOT NULL
+ );
+ """,
+ ]
+
+ indices = [
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_created_at ON workflow_library(created_at);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_updated_at ON workflow_library(updated_at);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_opened_at ON workflow_library(opened_at);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_category ON workflow_library(category);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_name ON workflow_library(name);",
+ "CREATE INDEX IF NOT EXISTS idx_workflow_library_description ON workflow_library(description);",
+ ]
+
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS tg_workflow_library_updated_at
+ AFTER UPDATE
+ ON workflow_library FOR EACH ROW
+ BEGIN
+ UPDATE workflow_library
+ SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE workflow_id = old.workflow_id;
+ END;
+ """
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+ def _drop_model_manager_metadata(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the `model_manager_metadata` table."""
+ cursor.execute("DROP TABLE IF EXISTS model_manager_metadata;")
+
+ def _migrate_embedded_workflows(self, cursor: sqlite3.Cursor) -> None:
+ """
+ In the v3.5.0 release, InvokeAI changed how it handles embedded workflows. The `images` table in
+ the database now has a `has_workflow` column, indicating if an image has a workflow embedded.
+
+ This migrate callback checks each image for the presence of an embedded workflow, then updates its entry
+ in the database accordingly.
+ """
+ # Get all image names
+ cursor.execute("SELECT image_name FROM images")
+ image_names: list[str] = [image[0] for image in cursor.fetchall()]
+ total_image_names = len(image_names)
+
+ if not total_image_names:
+ return
+
+ self._logger.info(f"Migrating workflows for {total_image_names} images")
+
+ # Migrate the images
+ to_migrate: list[tuple[bool, str]] = []
+ pbar = tqdm(image_names)
+ for idx, image_name in enumerate(pbar):
+ pbar.set_description(f"Checking image {idx + 1}/{total_image_names} for workflow")
+ try:
+ pil_image = self._image_files.get(image_name)
+ except ImageFileNotFoundException:
+ self._logger.warning(f"Image {image_name} not found, skipping")
+ continue
+ except Exception as e:
+ self._logger.warning(f"Error while checking image {image_name}, skipping: {e}")
+ continue
+ if "invokeai_workflow" in pil_image.info:
+ try:
+ UnsafeWorkflowWithVersionValidator.validate_json(pil_image.info.get("invokeai_workflow", ""))
+ except ValidationError:
+ self._logger.warning(f"Image {image_name} has invalid embedded workflow, skipping")
+ continue
+ to_migrate.append((True, image_name))
+
+ self._logger.info(f"Adding {len(to_migrate)} embedded workflows to database")
+ cursor.executemany("UPDATE images SET has_workflow = ? WHERE image_name = ?", to_migrate)
+
+
+def build_migration_2(image_files: ImageFileStorageBase, logger: Logger) -> Migration:
+ """
+ Builds the migration from database version 1 to 2.
+
+ Introduced in v3.5.0 for the new workflow library.
+
+ :param image_files: The image files service, used to check for embedded workflows
+ :param logger: The logger, used to log progress during embedded workflows handling
+
+ This migration does the following:
+ - Add `has_workflow` column to `images` table
+ - Add `workflow` column to `session_queue` table
+ - Drop `workflows` and `workflow_images` tables
+ - Add `workflow_library` table
+ - Drops the `model_manager_metadata` table
+ - Drops the `model_config` table, recreating it (at this point, there is no user data in this table)
+ - Populates the `has_workflow` column in the `images` table (requires `image_files` & `logger` dependencies)
+ """
+ migration_2 = Migration(
+ from_version=1,
+ to_version=2,
+ callback=Migration2Callback(image_files=image_files, logger=logger),
+ )
+
+ return migration_2
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_20.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_20.py
new file mode 100644
index 00000000000..420b3835705
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_20.py
@@ -0,0 +1,37 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration20Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """
+ -- many-to-many relationship table for models
+ CREATE TABLE IF NOT EXISTS model_relationships (
+ -- model_key_1 and model_key_2 are the same as the key(primary key) in the models table
+ model_key_1 TEXT NOT NULL,
+ model_key_2 TEXT NOT NULL,
+ created_at TEXT DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ PRIMARY KEY (model_key_1, model_key_2),
+ -- model_key_1 < model_key_2, to ensure uniqueness and prevent duplicates
+ FOREIGN KEY (model_key_1) REFERENCES models(id) ON DELETE CASCADE,
+ FOREIGN KEY (model_key_2) REFERENCES models(id) ON DELETE CASCADE
+ );
+ """
+ )
+ cursor.execute(
+ """
+ -- Creates an index to keep performance equal when searching for model_key_1 or model_key_2
+ CREATE INDEX IF NOT EXISTS keyx_model_relationships_model_key_2
+ ON model_relationships(model_key_2)
+ """
+ )
+
+
+def build_migration_20() -> Migration:
+ return Migration(
+ from_version=19,
+ to_version=20,
+ callback=Migration20Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_21.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_21.py
new file mode 100644
index 00000000000..82f63772c7c
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_21.py
@@ -0,0 +1,40 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration21Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """
+ CREATE TABLE client_state (
+ id INTEGER PRIMARY KEY CHECK(id = 1),
+ data TEXT NOT NULL, -- Frontend will handle the shape of this data
+ updated_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
+ );
+ """
+ )
+ cursor.execute(
+ """
+ CREATE TRIGGER tg_client_state_updated_at
+ AFTER UPDATE ON client_state
+ FOR EACH ROW
+ BEGIN
+ UPDATE client_state
+ SET updated_at = CURRENT_TIMESTAMP
+ WHERE id = OLD.id;
+ END;
+ """
+ )
+
+
+def build_migration_21() -> Migration:
+ """Builds the migration object for migrating from version 20 to version 21. This includes:
+ - Creating the `client_state` table.
+ - Adding a trigger to update the `updated_at` field on updates.
+ """
+ return Migration(
+ from_version=20,
+ to_version=21,
+ callback=Migration21Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_22.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_22.py
new file mode 100644
index 00000000000..bf97cbd00ac
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_22.py
@@ -0,0 +1,89 @@
+import sqlite3
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration22Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+ self._models_dir = app_config.models_path.resolve()
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._logger.info("Removing UNIQUE(name, base, type) constraint from models table")
+
+ # Step 1: Rename the existing models table
+ cursor.execute("ALTER TABLE models RENAME TO models_old;")
+
+ # Step 2: Create the new models table without the UNIQUE(name, base, type) constraint
+ cursor.execute(
+ """--sql
+ CREATE TABLE models (
+ id TEXT NOT NULL PRIMARY KEY,
+ hash TEXT GENERATED ALWAYS as (json_extract(config, '$.hash')) VIRTUAL NOT NULL,
+ base TEXT GENERATED ALWAYS as (json_extract(config, '$.base')) VIRTUAL NOT NULL,
+ type TEXT GENERATED ALWAYS as (json_extract(config, '$.type')) VIRTUAL NOT NULL,
+ path TEXT GENERATED ALWAYS as (json_extract(config, '$.path')) VIRTUAL NOT NULL,
+ format TEXT GENERATED ALWAYS as (json_extract(config, '$.format')) VIRTUAL NOT NULL,
+ name TEXT GENERATED ALWAYS as (json_extract(config, '$.name')) VIRTUAL NOT NULL,
+ description TEXT GENERATED ALWAYS as (json_extract(config, '$.description')) VIRTUAL,
+ source TEXT GENERATED ALWAYS as (json_extract(config, '$.source')) VIRTUAL NOT NULL,
+ source_type TEXT GENERATED ALWAYS as (json_extract(config, '$.source_type')) VIRTUAL NOT NULL,
+ source_api_response TEXT GENERATED ALWAYS as (json_extract(config, '$.source_api_response')) VIRTUAL,
+ trigger_phrases TEXT GENERATED ALWAYS as (json_extract(config, '$.trigger_phrases')) VIRTUAL,
+ file_size INTEGER GENERATED ALWAYS as (json_extract(config, '$.file_size')) VIRTUAL NOT NULL,
+ -- Serialized JSON representation of the whole config object, which will contain additional fields from subclasses
+ config TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Explicit unique constraint on path
+ UNIQUE(path)
+ );
+ """
+ )
+
+ # Step 3: Copy all data from the old table to the new table
+ # Only copy the stored columns (id, config, created_at, updated_at), not the virtual columns
+ cursor.execute(
+ "INSERT INTO models (id, config, created_at, updated_at) "
+ "SELECT id, config, created_at, updated_at FROM models_old;"
+ )
+
+ # Step 4: Drop the old table
+ cursor.execute("DROP TABLE models_old;")
+
+ # Step 5: Recreate indexes
+ cursor.execute("CREATE INDEX IF NOT EXISTS base_index ON models(base);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS type_index ON models(type);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS name_index ON models(name);")
+
+ # Step 6: Recreate the updated_at trigger
+ cursor.execute(
+ """--sql
+ CREATE TRIGGER models_updated_at
+ AFTER UPDATE
+ ON models FOR EACH ROW
+ BEGIN
+ UPDATE models SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ )
+
+
+def build_migration_22(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """Builds the migration object for migrating from version 21 to version 22.
+
+ This migration:
+ - Removes the UNIQUE constraint on the combination of (base, name, type) columns in the models table
+ - Adds an explicit UNIQUE contraint on the path column
+ """
+
+ return Migration(
+ from_version=21,
+ to_version=22,
+ callback=Migration22Callback(app_config=app_config, logger=logger),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_23.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_23.py
new file mode 100644
index 00000000000..3b5dc467b38
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_23.py
@@ -0,0 +1,193 @@
+import json
+import sqlite3
+from copy import deepcopy
+from logging import Logger
+from typing import Any
+
+from pydantic import ValidationError
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.backend.model_manager.configs.factory import AnyModelConfig, AnyModelConfigValidator
+from invokeai.backend.model_manager.configs.unknown import Unknown_Config
+from invokeai.backend.model_manager.taxonomy import (
+ BaseModelType,
+ ClipVariantType,
+ FluxVariantType,
+ ModelFormat,
+ ModelType,
+ ModelVariantType,
+ SchedulerPredictionType,
+)
+
+
+class Migration23Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+ self._models_dir = app_config.models_path.resolve()
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ # Grab all model records
+ cursor.execute("SELECT id, config FROM models;")
+ rows = cursor.fetchall()
+
+ migrated_count = 0
+ fallback_count = 0
+
+ for model_id, config_json in rows:
+ try:
+ # Migrate the config JSON to the latest schema
+ config_dict: dict[str, Any] = json.loads(config_json)
+ migrated_config = self._parse_and_migrate_config(config_dict)
+
+ if isinstance(migrated_config, Unknown_Config):
+ fallback_count += 1
+ else:
+ migrated_count += 1
+
+ # Write the migrated config back to the database
+ cursor.execute(
+ "UPDATE models SET config = ? WHERE id = ?;",
+ (migrated_config.model_dump_json(), model_id),
+ )
+ except ValidationError as e:
+ self._logger.error("Invalid config schema for model %s: %s", model_id, e)
+ raise
+ except json.JSONDecodeError as e:
+ self._logger.error("Invalid config JSON for model %s: %s", model_id, e)
+ raise
+
+ if migrated_count > 0 and fallback_count == 0:
+ self._logger.info(f"Migration complete: {migrated_count} model configs migrated")
+ elif migrated_count > 0 and fallback_count > 0:
+ self._logger.warning(
+ f"Migration complete: {migrated_count} model configs migrated, "
+ f"{fallback_count} model configs could not be migrated and were saved as unknown models",
+ )
+ elif migrated_count == 0 and fallback_count > 0:
+ self._logger.warning(
+ f"Migration complete: all {fallback_count} model configs could not be migrated and were saved as unknown models",
+ )
+ else:
+ self._logger.info("Migration complete: no model configs needed migration")
+
+ def _parse_and_migrate_config(self, config_dict: dict[str, Any]) -> AnyModelConfig:
+ # In v6.9.0 we made some improvements to the model taxonomy and the model config schemas. There are a changes
+ # we need to make to old configs to bring them up to date.
+
+ type = config_dict.get("type")
+ format = config_dict.get("format")
+ base = config_dict.get("base")
+
+ if base == BaseModelType.Flux.value and type == ModelType.Main.value:
+ # Prior to v6.9.0, we used an awkward combination of `config_path` and `variant` to distinguish between FLUX
+ # variants.
+ #
+ # `config_path` was set to one of:
+ # - flux-dev
+ # - flux-dev-fill
+ # - flux-schnell
+ #
+ # `variant` was set to ModelVariantType.Inpaint for FLUX Fill models and ModelVariantType.Normal for all other FLUX
+ # models.
+ #
+ # We now use the `variant` field to directly represent the FLUX variant type, and `config_path` is no longer used.
+
+ # Extract and remove `config_path` if present.
+ config_path = config_dict.pop("config_path", None)
+
+ match config_path:
+ case "flux-dev":
+ config_dict["variant"] = FluxVariantType.Dev.value
+ case "flux-dev-fill":
+ config_dict["variant"] = FluxVariantType.DevFill.value
+ case "flux-schnell":
+ config_dict["variant"] = FluxVariantType.Schnell.value
+ case _:
+ # Unknown config_path - default to Dev variant
+ config_dict["variant"] = FluxVariantType.Dev.value
+
+ if (
+ base
+ in {
+ BaseModelType.StableDiffusion1.value,
+ BaseModelType.StableDiffusion2.value,
+ BaseModelType.StableDiffusionXL.value,
+ BaseModelType.StableDiffusionXLRefiner.value,
+ }
+ and type == ModelType.Main.value
+ ):
+ # Prior to v6.9.0, the prediction_type field was optional and would default to Epsilon if not present.
+ # We now make it explicit and always present. Use the existing value if present, otherwise default to
+ # Epsilon, matching the probe logic.
+ #
+ # It's only on SD1.x, SD2.x, and SDXL main models.
+ config_dict["prediction_type"] = config_dict.get("prediction_type", SchedulerPredictionType.Epsilon.value)
+
+ # Prior to v6.9.0, the variant field was optional and would default to Normal if not present.
+ # We now make it explicit and always present. Use the existing value if present, otherwise default to
+ # Normal. It's only on SD main models.
+ config_dict["variant"] = config_dict.get("variant", ModelVariantType.Normal.value)
+
+ if base == BaseModelType.Flux.value and type == ModelType.LoRA.value and format == ModelFormat.Diffusers.value:
+ # Prior to v6.9.0, we used the Diffusers format for FLUX LoRA models that used the diffusers _key_
+ # structure. This was misleading, as everywhere else in the application, we used the Diffusers format
+ # to indicate that the model files were in the Diffusers _file_ format (i.e. a directory containing
+ # the weights and config files).
+ #
+ # At runtime, we check the LoRA's state dict directly to determine the key structure, so we do not need
+ # to rely on the format field for this purpose. As of v6.9.0, we always use the LyCORIS format for single-
+ # file LoRAs, regardless of the key structure.
+ #
+ # This change allows LoRA model identification to not need a special case for FLUX LoRAs in the diffusers
+ # key format.
+ config_dict["format"] = ModelFormat.LyCORIS.value
+
+ if type == ModelType.CLIPVision.value:
+ # Prior to v6.9.0, some CLIP Vision models were associated with a specific base model architecture:
+ # - CLIP-ViT-bigG-14-laion2B-39B-b160k is the image encoder for SDXL IP Adapter and was associated with SDXL
+ # - CLIP-ViT-H-14-laion2B-s32B-b79K is the image encoder for SD1.5 IP Adapter and was associated with SD1.5
+ #
+ # While this made some sense at the time, it is more correct and flexible to treat CLIP Vision models
+ # as independent of any specific base model architecture.
+ config_dict["base"] = BaseModelType.Any.value
+
+ if type == ModelType.CLIPEmbed.value:
+ # Prior to v6.9.0, some CLIP Embed models did not have a variant set. The default was the L variant.
+ # We now make it explicit and always present. Use the existing value if present, otherwise default to
+ # L variant. Also, treat CLIP Embed models as independent of any specific base model architecture.
+ config_dict["base"] = BaseModelType.Any.value
+ config_dict["variant"] = config_dict.get("variant", ClipVariantType.L.value)
+
+ try:
+ migrated_config = AnyModelConfigValidator.validate_python(config_dict)
+ # This could be a ValidationError or any other error that occurs during validation. A failure to generate a
+ # union discriminator could raise a ValueError, for example. Who knows what else could fail - catch all.
+ except Exception as e:
+ self._logger.error("Failed to validate migrated config, attempting to save as unknown model: %s", e)
+ cloned_config_dict = deepcopy(config_dict)
+ cloned_config_dict.pop("base", None)
+ cloned_config_dict.pop("type", None)
+ cloned_config_dict.pop("format", None)
+
+ migrated_config = Unknown_Config(
+ **cloned_config_dict,
+ base=BaseModelType.Unknown,
+ type=ModelType.Unknown,
+ format=ModelFormat.Unknown,
+ )
+ return migrated_config
+
+
+def build_migration_23(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """Builds the migration object for migrating from version 22 to version 23.
+
+ This migration updates model configurations to the latest config schemas for v6.9.0.
+ """
+
+ return Migration(
+ from_version=22,
+ to_version=23,
+ callback=Migration23Callback(app_config=app_config, logger=logger),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_24.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_24.py
new file mode 100644
index 00000000000..5ae8563b3e6
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_24.py
@@ -0,0 +1,240 @@
+import json
+import sqlite3
+from logging import Logger
+from pathlib import Path
+from typing import NamedTuple
+
+from pydantic import ValidationError
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.backend.model_manager.configs.factory import AnyModelConfigValidator
+
+
+class NormalizeResult(NamedTuple):
+ new_relative_path: str | None
+ rollback_ops: list[tuple[Path, Path]]
+
+
+class Migration24Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+ self._models_dir = app_config.models_path.resolve()
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ # Grab all model records
+ cursor.execute("SELECT id, config FROM models;")
+ rows = cursor.fetchall()
+
+ for model_id, config_json in rows:
+ try:
+ config = AnyModelConfigValidator.validate_json(config_json)
+ except ValidationError:
+ # This could happen if the config schema changed in a way that makes old configs invalid. Unlikely
+ # for users, more likely for devs testing out migration paths.
+ self._logger.warning("Skipping model %s: invalid config schema", model_id)
+ continue
+ except json.JSONDecodeError:
+ # This should never happen, as we use pydantic to serialize the config to JSON.
+ self._logger.warning("Skipping model %s: invalid config JSON", model_id)
+ continue
+
+ # We'll use a savepoint so we can roll back the database update if something goes wrong, and a simple
+ # rollback of file operations if needed.
+ cursor.execute("SAVEPOINT migrate_model")
+ try:
+ new_relative_path, rollback_ops = self._normalize_model_storage(
+ key=config.key,
+ path_value=config.path,
+ )
+ except Exception as err:
+ self._logger.error("Error normalizing model %s: %s", config.key, err)
+ cursor.execute("ROLLBACK TO SAVEPOINT migrate_model")
+ cursor.execute("RELEASE SAVEPOINT migrate_model")
+ continue
+
+ if new_relative_path is None:
+ cursor.execute("RELEASE SAVEPOINT migrate_model")
+ continue
+
+ config.path = new_relative_path
+ try:
+ cursor.execute(
+ "UPDATE models SET config = ? WHERE id = ?;",
+ (config.model_dump_json(), model_id),
+ )
+ except Exception as err:
+ self._logger.error("Database update failed for model %s: %s", config.key, err)
+ cursor.execute("ROLLBACK TO SAVEPOINT migrate_model")
+ cursor.execute("RELEASE SAVEPOINT migrate_model")
+ self._rollback_file_ops(rollback_ops)
+ continue
+
+ cursor.execute("RELEASE SAVEPOINT migrate_model")
+
+ self._prune_empty_directories()
+
+ def _normalize_model_storage(self, key: str, path_value: str) -> NormalizeResult:
+ models_dir = self._models_dir
+ stored_path = Path(path_value)
+
+ relative_path: Path | None
+ if stored_path.is_absolute():
+ # If the stored path is absolute, we need to check if it's inside the models directory, which means it is
+ # an Invoke-managed model. If it's outside, it is user-managed we leave it alone.
+ try:
+ relative_path = stored_path.resolve().relative_to(models_dir)
+ except ValueError:
+ self._logger.info("Leaving user-managed model %s at %s", key, stored_path)
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+ else:
+ # Relative paths are always relative to the models directory and thus Invoke-managed.
+ relative_path = stored_path
+
+ # If the relative path is empty, assume something is wrong. Warn and skip.
+ if not relative_path.parts:
+ self._logger.warning("Skipping model %s: empty relative path", key)
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+
+ # Sanity check: the path is relative. It should be present in the models directory.
+ absolute_path = (models_dir / relative_path).resolve()
+ if not absolute_path.exists():
+ self._logger.warning(
+ "Skipping model %s: expected model files at %s but nothing was found",
+ key,
+ absolute_path,
+ )
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+
+ if relative_path.parts[0] == key:
+ # Already normalized. Still ensure the stored path is relative.
+ normalized_path = relative_path.as_posix()
+ # If the stored path is already the normalized path, no change is needed.
+ new_relative_path = normalized_path if stored_path.as_posix() != normalized_path else None
+ return NormalizeResult(new_relative_path=new_relative_path, rollback_ops=[])
+
+ # We'll store the file operations we perform so we can roll them back if needed.
+ rollback_ops: list[tuple[Path, Path]] = []
+
+ # Destination directory is models_dir/ - a flat directory structure.
+ destination_dir = models_dir / key
+
+ try:
+ if absolute_path.is_file():
+ destination_dir.mkdir(parents=True, exist_ok=True)
+ dest_file = destination_dir / absolute_path.name
+ # This really shouldn't happen.
+ if dest_file.exists():
+ self._logger.warning(
+ "Destination for model %s already exists at %s; skipping move",
+ key,
+ dest_file,
+ )
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+
+ self._logger.info("Moving model file %s -> %s", absolute_path, dest_file)
+
+ # `Path.rename()` effectively moves the file or directory.
+ absolute_path.rename(dest_file)
+ rollback_ops.append((dest_file, absolute_path))
+
+ return NormalizeResult(
+ new_relative_path=(Path(key) / dest_file.name).as_posix(),
+ rollback_ops=rollback_ops,
+ )
+
+ if absolute_path.is_dir():
+ dest_path = destination_dir
+ # This really shouldn't happen.
+ if dest_path.exists():
+ self._logger.warning(
+ "Destination directory %s already exists for model %s; skipping",
+ dest_path,
+ key,
+ )
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+
+ self._logger.info("Moving model directory %s -> %s", absolute_path, dest_path)
+
+ # `Path.rename()` effectively moves the file or directory.
+ absolute_path.rename(dest_path)
+ rollback_ops.append((dest_path, absolute_path))
+
+ return NormalizeResult(
+ new_relative_path=Path(key).as_posix(),
+ rollback_ops=rollback_ops,
+ )
+
+ # Maybe a broken symlink or something else weird?
+ self._logger.warning("Skipping model %s: path %s is neither a file nor directory", key, absolute_path)
+ return NormalizeResult(new_relative_path=None, rollback_ops=[])
+ except Exception:
+ self._rollback_file_ops(rollback_ops)
+ raise
+
+ def _rollback_file_ops(self, rollback_ops: list[tuple[Path, Path]]) -> None:
+ # This is a super-simple rollback that just reverses the move operations we performed.
+ for source, destination in reversed(rollback_ops):
+ try:
+ if source.exists():
+ source.rename(destination)
+ except Exception as err:
+ self._logger.error("Failed to rollback move %s -> %s: %s", source, destination, err)
+
+ def _prune_empty_directories(self) -> None:
+ # These directories are system directories we want to keep even if empty. Technically, the app should not
+ # have any problems if these are removed, creating them as needed, but it's cleaner to just leave them alone.
+ keep_names = {"model_images", ".download_cache"}
+ keep_dirs = {self._models_dir / name for name in keep_names}
+ removed_dirs: set[Path] = set()
+
+ # Walk the models directory tree from the bottom up, removing empty directories. We sort by path length
+ # descending to ensure we visit children before parents.
+ for directory in sorted(self._models_dir.rglob("*"), key=lambda p: len(p.parts), reverse=True):
+ if not directory.is_dir():
+ continue
+ if directory == self._models_dir:
+ continue
+ if any(directory == keep or keep in directory.parents for keep in keep_dirs):
+ continue
+
+ try:
+ next(directory.iterdir())
+ except StopIteration:
+ try:
+ directory.rmdir()
+ removed_dirs.add(directory)
+ self._logger.debug("Removed empty directory %s", directory)
+ except OSError:
+ # Directory not empty (or some other error) - bail out.
+ self._logger.warning("Failed to prune directory %s - not empty?", directory)
+ continue
+ except OSError:
+ continue
+
+ self._logger.info("Pruned %d empty directories under %s", len(removed_dirs), self._models_dir)
+
+
+def build_migration_24(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """Builds the migration object for migrating from version 23 to version 24.
+
+ This migration normalizes on-disk model storage so that each model lives within
+ a directory named by its key inside the Invoke-managed models directory, and
+ updates database records to reference the new relative paths.
+
+ This migration behaves a bit differently than others. Because it involves FS operations, if we rolled the
+ DB back on any failure, we could leave the FS out of sync with the DB. Instead, we use savepoints
+ to roll back individual model updates on failure, and we roll back any FS operations we performed
+ for that model.
+
+ If a model cannot be migrated for any reason (invalid config, missing files, FS errors, DB errors), we log a
+ warning and skip it, leaving it in its original state and location. The model will still work, but it will be in
+ the "wrong" location on disk.
+ """
+
+ return Migration(
+ from_version=23,
+ to_version=24,
+ callback=Migration24Callback(app_config=app_config, logger=logger),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_25.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_25.py
new file mode 100644
index 00000000000..0ce8a8ff6a5
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_25.py
@@ -0,0 +1,61 @@
+import json
+import sqlite3
+from logging import Logger
+from typing import Any
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.backend.model_manager.taxonomy import ModelType, Qwen3VariantType
+
+
+class Migration25Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute("SELECT id, config FROM models;")
+ rows = cursor.fetchall()
+
+ migrated_count = 0
+
+ for model_id, config_json in rows:
+ try:
+ config_dict: dict[str, Any] = json.loads(config_json)
+
+ if config_dict.get("type") != ModelType.Qwen3Encoder.value:
+ continue
+
+ if "variant" in config_dict:
+ continue
+
+ config_dict["variant"] = Qwen3VariantType.Qwen3_4B.value
+
+ cursor.execute(
+ "UPDATE models SET config = ? WHERE id = ?;",
+ (json.dumps(config_dict), model_id),
+ )
+ migrated_count += 1
+
+ except json.JSONDecodeError as e:
+ self._logger.error("Invalid config JSON for model %s: %s", model_id, e)
+ raise
+
+ if migrated_count > 0:
+ self._logger.info(f"Migration complete: {migrated_count} Qwen3 encoder configs updated with variant field")
+ else:
+ self._logger.info("Migration complete: no Qwen3 encoder configs needed migration")
+
+
+def build_migration_25(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """Builds the migration object for migrating from version 24 to version 25.
+
+ This migration adds the variant field to existing Qwen3 encoder models.
+ Models installed before the variant field was added will default to Qwen3_4B (for Z-Image compatibility).
+ """
+
+ return Migration(
+ from_version=24,
+ to_version=25,
+ callback=Migration25Callback(app_config=app_config, logger=logger),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_26.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_26.py
new file mode 100644
index 00000000000..d392d284139
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_26.py
@@ -0,0 +1,115 @@
+import json
+import sqlite3
+from logging import Logger
+from pathlib import Path
+from typing import Any
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType, ZImageVariantType
+
+
+class Migration26Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ def _detect_variant_from_scheduler(self, model_path: Path) -> ZImageVariantType:
+ """Detect Z-Image variant from scheduler config for Diffusers models.
+
+ Z-Image variants are distinguished by the scheduler shift value:
+ - Turbo (distilled): shift = 3.0
+ - Base (undistilled): shift = 6.0
+ """
+ scheduler_config_path = model_path / "scheduler" / "scheduler_config.json"
+
+ if not scheduler_config_path.exists():
+ return ZImageVariantType.Turbo
+
+ try:
+ with open(scheduler_config_path, "r", encoding="utf-8") as f:
+ scheduler_config = json.load(f)
+
+ shift = scheduler_config.get("shift", 3.0)
+
+ # ZBase (undistilled) uses shift = 6.0, Turbo uses shift = 3.0
+ if shift >= 5.0:
+ return ZImageVariantType.ZBase
+ else:
+ return ZImageVariantType.Turbo
+ except (json.JSONDecodeError, OSError) as e:
+ self._logger.warning(f"Could not read scheduler config: {e}, defaulting to Turbo")
+ return ZImageVariantType.Turbo
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute("SELECT id, config FROM models;")
+ rows = cursor.fetchall()
+
+ migrated_turbo = 0
+ migrated_base = 0
+
+ for model_id, config_json in rows:
+ try:
+ config_dict: dict[str, Any] = json.loads(config_json)
+
+ # Only migrate Z-Image main models
+ if config_dict.get("base") != BaseModelType.ZImage.value:
+ continue
+
+ if config_dict.get("type") != ModelType.Main.value:
+ continue
+
+ # Skip if variant already set
+ if "variant" in config_dict:
+ continue
+
+ # Determine variant based on format
+ model_format = config_dict.get("format")
+ model_path = config_dict.get("path")
+
+ if model_format == ModelFormat.Diffusers.value and model_path:
+ # For Diffusers models, detect from scheduler config
+ variant = self._detect_variant_from_scheduler(Path(model_path))
+ else:
+ # For Checkpoint/GGUF, default to Turbo (Base only available as Diffusers)
+ variant = ZImageVariantType.Turbo
+
+ config_dict["variant"] = variant.value
+
+ cursor.execute(
+ "UPDATE models SET config = ? WHERE id = ?;",
+ (json.dumps(config_dict), model_id),
+ )
+
+ if variant == ZImageVariantType.ZBase:
+ migrated_base += 1
+ else:
+ migrated_turbo += 1
+
+ except json.JSONDecodeError as e:
+ self._logger.error("Invalid config JSON for model %s: %s", model_id, e)
+ raise
+
+ total = migrated_turbo + migrated_base
+ if total > 0:
+ self._logger.info(
+ f"Migration complete: {total} Z-Image model configs updated "
+ f"({migrated_turbo} Turbo, {migrated_base} Base)"
+ )
+ else:
+ self._logger.info("Migration complete: no Z-Image model configs needed migration")
+
+
+def build_migration_26(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """Builds the migration object for migrating from version 25 to version 26.
+
+ This migration adds the variant field to existing Z-Image main models.
+ Models installed before the variant field was added will default to Turbo
+ (the only variant available before Z-Image Base support was added).
+ """
+
+ return Migration(
+ from_version=25,
+ to_version=26,
+ callback=Migration26Callback(app_config=app_config, logger=logger),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_27.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_27.py
new file mode 100644
index 00000000000..b80ea073ef8
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_27.py
@@ -0,0 +1,366 @@
+"""Migration 27: Add multi-user support, per-user client state, and app settings.
+
+This migration adds the database schema for multi-user support, including:
+- users table for user accounts
+- user_sessions table for session management
+- user_invitations table for invitation system
+- shared_boards table for board sharing
+- Adding user_id columns to existing tables for data ownership
+- Restructuring client_state table to support per-user storage
+- app_settings table for storing JWT secret and other app-level settings
+"""
+
+import json
+import secrets
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration27Callback:
+ """Migration to add multi-user support, per-user client state, and app settings."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._create_users_table(cursor)
+ self._create_user_sessions_table(cursor)
+ self._create_user_invitations_table(cursor)
+ self._create_shared_boards_table(cursor)
+ self._update_boards_table(cursor)
+ self._update_images_table(cursor)
+ self._update_workflows_table(cursor)
+ self._update_session_queue_table(cursor)
+ self._update_style_presets_table(cursor)
+ self._create_system_user(cursor)
+ self._update_client_state_table(cursor)
+ self._create_app_settings_table(cursor)
+ self._generate_jwt_secret(cursor)
+
+ def _create_users_table(self, cursor: sqlite3.Cursor) -> None:
+ """Create users table."""
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS users (
+ user_id TEXT NOT NULL PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ display_name TEXT,
+ password_hash TEXT NOT NULL,
+ is_admin BOOLEAN NOT NULL DEFAULT FALSE,
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ last_login_at DATETIME
+ );
+ """)
+
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_is_admin ON users(is_admin);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);")
+
+ cursor.execute("""
+ CREATE TRIGGER IF NOT EXISTS tg_users_updated_at
+ AFTER UPDATE ON users FOR EACH ROW
+ BEGIN
+ UPDATE users SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE user_id = old.user_id;
+ END;
+ """)
+
+ def _create_user_sessions_table(self, cursor: sqlite3.Cursor) -> None:
+ """Create user_sessions table for session management."""
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS user_sessions (
+ session_id TEXT NOT NULL PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ token_hash TEXT NOT NULL,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ last_activity_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """)
+
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_sessions_token_hash ON user_sessions(token_hash);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);")
+
+ def _create_user_invitations_table(self, cursor: sqlite3.Cursor) -> None:
+ """Create user_invitations table for invitation system."""
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS user_invitations (
+ invitation_id TEXT NOT NULL PRIMARY KEY,
+ email TEXT NOT NULL,
+ invited_by TEXT NOT NULL,
+ invitation_code TEXT NOT NULL UNIQUE,
+ is_admin BOOLEAN NOT NULL DEFAULT FALSE,
+ expires_at DATETIME NOT NULL,
+ used_at DATETIME,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ FOREIGN KEY (invited_by) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """)
+
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_invitations_email ON user_invitations(email);")
+ cursor.execute(
+ "CREATE INDEX IF NOT EXISTS idx_user_invitations_invitation_code ON user_invitations(invitation_code);"
+ )
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_invitations_expires_at ON user_invitations(expires_at);")
+
+ def _create_shared_boards_table(self, cursor: sqlite3.Cursor) -> None:
+ """Create shared_boards table for board sharing."""
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS shared_boards (
+ board_id TEXT NOT NULL,
+ user_id TEXT NOT NULL,
+ can_edit BOOLEAN NOT NULL DEFAULT FALSE,
+ shared_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ PRIMARY KEY (board_id, user_id),
+ FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE,
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """)
+
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_shared_boards_user_id ON shared_boards(user_id);")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_shared_boards_board_id ON shared_boards(board_id);")
+
+ def _update_boards_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id and is_public columns to boards table."""
+ # Check if boards table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='boards';")
+ if cursor.fetchone() is None:
+ return
+
+ # Check if user_id column exists
+ cursor.execute("PRAGMA table_info(boards);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE boards ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_boards_user_id ON boards(user_id);")
+
+ if "is_public" not in columns:
+ cursor.execute("ALTER TABLE boards ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_boards_is_public ON boards(is_public);")
+
+ def _update_images_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id column to images table."""
+ # Check if images table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='images';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(images);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE images ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_images_user_id ON images(user_id);")
+
+ def _update_workflows_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id and is_public columns to workflows table."""
+ # Check if workflows table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='workflows';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(workflows);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE workflows ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_workflows_user_id ON workflows(user_id);")
+
+ if "is_public" not in columns:
+ cursor.execute("ALTER TABLE workflows ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_workflows_is_public ON workflows(is_public);")
+
+ def _update_session_queue_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id column to session_queue table."""
+ # Check if session_queue table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='session_queue';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(session_queue);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_session_queue_user_id ON session_queue(user_id);")
+
+ def _update_style_presets_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id and is_public columns to style_presets table."""
+ # Check if style_presets table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='style_presets';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(style_presets);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE style_presets ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_style_presets_user_id ON style_presets(user_id);")
+
+ if "is_public" not in columns:
+ cursor.execute("ALTER TABLE style_presets ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_style_presets_is_public ON style_presets(is_public);")
+
+ def _create_system_user(self, cursor: sqlite3.Cursor) -> None:
+ """Create system user for backward compatibility.
+
+ The system user is NOT an admin - it's just used to own existing data
+ from before multi-user support was added. Real admin users should be
+ created through the /auth/setup endpoint.
+ """
+ cursor.execute("""
+ INSERT OR IGNORE INTO users (user_id, email, display_name, password_hash, is_admin, is_active)
+ VALUES ('system', 'system@system.invokeai', 'System', '', FALSE, TRUE);
+ """)
+
+ def _update_client_state_table(self, cursor: sqlite3.Cursor) -> None:
+ """Restructure client_state table to support per-user storage."""
+ # Check if client_state table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='client_state';")
+ if cursor.fetchone() is None:
+ # Table doesn't exist, create it with the new schema
+ cursor.execute(
+ """
+ CREATE TABLE client_state (
+ user_id TEXT NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT NOT NULL,
+ updated_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP),
+ PRIMARY KEY (user_id, key),
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """
+ )
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_client_state_user_id ON client_state(user_id);")
+ cursor.execute(
+ """
+ CREATE TRIGGER tg_client_state_updated_at
+ AFTER UPDATE ON client_state
+ FOR EACH ROW
+ BEGIN
+ UPDATE client_state
+ SET updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = OLD.user_id AND key = OLD.key;
+ END;
+ """
+ )
+ return
+
+ # Table exists with old schema - migrate it
+ # Get existing data if the data column is present (it may be absent if an older
+ # version of migration 21 was deployed without the column)
+ cursor.execute("PRAGMA table_info(client_state);")
+ columns = [row[1] for row in cursor.fetchall()]
+ existing_data = {}
+ if "data" in columns:
+ cursor.execute("SELECT data FROM client_state WHERE id = 1;")
+ row = cursor.fetchone()
+ if row is not None:
+ try:
+ existing_data = json.loads(row[0])
+ except (json.JSONDecodeError, TypeError):
+ # If data is corrupt, just start fresh
+ pass
+
+ # Drop the old table
+ cursor.execute("DROP TABLE IF EXISTS client_state;")
+
+ # Create new table with per-user schema
+ cursor.execute(
+ """
+ CREATE TABLE client_state (
+ user_id TEXT NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT NOT NULL,
+ updated_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP),
+ PRIMARY KEY (user_id, key),
+ FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
+ );
+ """
+ )
+
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_client_state_user_id ON client_state(user_id);")
+
+ cursor.execute(
+ """
+ CREATE TRIGGER tg_client_state_updated_at
+ AFTER UPDATE ON client_state
+ FOR EACH ROW
+ BEGIN
+ UPDATE client_state
+ SET updated_at = CURRENT_TIMESTAMP
+ WHERE user_id = OLD.user_id AND key = OLD.key;
+ END;
+ """
+ )
+
+ # Migrate existing data to 'system' user
+ for key, value in existing_data.items():
+ cursor.execute(
+ """
+ INSERT INTO client_state (user_id, key, value)
+ VALUES ('system', ?, ?);
+ """,
+ (key, value),
+ )
+
+ def _create_app_settings_table(self, cursor: sqlite3.Cursor) -> None:
+ """Create app_settings table for storing application-level configuration."""
+ cursor.execute(
+ """
+ CREATE TABLE IF NOT EXISTS app_settings (
+ key TEXT NOT NULL PRIMARY KEY,
+ value TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
+ );
+ """
+ )
+
+ cursor.execute(
+ """
+ CREATE TRIGGER IF NOT EXISTS tg_app_settings_updated_at
+ AFTER UPDATE ON app_settings
+ FOR EACH ROW
+ BEGIN
+ UPDATE app_settings SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE key = OLD.key;
+ END;
+ """
+ )
+
+ def _generate_jwt_secret(self, cursor: sqlite3.Cursor) -> None:
+ """Generate and store a cryptographically secure JWT secret key.
+
+ The secret is a 64-character hexadecimal string (256 bits of entropy),
+ which is suitable for HS256 JWT signing.
+ """
+ # Check if JWT secret already exists
+ cursor.execute("SELECT value FROM app_settings WHERE key = 'jwt_secret';")
+ existing_secret = cursor.fetchone()
+
+ if existing_secret is None:
+ # Generate a new cryptographically secure secret (256 bits)
+ jwt_secret = secrets.token_hex(32) # 32 bytes = 256 bits = 64 hex characters
+
+ # Store in database
+ cursor.execute(
+ "INSERT INTO app_settings (key, value) VALUES ('jwt_secret', ?);",
+ (jwt_secret,),
+ )
+
+
+def build_migration_27() -> Migration:
+ """Builds the migration object for migrating from version 26 to version 27.
+
+ This migration adds multi-user support, per-user client state, and app settings
+ (including a JWT secret) to the database schema.
+ """
+ return Migration(
+ from_version=26,
+ to_version=27,
+ callback=Migration27Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_28.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_28.py
new file mode 100644
index 00000000000..60e5d8f19bf
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_28.py
@@ -0,0 +1,48 @@
+"""Migration 28: Add per-user workflow isolation columns to workflow_library.
+
+This migration adds the database columns required for multiuser workflow isolation
+to the workflow_library table:
+- user_id: the owner of the workflow (defaults to 'system' for existing workflows)
+- is_public: whether the workflow is shared with all users
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration28Callback:
+ """Migration to add user_id and is_public to the workflow_library table."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._update_workflow_library_table(cursor)
+
+ def _update_workflow_library_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add user_id and is_public columns to workflow_library table."""
+ cursor.execute("PRAGMA table_info(workflow_library);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "user_id" not in columns:
+ cursor.execute("ALTER TABLE workflow_library ADD COLUMN user_id TEXT DEFAULT 'system';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_workflow_library_user_id ON workflow_library(user_id);")
+
+ if "is_public" not in columns:
+ cursor.execute("ALTER TABLE workflow_library ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_workflow_library_is_public ON workflow_library(is_public);")
+ cursor.execute(
+ "UPDATE workflow_library SET is_public = TRUE WHERE user_id = 'system';"
+ ) # one-time fix for legacy workflows
+
+
+def build_migration_28() -> Migration:
+ """Builds the migration object for migrating from version 27 to version 28.
+
+ This migration adds per-user workflow isolation to the workflow_library table:
+ - user_id column: identifies the owner of each workflow
+ - is_public column: controls whether a workflow is shared with all users
+ """
+ return Migration(
+ from_version=27,
+ to_version=28,
+ callback=Migration28Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_29.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_29.py
new file mode 100644
index 00000000000..c9eb7c901ba
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_29.py
@@ -0,0 +1,53 @@
+"""Migration 29: Add board_visibility column to boards table.
+
+This migration adds a board_visibility column to the boards table to support
+three visibility levels:
+ - 'private': only the board owner (and admins) can view/modify
+ - 'shared': all users can view, but only the owner (and admins) can modify
+ - 'public': all users can view; only the owner (and admins) can modify the
+ board structure (rename/archive/delete)
+
+Existing boards with is_public = 1 are migrated to 'public'.
+All other existing boards default to 'private'.
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration29Callback:
+ """Migration to add board_visibility column to the boards table."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._update_boards_table(cursor)
+
+ def _update_boards_table(self, cursor: sqlite3.Cursor) -> None:
+ """Add board_visibility column to boards table."""
+ # Check if boards table exists
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='boards';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(boards);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "board_visibility" not in columns:
+ cursor.execute("ALTER TABLE boards ADD COLUMN board_visibility TEXT NOT NULL DEFAULT 'private';")
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_boards_board_visibility ON boards(board_visibility);")
+ # Migrate existing is_public = 1 boards to 'public'
+ if "is_public" in columns:
+ cursor.execute("UPDATE boards SET board_visibility = 'public' WHERE is_public = 1;")
+
+
+def build_migration_29() -> Migration:
+ """Builds the migration object for migrating from version 28 to version 29.
+
+ This migration adds the board_visibility column to the boards table,
+ supporting 'private', 'shared', and 'public' visibility levels.
+ """
+ return Migration(
+ from_version=28,
+ to_version=29,
+ callback=Migration29Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_3.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_3.py
new file mode 100644
index 00000000000..48eb1db8541
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_3.py
@@ -0,0 +1,70 @@
+import sqlite3
+from logging import Logger
+
+from invokeai.app.services.config import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration3Callback:
+ def __init__(self, app_config: InvokeAIAppConfig, logger: Logger) -> None:
+ self._app_config = app_config
+ self._logger = logger
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._drop_model_manager_metadata(cursor)
+ self._recreate_model_config(cursor)
+
+ def _drop_model_manager_metadata(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the `model_manager_metadata` table."""
+ cursor.execute("DROP TABLE IF EXISTS model_manager_metadata;")
+
+ def _recreate_model_config(self, cursor: sqlite3.Cursor) -> None:
+ """
+ Drops the `model_config` table, recreating it.
+
+ In 3.4.0, this table used explicit columns but was changed to use json_extract 3.5.0.
+
+ Because this table is not used in production, we are able to simply drop it and recreate it.
+ """
+
+ cursor.execute("DROP TABLE IF EXISTS model_config;")
+
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS model_config (
+ id TEXT NOT NULL PRIMARY KEY,
+ -- The next 3 fields are enums in python, unrestricted string here
+ base TEXT GENERATED ALWAYS as (json_extract(config, '$.base')) VIRTUAL NOT NULL,
+ type TEXT GENERATED ALWAYS as (json_extract(config, '$.type')) VIRTUAL NOT NULL,
+ name TEXT GENERATED ALWAYS as (json_extract(config, '$.name')) VIRTUAL NOT NULL,
+ path TEXT GENERATED ALWAYS as (json_extract(config, '$.path')) VIRTUAL NOT NULL,
+ format TEXT GENERATED ALWAYS as (json_extract(config, '$.format')) VIRTUAL NOT NULL,
+ original_hash TEXT, -- could be null
+ -- Serialized JSON representation of the whole config object,
+ -- which will contain additional fields from subclasses
+ config TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- unique constraint on combo of name, base and type
+ UNIQUE(name, base, type)
+ );
+ """
+ )
+
+
+def build_migration_3(app_config: InvokeAIAppConfig, logger: Logger) -> Migration:
+ """
+ Build the migration from database version 2 to 3.
+
+ This migration does the following:
+ - Drops the `model_config` table, recreating it
+ - Migrates data from `models.yaml` into the `model_config` table
+ """
+ migration_3 = Migration(
+ from_version=2,
+ to_version=3,
+ callback=Migration3Callback(app_config=app_config, logger=logger),
+ )
+
+ return migration_3
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_30.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_30.py
new file mode 100644
index 00000000000..d60270bfa1c
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_30.py
@@ -0,0 +1,33 @@
+"""Migration 30: Add per-item queue status sequencing.
+
+This migration adds a `status_sequence` column to `session_queue` so queue item
+status updates can be ordered across asynchronous event and snapshot channels.
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration30Callback:
+ """Add a per-queue-item status sequence for cross-channel ordering."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='session_queue';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(session_queue);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "status_sequence" not in columns:
+ cursor.execute("ALTER TABLE session_queue ADD COLUMN status_sequence INTEGER DEFAULT 0;")
+ cursor.execute("UPDATE session_queue SET status_sequence = 0 WHERE status_sequence IS NULL;")
+
+
+def build_migration_30() -> Migration:
+ return Migration(
+ from_version=29,
+ to_version=30,
+ callback=Migration30Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_31.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_31.py
new file mode 100644
index 00000000000..9f5b36a5f2d
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_31.py
@@ -0,0 +1,41 @@
+"""Migration 31: Add image_subfolder column to images table.
+
+This migration adds an image_subfolder column to the images table to support
+configurable image subfolder strategies (flat, date, type, hash).
+Existing images get an empty string (flat/root directory).
+"""
+
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration31Callback:
+ """Migration to add image_subfolder column to images table."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_image_subfolder_column(cursor)
+
+ def _add_image_subfolder_column(self, cursor: sqlite3.Cursor) -> None:
+ """Add image_subfolder column to images table."""
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='images';")
+ if cursor.fetchone() is None:
+ return
+
+ cursor.execute("PRAGMA table_info(images);")
+ columns = [row[1] for row in cursor.fetchall()]
+
+ if "image_subfolder" not in columns:
+ cursor.execute("ALTER TABLE images ADD COLUMN image_subfolder TEXT NOT NULL DEFAULT '';")
+
+
+def build_migration_31() -> Migration:
+ """Builds the migration object for migrating from version 30 to version 31.
+
+ This migration adds an image_subfolder column to the images table.
+ """
+ return Migration(
+ from_version=30,
+ to_version=31,
+ callback=Migration31Callback(),
+ )
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_4.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_4.py
new file mode 100644
index 00000000000..b8dc4dd83b4
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_4.py
@@ -0,0 +1,83 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration4Callback:
+ """Callback to do step 4 of migration."""
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None: # noqa D102
+ self._create_model_metadata(cursor)
+ self._create_model_tags(cursor)
+ self._create_tags(cursor)
+ self._create_triggers(cursor)
+
+ def _create_model_metadata(self, cursor: sqlite3.Cursor) -> None:
+ """Create the table used to store model metadata downloaded from remote sources."""
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS model_metadata (
+ id TEXT NOT NULL PRIMARY KEY,
+ name TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.name')) VIRTUAL NOT NULL,
+ author TEXT GENERATED ALWAYS AS (json_extract(metadata, '$.author')) VIRTUAL NOT NULL,
+ -- Serialized JSON representation of the whole metadata object,
+ -- which will contain additional fields from subclasses
+ metadata TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ FOREIGN KEY(id) REFERENCES model_config(id) ON DELETE CASCADE
+ );
+ """
+ )
+
+ def _create_model_tags(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS model_tags (
+ model_id TEXT NOT NULL,
+ tag_id INTEGER NOT NULL,
+ FOREIGN KEY(model_id) REFERENCES model_config(id) ON DELETE CASCADE,
+ FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON DELETE CASCADE,
+ UNIQUE(model_id,tag_id)
+ );
+ """
+ )
+
+ def _create_tags(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """--sql
+ CREATE TABLE IF NOT EXISTS tags (
+ tag_id INTEGER NOT NULL PRIMARY KEY,
+ tag_text TEXT NOT NULL UNIQUE
+ );
+ """
+ )
+
+ def _create_triggers(self, cursor: sqlite3.Cursor) -> None:
+ cursor.execute(
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS model_metadata_updated_at
+ AFTER UPDATE
+ ON model_metadata FOR EACH ROW
+ BEGIN
+ UPDATE model_metadata SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ )
+
+
+def build_migration_4() -> Migration:
+ """
+ Build the migration from database version 3 to 4.
+
+ Adds the tables needed to store model metadata and tags.
+ """
+ migration_4 = Migration(
+ from_version=3,
+ to_version=4,
+ callback=Migration4Callback(),
+ )
+
+ return migration_4
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_5.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_5.py
new file mode 100644
index 00000000000..b2e8c206d8d
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_5.py
@@ -0,0 +1,34 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration5Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._drop_graph_executions(cursor)
+
+ def _drop_graph_executions(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the `graph_executions` table."""
+
+ cursor.execute(
+ """--sql
+ DROP TABLE IF EXISTS graph_executions;
+ """
+ )
+
+
+def build_migration_5() -> Migration:
+ """
+ Build the migration from database version 4 to 5.
+
+ Introduced in v3.6.3, this migration:
+ - Drops the `graph_executions` table. We are able to do this because we are moving the graph storage
+ to be purely in-memory.
+ """
+ migration_5 = Migration(
+ from_version=4,
+ to_version=5,
+ callback=Migration5Callback(),
+ )
+
+ return migration_5
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py
new file mode 100644
index 00000000000..1f9ac56518c
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_6.py
@@ -0,0 +1,62 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration6Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._recreate_model_triggers(cursor)
+ self._delete_ip_adapters(cursor)
+
+ def _recreate_model_triggers(self, cursor: sqlite3.Cursor) -> None:
+ """
+ Adds the timestamp trigger to the model_config table.
+
+ This trigger was inadvertently dropped in earlier migration scripts.
+ """
+
+ cursor.execute(
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS model_config_updated_at
+ AFTER UPDATE
+ ON model_config FOR EACH ROW
+ BEGIN
+ UPDATE model_config SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ )
+
+ def _delete_ip_adapters(self, cursor: sqlite3.Cursor) -> None:
+ """
+ Delete all the IP adapters.
+
+ The model manager will automatically find and re-add them after the migration
+ is done. This allows the manager to add the correct image encoder to their
+ configuration records.
+ """
+
+ cursor.execute(
+ """--sql
+ DELETE FROM model_config
+ WHERE type='ip_adapter';
+ """
+ )
+
+
+def build_migration_6() -> Migration:
+ """
+ Build the migration from database version 5 to 6.
+
+ This migration does the following:
+ - Adds the model_config_updated_at trigger if it does not exist
+ - Delete all ip_adapter models so that the model prober can find and
+ update with the correct image processor model.
+ """
+ migration_6 = Migration(
+ from_version=5,
+ to_version=6,
+ callback=Migration6Callback(),
+ )
+
+ return migration_6
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_7.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_7.py
new file mode 100644
index 00000000000..fa573d63a63
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_7.py
@@ -0,0 +1,88 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration7Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._create_models_table(cursor)
+ self._drop_old_models_tables(cursor)
+
+ def _drop_old_models_tables(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the old model_records, model_metadata, model_tags and tags tables."""
+
+ tables = ["model_config", "model_metadata", "model_tags", "tags"]
+
+ for table in tables:
+ cursor.execute(f"DROP TABLE IF EXISTS {table};")
+
+ def _create_models_table(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the v4.0.0 models table."""
+
+ tables = [
+ """--sql
+ CREATE TABLE IF NOT EXISTS models (
+ id TEXT NOT NULL PRIMARY KEY,
+ hash TEXT GENERATED ALWAYS as (json_extract(config, '$.hash')) VIRTUAL NOT NULL,
+ base TEXT GENERATED ALWAYS as (json_extract(config, '$.base')) VIRTUAL NOT NULL,
+ type TEXT GENERATED ALWAYS as (json_extract(config, '$.type')) VIRTUAL NOT NULL,
+ path TEXT GENERATED ALWAYS as (json_extract(config, '$.path')) VIRTUAL NOT NULL,
+ format TEXT GENERATED ALWAYS as (json_extract(config, '$.format')) VIRTUAL NOT NULL,
+ name TEXT GENERATED ALWAYS as (json_extract(config, '$.name')) VIRTUAL NOT NULL,
+ description TEXT GENERATED ALWAYS as (json_extract(config, '$.description')) VIRTUAL,
+ source TEXT GENERATED ALWAYS as (json_extract(config, '$.source')) VIRTUAL NOT NULL,
+ source_type TEXT GENERATED ALWAYS as (json_extract(config, '$.source_type')) VIRTUAL NOT NULL,
+ source_api_response TEXT GENERATED ALWAYS as (json_extract(config, '$.source_api_response')) VIRTUAL,
+ trigger_phrases TEXT GENERATED ALWAYS as (json_extract(config, '$.trigger_phrases')) VIRTUAL,
+ -- Serialized JSON representation of the whole config object, which will contain additional fields from subclasses
+ config TEXT NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- Updated via trigger
+ updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
+ -- unique constraint on combo of name, base and type
+ UNIQUE(name, base, type)
+ );
+ """
+ ]
+
+ # Add trigger for `updated_at`.
+ triggers = [
+ """--sql
+ CREATE TRIGGER IF NOT EXISTS models_updated_at
+ AFTER UPDATE
+ ON models FOR EACH ROW
+ BEGIN
+ UPDATE models SET updated_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
+ WHERE id = old.id;
+ END;
+ """
+ ]
+
+ # Add indexes for searchable fields
+ indices = [
+ "CREATE INDEX IF NOT EXISTS base_index ON models(base);",
+ "CREATE INDEX IF NOT EXISTS type_index ON models(type);",
+ "CREATE INDEX IF NOT EXISTS name_index ON models(name);",
+ "CREATE UNIQUE INDEX IF NOT EXISTS path_index ON models(path);",
+ ]
+
+ for stmt in tables + indices + triggers:
+ cursor.execute(stmt)
+
+
+def build_migration_7() -> Migration:
+ """
+ Build the migration from database version 6 to 7.
+
+ This migration does the following:
+ - Adds the new models table
+ - Drops the old model_records, model_metadata, model_tags and tags tables.
+ - TODO(MM2): Migrates model names and descriptions from `models.yaml` to the new table (?).
+ """
+ migration_7 = Migration(
+ from_version=6,
+ to_version=7,
+ callback=Migration7Callback(),
+ )
+
+ return migration_7
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_8.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_8.py
new file mode 100644
index 00000000000..154a5236cae
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_8.py
@@ -0,0 +1,91 @@
+import sqlite3
+from pathlib import Path
+
+from invokeai.app.services.config.config_default import InvokeAIAppConfig
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration8Callback:
+ def __init__(self, app_config: InvokeAIAppConfig) -> None:
+ self._app_config = app_config
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._drop_model_config_table(cursor)
+ self._migrate_abs_models_to_rel(cursor)
+
+ def _drop_model_config_table(self, cursor: sqlite3.Cursor) -> None:
+ """Drops the old model_config table. This was missed in a previous migration."""
+
+ cursor.execute("DROP TABLE IF EXISTS model_config;")
+
+ def _migrate_abs_models_to_rel(self, cursor: sqlite3.Cursor) -> None:
+ """Check all model paths & legacy config paths to determine if they are inside Invoke-managed directories. If
+ they are, update the paths to be relative to the managed directories.
+
+ This migration is a no-op for normal users (their paths will already be relative), but is necessary for users
+ who have been testing the RCs with their live databases. The paths were made absolute in the initial RC, but this
+ change was reverted. To smooth over the revert for our tests, we can migrate the paths back to relative.
+ """
+
+ models_path = self._app_config.models_path
+ legacy_conf_path = self._app_config.legacy_conf_path
+ legacy_conf_dir = self._app_config.legacy_conf_dir
+
+ stmt = """---sql
+ SELECT
+ id,
+ path,
+ json_extract(config, '$.config_path') as config_path
+ FROM models;
+ """
+
+ all_models = cursor.execute(stmt).fetchall()
+
+ for model_id, model_path, model_config_path in all_models:
+ # If the model path is inside the models directory, update it to be relative to the models directory.
+ if Path(model_path).is_relative_to(models_path):
+ new_path = Path(model_path).relative_to(models_path)
+ cursor.execute(
+ """--sql
+ UPDATE models
+ SET config = json_set(config, '$.path', ?)
+ WHERE id = ?;
+ """,
+ (str(new_path), model_id),
+ )
+ # If the model has a legacy config path and it is inside the legacy conf directory, update it to be
+ # relative to the legacy conf directory. This also fixes up cases in which the config path was
+ # incorrectly relativized to the root directory. It will now be relativized to the legacy conf directory.
+ if model_config_path:
+ if Path(model_config_path).is_relative_to(legacy_conf_path):
+ new_config_path = Path(model_config_path).relative_to(legacy_conf_path)
+ elif Path(model_config_path).is_relative_to(legacy_conf_dir):
+ new_config_path = Path(*Path(model_config_path).parts[1:])
+ else:
+ new_config_path = None
+ if new_config_path:
+ cursor.execute(
+ """--sql
+ UPDATE models
+ SET config = json_set(config, '$.config_path', ?)
+ WHERE id = ?;
+ """,
+ (str(new_config_path), model_id),
+ )
+
+
+def build_migration_8(app_config: InvokeAIAppConfig) -> Migration:
+ """
+ Build the migration from database version 7 to 8.
+
+ This migration does the following:
+ - Removes the `model_config` table.
+ - Migrates absolute model & legacy config paths to be relative to the models directory.
+ """
+ migration_8 = Migration(
+ from_version=7,
+ to_version=8,
+ callback=Migration8Callback(app_config),
+ )
+
+ return migration_8
diff --git a/invokeai/app/services/shared/sqlite_migrator/migrations/migration_9.py b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_9.py
new file mode 100644
index 00000000000..acc4ef5017d
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/migrations/migration_9.py
@@ -0,0 +1,29 @@
+import sqlite3
+
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
+
+
+class Migration9Callback:
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._empty_session_queue(cursor)
+
+ def _empty_session_queue(self, cursor: sqlite3.Cursor) -> None:
+ """Empties the session queue. This is done to prevent any lingering session queue items from causing pydantic errors due to changed schemas."""
+
+ cursor.execute("DELETE FROM session_queue;")
+
+
+def build_migration_9() -> Migration:
+ """
+ Build the migration from database version 8 to 9.
+
+ This migration does the following:
+ - Empties the session queue. This is done to prevent any lingering session queue items from causing pydantic errors due to changed schemas.
+ """
+ migration_9 = Migration(
+ from_version=8,
+ to_version=9,
+ callback=Migration9Callback(),
+ )
+
+ return migration_9
diff --git a/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_common.py b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_common.py
new file mode 100644
index 00000000000..9b2444dae4b
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_common.py
@@ -0,0 +1,163 @@
+import sqlite3
+from typing import Optional, Protocol, runtime_checkable
+
+from pydantic import BaseModel, ConfigDict, Field, model_validator
+
+
+@runtime_checkable
+class MigrateCallback(Protocol):
+ """
+ A callback that performs a migration.
+
+ Migrate callbacks are provided an open cursor to the database. They should not commit their
+ transaction; this is handled by the migrator.
+
+ If the callback needs to access additional dependencies, will be provided to the callback at runtime.
+
+ See :class:`Migration` for an example.
+ """
+
+ def __call__(self, cursor: sqlite3.Cursor) -> None: ...
+
+
+class MigrationError(RuntimeError):
+ """Raised when a migration fails."""
+
+
+class MigrationVersionError(ValueError):
+ """Raised when a migration version is invalid."""
+
+
+class Migration(BaseModel):
+ """
+ Represents a migration for a SQLite database.
+
+ :param from_version: The database version on which this migration may be run
+ :param to_version: The database version that results from this migration
+ :param migrate_callback: The callback to run to perform the migration
+
+ Migration callbacks will be provided an open cursor to the database. They should not commit their
+ transaction; this is handled by the migrator.
+
+ It is suggested to use a class to define the migration callback and a builder function to create
+ the :class:`Migration`. This allows the callback to be provided with additional dependencies and
+ keeps things tidy, as all migration logic is self-contained.
+
+ Example:
+ ```py
+ # Define the migration callback class
+ class Migration1Callback:
+ # This migration needs a logger, so we define a class that accepts a logger in its constructor.
+ def __init__(self, image_files: ImageFileStorageBase) -> None:
+ self._image_files = ImageFileStorageBase
+
+ # This dunder method allows the instance of the class to be called like a function.
+ def __call__(self, cursor: sqlite3.Cursor) -> None:
+ self._add_with_banana_column(cursor)
+ self._do_something_with_images(cursor)
+
+ def _add_with_banana_column(self, cursor: sqlite3.Cursor) -> None:
+ \"""Adds the with_banana column to the sushi table.\"""
+ # Execute SQL using the cursor, taking care to *not commit* a transaction
+ cursor.execute('ALTER TABLE sushi ADD COLUMN with_banana BOOLEAN DEFAULT TRUE;')
+
+ def _do_something_with_images(self, cursor: sqlite3.Cursor) -> None:
+ \"""Does something with the image files service.\"""
+ self._image_files.get(...)
+
+ # Define the migration builder function. This function creates an instance of the migration callback
+ # class and returns a Migration.
+ def build_migration_1(image_files: ImageFileStorageBase) -> Migration:
+ \"""Builds the migration from database version 0 to 1.
+ Requires the image files service to...
+ \"""
+
+ migration_1 = Migration(
+ from_version=0,
+ to_version=1,
+ migrate_callback=Migration1Callback(image_files=image_files),
+ )
+
+ return migration_1
+
+ # Register the migration after all dependencies have been initialized
+ db = SqliteDatabase(db_path, logger)
+ migrator = SqliteMigrator(db)
+ migrator.register_migration(build_migration_1(image_files))
+ migrator.run_migrations()
+ ```
+ """
+
+ from_version: int = Field(ge=0, strict=True, description="The database version on which this migration may be run")
+ to_version: int = Field(ge=1, strict=True, description="The database version that results from this migration")
+ callback: MigrateCallback = Field(description="The callback to run to perform the migration")
+
+ @model_validator(mode="after")
+ def validate_to_version(self) -> "Migration":
+ """Validates that to_version is one greater than from_version."""
+ if self.to_version != self.from_version + 1:
+ raise MigrationVersionError("to_version must be one greater than from_version")
+ return self
+
+ def __hash__(self) -> int:
+ # Callables are not hashable, so we need to implement our own __hash__ function to use this class in a set.
+ return hash((self.from_version, self.to_version))
+
+ model_config = ConfigDict(arbitrary_types_allowed=True)
+
+
+class MigrationSet:
+ """
+ A set of Migrations. Performs validation during migration registration and provides utility methods.
+
+ Migrations should be registered with `register()`. Once all are registered, `validate_migration_chain()`
+ should be called to ensure that the migrations form a single chain of migrations from version 0 to the latest version.
+ """
+
+ def __init__(self) -> None:
+ self._migrations: set[Migration] = set()
+
+ def register(self, migration: Migration) -> None:
+ """Registers a migration."""
+ migration_from_already_registered = any(m.from_version == migration.from_version for m in self._migrations)
+ migration_to_already_registered = any(m.to_version == migration.to_version for m in self._migrations)
+ if migration_from_already_registered or migration_to_already_registered:
+ raise MigrationVersionError("Migration with from_version or to_version already registered")
+ self._migrations.add(migration)
+
+ def get(self, from_version: int) -> Optional[Migration]:
+ """Gets the migration that may be run on the given database version."""
+ # register() ensures that there is only one migration with a given from_version, so this is safe.
+ return next((m for m in self._migrations if m.from_version == from_version), None)
+
+ def validate_migration_chain(self) -> None:
+ """
+ Validates that the migrations form a single chain of migrations from version 0 to the latest version,
+ Raises a MigrationError if there is a problem.
+ """
+ if self.count == 0:
+ return
+ if self.latest_version == 0:
+ return
+ next_migration = self.get(from_version=0)
+ if next_migration is None:
+ raise MigrationError("Migration chain is fragmented")
+ touched_count = 1
+ while next_migration is not None:
+ next_migration = self.get(next_migration.to_version)
+ if next_migration is not None:
+ touched_count += 1
+ if touched_count != self.count:
+ raise MigrationError("Migration chain is fragmented")
+
+ @property
+ def count(self) -> int:
+ """The count of registered migrations."""
+ return len(self._migrations)
+
+ @property
+ def latest_version(self) -> int:
+ """Gets latest to_version among registered migrations. Returns 0 if there are no migrations registered."""
+ if self.count == 0:
+ return 0
+ return sorted(self._migrations, key=lambda m: m.to_version)[-1].to_version
diff --git a/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py
new file mode 100644
index 00000000000..310abf05200
--- /dev/null
+++ b/invokeai/app/services/shared/sqlite_migrator/sqlite_migrator_impl.py
@@ -0,0 +1,143 @@
+import sqlite3
+from contextlib import closing
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
+
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration, MigrationError, MigrationSet
+
+
+class SqliteMigrator:
+ """
+ Manages migrations for a SQLite database.
+
+ :param db: The instance of :class:`SqliteDatabase` to migrate.
+
+ Migrations should be registered with :meth:`register_migration`.
+
+ Each migration is run in a transaction. If a migration fails, the transaction is rolled back.
+
+ Example Usage:
+ ```py
+ db = SqliteDatabase(db_path="my_db.db", logger=logger)
+ migrator = SqliteMigrator(db=db)
+ migrator.register_migration(build_migration_1())
+ migrator.register_migration(build_migration_2())
+ migrator.run_migrations()
+ ```
+ """
+
+ backup_path: Optional[Path] = None
+
+ def __init__(self, db: SqliteDatabase) -> None:
+ self._db = db
+ self._logger = db._logger
+ self._migration_set = MigrationSet()
+ self._backup_path: Optional[Path] = None
+
+ def register_migration(self, migration: Migration) -> None:
+ """Registers a migration."""
+ self._migration_set.register(migration)
+ self._logger.debug(f"Registered migration {migration.from_version} -> {migration.to_version}")
+
+ def run_migrations(self) -> bool:
+ """Migrates the database to the latest version."""
+ # This throws if there is a problem.
+ self._migration_set.validate_migration_chain()
+ cursor = self._db._conn.cursor()
+ self._create_migrations_table(cursor=cursor)
+
+ if self._migration_set.count == 0:
+ self._logger.debug("No migrations registered")
+ return False
+
+ if self._get_current_version(cursor=cursor) == self._migration_set.latest_version:
+ self._logger.debug("Database is up to date, no migrations to run")
+ return False
+
+ self._logger.info("Database update needed")
+
+ # Make a backup of the db if it needs to be updated and is a file db
+ if self._db._db_path is not None:
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
+ self._backup_path = self._db._db_path.parent / f"{self._db._db_path.stem}_backup_{timestamp}.db"
+ self._logger.info(f"Backing up database to {str(self._backup_path)}")
+ # Use SQLite to do the backup
+ with closing(sqlite3.connect(self._backup_path)) as backup_conn:
+ self._db._conn.backup(backup_conn)
+ else:
+ self._logger.info("Using in-memory database, no backup needed")
+
+ next_migration = self._migration_set.get(from_version=self._get_current_version(cursor))
+ while next_migration is not None:
+ self._run_migration(next_migration)
+ next_migration = self._migration_set.get(self._get_current_version(cursor))
+ self._logger.info("Database updated successfully")
+ return True
+
+ def _run_migration(self, migration: Migration) -> None:
+ """Runs a single migration."""
+ try:
+ # Using sqlite3.Connection as a context manager commits a the transaction on exit, or rolls it back if an
+ # exception is raised.
+ with self._db._conn as conn:
+ cursor = conn.cursor()
+ if self._get_current_version(cursor) != migration.from_version:
+ raise MigrationError(
+ f"Database is at version {self._get_current_version(cursor)}, expected {migration.from_version}"
+ )
+ self._logger.debug(f"Running migration from {migration.from_version} to {migration.to_version}")
+
+ # Run the actual migration
+ migration.callback(cursor)
+
+ # Update the version
+ cursor.execute("INSERT INTO migrations (version) VALUES (?);", (migration.to_version,))
+
+ self._logger.debug(
+ f"Successfully migrated database from {migration.from_version} to {migration.to_version}"
+ )
+ # We want to catch *any* error, mirroring the behaviour of the sqlite3 module.
+ except Exception as e:
+ # The connection context manager has already rolled back the migration, so we don't need to do anything.
+ msg = f"Error migrating database from {migration.from_version} to {migration.to_version}: {e}"
+ self._logger.error(msg)
+ raise MigrationError(msg) from e
+
+ def _create_migrations_table(self, cursor: sqlite3.Cursor) -> None:
+ """Creates the migrations table for the database, if one does not already exist."""
+ try:
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations';")
+ if cursor.fetchone() is not None:
+ return
+ cursor.execute(
+ """--sql
+ CREATE TABLE migrations (
+ version INTEGER PRIMARY KEY,
+ migrated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
+ );
+ """
+ )
+ cursor.execute("INSERT INTO migrations (version) VALUES (0);")
+ cursor.connection.commit()
+ self._logger.debug("Created migrations table")
+ except sqlite3.Error as e:
+ msg = f"Problem creating migrations table: {e}"
+ self._logger.error(msg)
+ cursor.connection.rollback()
+ raise MigrationError(msg) from e
+
+ @classmethod
+ def _get_current_version(cls, cursor: sqlite3.Cursor) -> int:
+ """Gets the current version of the database, or 0 if the migrations table does not exist."""
+ try:
+ cursor.execute("SELECT MAX(version) FROM migrations;")
+ version: int = cursor.fetchone()[0]
+ if version is None:
+ return 0
+ return version
+ except sqlite3.OperationalError as e:
+ if "no such table" in str(e):
+ return 0
+ raise
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Anime.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Anime.png
new file mode 100644
index 00000000000..def6dce2592
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Anime.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Architectural Visualization.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Architectural Visualization.png
new file mode 100644
index 00000000000..97a2e74772f
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Architectural Visualization.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Character).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Character).png
new file mode 100644
index 00000000000..5db78ce086f
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Character).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Fantasy).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Fantasy).png
new file mode 100644
index 00000000000..93c3c5c301a
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Fantasy).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Painterly).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Painterly).png
new file mode 100644
index 00000000000..5d3d0c4af6e
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Painterly).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Sci-Fi).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Sci-Fi).png
new file mode 100644
index 00000000000..3f287fc3359
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Concept Art (Sci-Fi).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Environment Art.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Environment Art.png
new file mode 100644
index 00000000000..a0e1cbfb423
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Environment Art.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Illustration.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Illustration.png
new file mode 100644
index 00000000000..5b5976c4f95
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Illustration.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Interior Design (Visualization).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Interior Design (Visualization).png
new file mode 100644
index 00000000000..5c784103771
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Interior Design (Visualization).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Line Art.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Line Art.png
new file mode 100644
index 00000000000..b8cdfea030f
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Line Art.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Black and White).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Black and White).png
new file mode 100644
index 00000000000..b47da9fb941
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Black and White).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (General).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (General).png
new file mode 100644
index 00000000000..a034cd197bc
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (General).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Landscape).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Landscape).png
new file mode 100644
index 00000000000..5985fb6c4b2
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Landscape).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Portrait).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Portrait).png
new file mode 100644
index 00000000000..7718735b23f
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Portrait).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Studio Lighting).png b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Studio Lighting).png
new file mode 100644
index 00000000000..60bd40b1fa8
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Photography (Studio Lighting).png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Product Rendering.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Product Rendering.png
new file mode 100644
index 00000000000..4a426f47692
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Product Rendering.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Sketch.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Sketch.png
new file mode 100644
index 00000000000..08d240a29e6
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Sketch.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/Vehicles.png b/invokeai/app/services/style_preset_images/default_style_preset_images/Vehicles.png
new file mode 100644
index 00000000000..73c4c8db087
Binary files /dev/null and b/invokeai/app/services/style_preset_images/default_style_preset_images/Vehicles.png differ
diff --git a/invokeai/app/services/style_preset_images/default_style_preset_images/__init__.py b/invokeai/app/services/style_preset_images/default_style_preset_images/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/style_preset_images/style_preset_images_base.py b/invokeai/app/services/style_preset_images/style_preset_images_base.py
new file mode 100644
index 00000000000..d8158ad2ae2
--- /dev/null
+++ b/invokeai/app/services/style_preset_images/style_preset_images_base.py
@@ -0,0 +1,33 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+
+from PIL.Image import Image as PILImageType
+
+
+class StylePresetImageFileStorageBase(ABC):
+ """Low-level service responsible for storing and retrieving image files."""
+
+ @abstractmethod
+ def get(self, style_preset_id: str) -> PILImageType:
+ """Retrieves a style preset image as PIL Image."""
+ pass
+
+ @abstractmethod
+ def get_path(self, style_preset_id: str) -> Path:
+ """Gets the internal path to a style preset image."""
+ pass
+
+ @abstractmethod
+ def get_url(self, style_preset_id: str) -> str | None:
+ """Gets the URL to fetch a style preset image."""
+ pass
+
+ @abstractmethod
+ def save(self, style_preset_id: str, image: PILImageType) -> None:
+ """Saves a style preset image."""
+ pass
+
+ @abstractmethod
+ def delete(self, style_preset_id: str) -> None:
+ """Deletes a style preset image."""
+ pass
diff --git a/invokeai/app/services/style_preset_images/style_preset_images_common.py b/invokeai/app/services/style_preset_images/style_preset_images_common.py
new file mode 100644
index 00000000000..054a12b82b7
--- /dev/null
+++ b/invokeai/app/services/style_preset_images/style_preset_images_common.py
@@ -0,0 +1,19 @@
+class StylePresetImageFileNotFoundException(Exception):
+ """Raised when an image file is not found in storage."""
+
+ def __init__(self, message: str = "Style preset image file not found"):
+ super().__init__(message)
+
+
+class StylePresetImageFileSaveException(Exception):
+ """Raised when an image cannot be saved."""
+
+ def __init__(self, message: str = "Style preset image file not saved"):
+ super().__init__(message)
+
+
+class StylePresetImageFileDeleteException(Exception):
+ """Raised when an image cannot be deleted."""
+
+ def __init__(self, message: str = "Style preset image file not deleted"):
+ super().__init__(message)
diff --git a/invokeai/app/services/style_preset_images/style_preset_images_disk.py b/invokeai/app/services/style_preset_images/style_preset_images_disk.py
new file mode 100644
index 00000000000..cd2b29efd2a
--- /dev/null
+++ b/invokeai/app/services/style_preset_images/style_preset_images_disk.py
@@ -0,0 +1,88 @@
+from pathlib import Path
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.style_preset_images.style_preset_images_base import StylePresetImageFileStorageBase
+from invokeai.app.services.style_preset_images.style_preset_images_common import (
+ StylePresetImageFileDeleteException,
+ StylePresetImageFileNotFoundException,
+ StylePresetImageFileSaveException,
+)
+from invokeai.app.services.style_preset_records.style_preset_records_common import PresetType
+from invokeai.app.util.misc import uuid_string
+from invokeai.app.util.thumbnails import make_thumbnail
+
+
+class StylePresetImageFileStorageDisk(StylePresetImageFileStorageBase):
+ """Stores images on disk"""
+
+ def __init__(self, style_preset_images_folder: Path):
+ self._style_preset_images_folder = style_preset_images_folder
+ self._validate_storage_folders()
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def get(self, style_preset_id: str) -> PILImageType:
+ try:
+ path = self.get_path(style_preset_id)
+
+ return Image.open(path)
+ except FileNotFoundError as e:
+ raise StylePresetImageFileNotFoundException from e
+
+ def save(self, style_preset_id: str, image: PILImageType) -> None:
+ try:
+ self._validate_storage_folders()
+ image_path = self._style_preset_images_folder / (style_preset_id + ".webp")
+ thumbnail = make_thumbnail(image, 256)
+ thumbnail.save(image_path, format="webp")
+
+ except Exception as e:
+ raise StylePresetImageFileSaveException from e
+
+ def get_path(self, style_preset_id: str) -> Path:
+ style_preset = self._invoker.services.style_preset_records.get(style_preset_id)
+ if style_preset.type is PresetType.Default:
+ default_images_dir = Path(__file__).parent / Path("default_style_preset_images")
+ path = default_images_dir / (style_preset.name + ".png")
+ else:
+ path = self._style_preset_images_folder / (style_preset_id + ".webp")
+
+ return path
+
+ def get_url(self, style_preset_id: str) -> str | None:
+ path = self.get_path(style_preset_id)
+ if not self._validate_path(path):
+ return
+
+ url = self._invoker.services.urls.get_style_preset_image_url(style_preset_id)
+
+ # The image URL never changes, so we must add random query string to it to prevent caching
+ url += f"?{uuid_string()}"
+
+ return url
+
+ def delete(self, style_preset_id: str) -> None:
+ try:
+ path = self.get_path(style_preset_id)
+
+ if not self._validate_path(path):
+ raise StylePresetImageFileNotFoundException
+
+ path.unlink()
+
+ except StylePresetImageFileNotFoundException as e:
+ raise StylePresetImageFileNotFoundException from e
+ except Exception as e:
+ raise StylePresetImageFileDeleteException from e
+
+ def _validate_path(self, path: Path) -> bool:
+ """Validates the path given for an image."""
+ return path.exists()
+
+ def _validate_storage_folders(self) -> None:
+ """Checks if the required folders exist and create them if they don't"""
+ self._style_preset_images_folder.mkdir(parents=True, exist_ok=True)
diff --git a/invokeai/app/services/style_preset_records/__init__.py b/invokeai/app/services/style_preset_records/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/style_preset_records/default_style_presets.json b/invokeai/app/services/style_preset_records/default_style_presets.json
new file mode 100644
index 00000000000..1daadfa8ff7
--- /dev/null
+++ b/invokeai/app/services/style_preset_records/default_style_presets.json
@@ -0,0 +1,146 @@
+[
+ {
+ "name": "Photography (General)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt}. photography. f/2.8 macro photo, bokeh, photorealism",
+ "negative_prompt": "painting, digital art. sketch, blurry"
+ }
+ },
+ {
+ "name": "Photography (Studio Lighting)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt}, photography. f/8 photo. centered subject, studio lighting.",
+ "negative_prompt": "painting, digital art. sketch, blurry"
+ }
+ },
+ {
+ "name": "Photography (Landscape)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt}, landscape photograph, f/12, lifelike, highly detailed.",
+ "negative_prompt": "painting, digital art. sketch, blurry"
+ }
+ },
+ {
+ "name": "Photography (Portrait)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt}. photography. portraiture. catch light in eyes. one flash. rembrandt lighting. Soft box. dark shadows. High contrast. 80mm lens. F2.8.",
+ "negative_prompt": "painting, digital art. sketch, blurry"
+ }
+ },
+ {
+ "name": "Photography (Black and White)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} photography. natural light. 80mm lens. F1.4. strong contrast, hard light. dark contrast. blurred background. black and white",
+ "negative_prompt": "painting, digital art. sketch, colour+"
+ }
+ },
+ {
+ "name": "Architectural Visualization",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt}. architectural photography, f/12, luxury, aesthetically pleasing form and function.",
+ "negative_prompt": "painting, digital art. sketch, blurry"
+ }
+ },
+ {
+ "name": "Concept Art (Fantasy)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "concept artwork of a {prompt}. (digital painterly art style)++, mythological, (textured 2d dry media brushpack)++, glazed brushstrokes, otherworldly. painting+, illustration+",
+ "negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
+ }
+ },
+ {
+ "name": "Concept Art (Sci-Fi)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "(concept art)++, {prompt}, (sleek futurism)++, (textured 2d dry media)++, metallic highlights, digital painting style",
+ "negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
+ }
+ },
+ {
+ "name": "Concept Art (Character)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "(character concept art)++, stylized painterly digital painting of {prompt}, (painterly, impasto. Dry brush.)++",
+ "negative_prompt": "photo. distorted, blurry, out of focus. sketch. (cgi, 3d.)++"
+ }
+ },
+ {
+ "name": "Concept Art (Painterly)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} oil painting. high contrast. impasto. sfumato. chiaroscuro. Palette knife.",
+ "negative_prompt": "photo. smooth. border. frame"
+ }
+ },
+ {
+ "name": "Environment Art",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} environment artwork, hyper-realistic digital painting style with cinematic composition, atmospheric, depth and detail, voluminous. textured dry brush 2d media",
+ "negative_prompt": "photo, distorted, blurry, out of focus. sketch."
+ }
+ },
+ {
+ "name": "Interior Design (Visualization)",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} interior design photo, gentle shadows, light mid-tones, dimension, mix of smooth and textured surfaces, focus on negative space and clean lines, focus",
+ "negative_prompt": "photo, distorted. sketch."
+ }
+ },
+ {
+ "name": "Product Rendering",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} high quality product photography, 3d rendering with key lighting, shallow depth of field, simple plain background, studio lighting.",
+ "negative_prompt": "blurry, sketch, messy, dirty. unfinished."
+ }
+ },
+ {
+ "name": "Sketch",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} black and white pencil drawing, off-center composition, cross-hatching for shadows, bold strokes, textured paper. sketch+++",
+ "negative_prompt": "blurry, photo, painting, color. messy, dirty. unfinished. frame, borders."
+ }
+ },
+ {
+ "name": "Line Art",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} Line art. bold outline. simplistic. white background. 2d",
+ "negative_prompt": "photo. digital art. greyscale. solid black. painting"
+ }
+ },
+ {
+ "name": "Anime",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} anime++, bold outline, cel-shaded coloring, shounen, seinen",
+ "negative_prompt": "(photo)+++. greyscale. solid black. painting"
+ }
+ },
+ {
+ "name": "Illustration",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "{prompt} illustration, bold linework, illustrative details, vector art style, flat coloring",
+ "negative_prompt": "(photo)+++. greyscale. painting, black and white."
+ }
+ },
+ {
+ "name": "Vehicles",
+ "type": "default",
+ "preset_data": {
+ "positive_prompt": "A weird futuristic normal auto, {prompt} elegant design, nice color, nice wheels",
+ "negative_prompt": "sketch. digital art. greyscale. painting"
+ }
+ }
+]
diff --git a/invokeai/app/services/style_preset_records/style_preset_records_base.py b/invokeai/app/services/style_preset_records/style_preset_records_base.py
new file mode 100644
index 00000000000..87437a8dc0d
--- /dev/null
+++ b/invokeai/app/services/style_preset_records/style_preset_records_base.py
@@ -0,0 +1,53 @@
+from abc import ABC, abstractmethod
+
+from invokeai.app.services.style_preset_records.style_preset_records_common import (
+ PresetType,
+ StylePresetChanges,
+ StylePresetRecordDTO,
+ StylePresetWithoutId,
+)
+
+
+class StylePresetRecordsStorageBase(ABC):
+ """Base class for style preset storage services."""
+
+ @abstractmethod
+ def get(self, style_preset_id: str) -> StylePresetRecordDTO:
+ """Get style preset by id. Authorization is the caller's responsibility."""
+ pass
+
+ @abstractmethod
+ def create(self, style_preset: StylePresetWithoutId, user_id: str) -> StylePresetRecordDTO:
+ """Creates a style preset owned by user_id."""
+ pass
+
+ @abstractmethod
+ def create_many(self, style_presets: list[StylePresetWithoutId], user_id: str) -> None:
+ """Creates many style presets owned by user_id."""
+ pass
+
+ @abstractmethod
+ def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
+ """Updates a style preset. Authorization is the caller's responsibility."""
+ pass
+
+ @abstractmethod
+ def delete(self, style_preset_id: str) -> None:
+ """Deletes a style preset. Authorization is the caller's responsibility."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ type: PresetType | None = None,
+ user_id: str | None = None,
+ is_admin: bool = False,
+ ) -> list[StylePresetRecordDTO]:
+ """Gets style presets visible to user_id.
+
+ Visibility rules:
+ - is_admin=True: all presets.
+ - Else: presets owned by user_id, plus all `default` presets, plus any public preset.
+ - If user_id is None and is_admin is False: only `default` and public presets.
+ """
+ pass
diff --git a/invokeai/app/services/style_preset_records/style_preset_records_common.py b/invokeai/app/services/style_preset_records/style_preset_records_common.py
new file mode 100644
index 00000000000..9e6df88c989
--- /dev/null
+++ b/invokeai/app/services/style_preset_records/style_preset_records_common.py
@@ -0,0 +1,141 @@
+import codecs
+import csv
+import json
+from enum import Enum
+from typing import Any, Optional
+
+import pydantic
+from fastapi import UploadFile
+from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter
+
+from invokeai.app.util.metaenum import MetaEnum
+
+
+class StylePresetNotFoundError(Exception):
+ """Raised when a style preset is not found"""
+
+
+class PresetData(BaseModel, extra="forbid"):
+ positive_prompt: str = Field(description="Positive prompt")
+ negative_prompt: str = Field(description="Negative prompt")
+
+
+PresetDataValidator = TypeAdapter(PresetData)
+
+
+class PresetType(str, Enum, metaclass=MetaEnum):
+ User = "user"
+ Default = "default"
+
+
+class StylePresetChanges(BaseModel, extra="forbid"):
+ name: Optional[str] = Field(default=None, description="The style preset's new name.")
+ preset_data: Optional[PresetData] = Field(default=None, description="The updated data for style preset.")
+ type: Optional[PresetType] = Field(description="The updated type of the style preset")
+ is_public: Optional[bool] = Field(default=None, description="Whether the preset is visible to other users.")
+
+
+class StylePresetWithoutId(BaseModel):
+ name: str = Field(description="The name of the style preset.")
+ preset_data: PresetData = Field(description="The preset data")
+ type: PresetType = Field(description="The type of style preset")
+ is_public: bool = Field(default=False, description="Whether the preset is visible to other users.")
+
+
+class StylePresetRecordDTO(StylePresetWithoutId):
+ id: str = Field(description="The style preset ID.")
+ user_id: str = Field(description="The user who owns this style preset.")
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "StylePresetRecordDTO":
+ data["preset_data"] = PresetDataValidator.validate_json(data.get("preset_data", ""))
+ return StylePresetRecordDTOValidator.validate_python(data)
+
+
+StylePresetRecordDTOValidator = TypeAdapter(StylePresetRecordDTO)
+
+
+class StylePresetRecordWithImage(StylePresetRecordDTO):
+ image: Optional[str] = Field(description="The path for image")
+
+
+class StylePresetImportRow(BaseModel):
+ name: str = Field(min_length=1, description="The name of the preset.")
+ positive_prompt: str = Field(
+ default="",
+ description="The positive prompt for the preset.",
+ validation_alias=AliasChoices("positive_prompt", "prompt"),
+ )
+ negative_prompt: str = Field(default="", description="The negative prompt for the preset.")
+
+ model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
+
+
+StylePresetImportList = list[StylePresetImportRow]
+StylePresetImportListTypeAdapter = TypeAdapter(StylePresetImportList)
+
+
+class UnsupportedFileTypeError(ValueError):
+ """Raised when an unsupported file type is encountered"""
+
+ pass
+
+
+class InvalidPresetImportDataError(ValueError):
+ """Raised when invalid preset import data is encountered"""
+
+ pass
+
+
+async def parse_presets_from_file(file: UploadFile) -> list[StylePresetWithoutId]:
+ """Parses style presets from a file. The file must be a CSV or JSON file.
+
+ If CSV, the file must have the following columns:
+ - name
+ - prompt (or positive_prompt)
+ - negative_prompt
+
+ If JSON, the file must be a list of objects with the following keys:
+ - name
+ - prompt (or positive_prompt)
+ - negative_prompt
+
+ Args:
+ file (UploadFile): The file to parse.
+
+ Returns:
+ list[StylePresetWithoutId]: The parsed style presets.
+
+ Raises:
+ UnsupportedFileTypeError: If the file type is not supported.
+ InvalidPresetImportDataError: If the data in the file is invalid.
+ """
+ if file.content_type not in ["text/csv", "application/json"]:
+ raise UnsupportedFileTypeError()
+
+ if file.content_type == "text/csv":
+ csv_reader = csv.DictReader(codecs.iterdecode(file.file, "utf-8"))
+ data = list(csv_reader)
+ else: # file.content_type == "application/json":
+ json_data = await file.read()
+ data = json.loads(json_data)
+
+ try:
+ imported_presets = StylePresetImportListTypeAdapter.validate_python(data)
+
+ style_presets: list[StylePresetWithoutId] = []
+
+ for imported in imported_presets:
+ preset_data = PresetData(positive_prompt=imported.positive_prompt, negative_prompt=imported.negative_prompt)
+ style_preset = StylePresetWithoutId(name=imported.name, preset_data=preset_data, type=PresetType.User)
+ style_presets.append(style_preset)
+ except pydantic.ValidationError as e:
+ if file.content_type == "text/csv":
+ msg = "Invalid CSV format: must include columns 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
+ else: # file.content_type == "application/json":
+ msg = "Invalid JSON format: must be a list of objects with keys 'name', 'prompt', and 'negative_prompt' and name cannot be blank"
+ raise InvalidPresetImportDataError(msg) from e
+ finally:
+ file.file.close()
+
+ return style_presets
diff --git a/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py b/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py
new file mode 100644
index 00000000000..03397133ae9
--- /dev/null
+++ b/invokeai/app/services/style_preset_records/style_preset_records_sqlite.py
@@ -0,0 +1,188 @@
+import json
+from pathlib import Path
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.style_preset_records.style_preset_records_base import StylePresetRecordsStorageBase
+from invokeai.app.services.style_preset_records.style_preset_records_common import (
+ PresetType,
+ StylePresetChanges,
+ StylePresetNotFoundError,
+ StylePresetRecordDTO,
+ StylePresetWithoutId,
+)
+from invokeai.app.util.misc import uuid_string
+
+# System user id used for default / shipped presets and for legacy rows pre-dating
+# the per-user ownership columns added in migration 27.
+SYSTEM_USER_ID = "system"
+
+
+class SqliteStylePresetRecordsStorage(StylePresetRecordsStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+ self._sync_default_style_presets()
+
+ def get(self, style_preset_id: str) -> StylePresetRecordDTO:
+ """Gets a style preset by ID."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT *
+ FROM style_presets
+ WHERE id = ?;
+ """,
+ (style_preset_id,),
+ )
+ row = cursor.fetchone()
+ if row is None:
+ raise StylePresetNotFoundError(f"Style preset with id {style_preset_id} not found")
+ return StylePresetRecordDTO.from_dict(dict(row))
+
+ def create(self, style_preset: StylePresetWithoutId, user_id: str) -> StylePresetRecordDTO:
+ style_preset_id = uuid_string()
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO style_presets (
+ id,
+ name,
+ preset_data,
+ type,
+ user_id,
+ is_public
+ )
+ VALUES (?, ?, ?, ?, ?, ?);
+ """,
+ (
+ style_preset_id,
+ style_preset.name,
+ style_preset.preset_data.model_dump_json(),
+ style_preset.type,
+ user_id,
+ 1 if style_preset.is_public else 0,
+ ),
+ )
+ return self.get(style_preset_id)
+
+ def create_many(self, style_presets: list[StylePresetWithoutId], user_id: str) -> None:
+ with self._db.transaction() as cursor:
+ for style_preset in style_presets:
+ style_preset_id = uuid_string()
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO style_presets (
+ id,
+ name,
+ preset_data,
+ type,
+ user_id,
+ is_public
+ )
+ VALUES (?, ?, ?, ?, ?, ?);
+ """,
+ (
+ style_preset_id,
+ style_preset.name,
+ style_preset.preset_data.model_dump_json(),
+ style_preset.type,
+ user_id,
+ 1 if style_preset.is_public else 0,
+ ),
+ )
+
+ return None
+
+ def update(self, style_preset_id: str, changes: StylePresetChanges) -> StylePresetRecordDTO:
+ with self._db.transaction() as cursor:
+ if changes.name is not None:
+ cursor.execute(
+ """--sql
+ UPDATE style_presets
+ SET name = ?
+ WHERE id = ?;
+ """,
+ (changes.name, style_preset_id),
+ )
+
+ if changes.preset_data is not None:
+ cursor.execute(
+ """--sql
+ UPDATE style_presets
+ SET preset_data = ?
+ WHERE id = ?;
+ """,
+ (changes.preset_data.model_dump_json(), style_preset_id),
+ )
+
+ if changes.is_public is not None:
+ cursor.execute(
+ """--sql
+ UPDATE style_presets
+ SET is_public = ?
+ WHERE id = ?;
+ """,
+ (1 if changes.is_public else 0, style_preset_id),
+ )
+
+ return self.get(style_preset_id)
+
+ def delete(self, style_preset_id: str) -> None:
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE from style_presets
+ WHERE id = ?;
+ """,
+ (style_preset_id,),
+ )
+ return None
+
+ def get_many(
+ self,
+ type: PresetType | None = None,
+ user_id: str | None = None,
+ is_admin: bool = False,
+ ) -> list[StylePresetRecordDTO]:
+ clauses: list[str] = []
+ params: list[object] = []
+
+ if not is_admin:
+ # Visible to non-admin: own + default + public.
+ visibility = "(type = 'default' OR is_public = 1"
+ if user_id is not None:
+ visibility += " OR user_id = ?"
+ params.append(user_id)
+ visibility += ")"
+ clauses.append(visibility)
+
+ if type is not None:
+ clauses.append("type = ?")
+ params.append(type)
+
+ where = f"WHERE {' AND '.join(clauses)} " if clauses else ""
+ query = f"SELECT * FROM style_presets {where}ORDER BY LOWER(name) ASC"
+
+ with self._db.transaction() as cursor:
+ cursor.execute(query, params)
+ rows = cursor.fetchall()
+ return [StylePresetRecordDTO.from_dict(dict(row)) for row in rows]
+
+ def _sync_default_style_presets(self) -> None:
+ """Syncs default style presets to the database. Internal use only."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ DELETE FROM style_presets
+ WHERE type = "default";
+ """
+ )
+ with open(Path(__file__).parent / Path("default_style_presets.json"), "r") as file:
+ presets = json.load(file)
+ for preset in presets:
+ style_preset = StylePresetWithoutId.model_validate(preset)
+ self.create(style_preset, user_id=SYSTEM_USER_ID)
diff --git a/invokeai/app/services/urls/__init__.py b/invokeai/app/services/urls/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/urls/urls_base.py b/invokeai/app/services/urls/urls_base.py
new file mode 100644
index 00000000000..a5602abb3b4
--- /dev/null
+++ b/invokeai/app/services/urls/urls_base.py
@@ -0,0 +1,25 @@
+from abc import ABC, abstractmethod
+
+
+class UrlServiceBase(ABC):
+ """Responsible for building URLs for resources."""
+
+ @abstractmethod
+ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str:
+ """Gets the URL for an image or thumbnail."""
+ pass
+
+ @abstractmethod
+ def get_model_image_url(self, model_key: str) -> str:
+ """Gets the URL for a model image"""
+ pass
+
+ @abstractmethod
+ def get_style_preset_image_url(self, style_preset_id: str) -> str:
+ """Gets the URL for a style preset image"""
+ pass
+
+ @abstractmethod
+ def get_workflow_thumbnail_url(self, workflow_id: str) -> str:
+ """Gets the URL for a workflow thumbnail"""
+ pass
diff --git a/invokeai/app/services/urls/urls_default.py b/invokeai/app/services/urls/urls_default.py
new file mode 100644
index 00000000000..2e4f36d9d51
--- /dev/null
+++ b/invokeai/app/services/urls/urls_default.py
@@ -0,0 +1,27 @@
+import os
+
+from invokeai.app.services.urls.urls_base import UrlServiceBase
+
+
+class LocalUrlService(UrlServiceBase):
+ def __init__(self, base_url: str = "api/v1", base_url_v2: str = "api/v2"):
+ self._base_url = base_url
+ self._base_url_v2 = base_url_v2
+
+ def get_image_url(self, image_name: str, thumbnail: bool = False) -> str:
+ image_basename = os.path.basename(image_name)
+
+ # These paths are determined by the routes in invokeai/app/api/routers/images.py
+ if thumbnail:
+ return f"{self._base_url}/images/i/{image_basename}/thumbnail"
+
+ return f"{self._base_url}/images/i/{image_basename}/full"
+
+ def get_model_image_url(self, model_key: str) -> str:
+ return f"{self._base_url_v2}/models/i/{model_key}/image"
+
+ def get_style_preset_image_url(self, style_preset_id: str) -> str:
+ return f"{self._base_url}/style_presets/i/{style_preset_id}/image"
+
+ def get_workflow_thumbnail_url(self, workflow_id: str) -> str:
+ return f"{self._base_url}/workflows/i/{workflow_id}/thumbnail"
diff --git a/invokeai/app/services/users/__init__.py b/invokeai/app/services/users/__init__.py
new file mode 100644
index 00000000000..f4976759504
--- /dev/null
+++ b/invokeai/app/services/users/__init__.py
@@ -0,0 +1 @@
+"""User service module."""
diff --git a/invokeai/app/services/users/users_base.py b/invokeai/app/services/users/users_base.py
new file mode 100644
index 00000000000..dd789b561ee
--- /dev/null
+++ b/invokeai/app/services/users/users_base.py
@@ -0,0 +1,150 @@
+"""Abstract base class for user service."""
+
+from abc import ABC, abstractmethod
+
+from invokeai.app.services.users.users_common import UserCreateRequest, UserDTO, UserUpdateRequest
+
+
+class UserServiceBase(ABC):
+ """High-level service for user management."""
+
+ @abstractmethod
+ def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Create a new user.
+
+ Args:
+ user_data: User creation data
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
+
+ Returns:
+ The created user
+
+ Raises:
+ ValueError: If email already exists or (when strict) password is weak
+ """
+ pass
+
+ @abstractmethod
+ def get(self, user_id: str) -> UserDTO | None:
+ """Get user by ID.
+
+ Args:
+ user_id: The user ID
+
+ Returns:
+ UserDTO if found, None otherwise
+ """
+ pass
+
+ @abstractmethod
+ def get_by_email(self, email: str) -> UserDTO | None:
+ """Get user by email.
+
+ Args:
+ email: The email address
+
+ Returns:
+ UserDTO if found, None otherwise
+ """
+ pass
+
+ @abstractmethod
+ def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Update user.
+
+ Args:
+ user_id: The user ID
+ changes: Fields to update
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
+
+ Returns:
+ The updated user
+
+ Raises:
+ ValueError: If user not found or (when strict) password is weak
+ """
+ pass
+
+ @abstractmethod
+ def delete(self, user_id: str) -> None:
+ """Delete user.
+
+ Args:
+ user_id: The user ID
+
+ Raises:
+ ValueError: If user not found
+ """
+ pass
+
+ @abstractmethod
+ def authenticate(self, email: str, password: str) -> UserDTO | None:
+ """Authenticate user credentials.
+
+ Args:
+ email: User email
+ password: User password
+
+ Returns:
+ UserDTO if authentication successful, None otherwise
+ """
+ pass
+
+ @abstractmethod
+ def has_admin(self) -> bool:
+ """Check if any admin user exists.
+
+ Returns:
+ True if at least one admin user exists, False otherwise
+ """
+ pass
+
+ @abstractmethod
+ def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Create an admin user (for initial setup).
+
+ Args:
+ user_data: User creation data
+ strict_password_checking: If True (default), passwords must meet strength requirements.
+ If False, any non-empty password is accepted.
+
+ Returns:
+ The created admin user
+
+ Raises:
+ ValueError: If admin already exists or (when strict) password is weak
+ """
+ pass
+
+ @abstractmethod
+ def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
+ """List all users.
+
+ Args:
+ limit: Maximum number of users to return
+ offset: Number of users to skip
+
+ Returns:
+ List of users
+ """
+ pass
+
+ @abstractmethod
+ def get_admin_email(self) -> str | None:
+ """Get the email address of the first active admin user.
+
+ Returns:
+ Email address of the first active admin, or None if no admin exists
+ """
+ pass
+
+ @abstractmethod
+ def count_admins(self) -> int:
+ """Count active admin users.
+
+ Returns:
+ The number of active admin users
+ """
+ pass
diff --git a/invokeai/app/services/users/users_common.py b/invokeai/app/services/users/users_common.py
new file mode 100644
index 00000000000..c13150a3369
--- /dev/null
+++ b/invokeai/app/services/users/users_common.py
@@ -0,0 +1,114 @@
+"""Common types and data models for user service."""
+
+from datetime import datetime
+
+from pydantic import BaseModel, Field, field_validator
+from pydantic_core import PydanticCustomError
+
+
+def validate_email_with_special_domains(email: str) -> str:
+ """Validate email address, allowing special-use domains like .local for testing.
+
+ This validator first tries standard email validation using email-validator library.
+ If it fails due to special-use domains (like .local, .test, .localhost), it performs
+ a basic syntax check instead. This allows development/testing with non-routable domains
+ while still catching actual typos and malformed emails.
+
+ Args:
+ email: The email address to validate
+
+ Returns:
+ The validated email address (lowercased)
+
+ Raises:
+ PydanticCustomError: If the email format is invalid
+ """
+ try:
+ # Try standard email validation using email-validator
+ from email_validator import EmailNotValidError, validate_email
+
+ result = validate_email(email, check_deliverability=False)
+ return result.normalized
+ except EmailNotValidError as e:
+ error_msg = str(e)
+
+ # Check if the error is specifically about special-use/reserved domains or localhost
+ if (
+ "special-use" in error_msg.lower()
+ or "reserved" in error_msg.lower()
+ or "should have a period" in error_msg.lower()
+ ):
+ # Perform basic email syntax validation
+ email = email.strip().lower()
+
+ if "@" not in email:
+ raise PydanticCustomError(
+ "value_error",
+ "Email address must contain an @ symbol",
+ )
+
+ local_part, domain = email.rsplit("@", 1)
+
+ if not local_part or not domain:
+ raise PydanticCustomError(
+ "value_error",
+ "Email address must have both local and domain parts",
+ )
+
+ # Allow localhost and domains with dots
+ if domain == "localhost" or "." in domain:
+ return email
+
+ raise PydanticCustomError(
+ "value_error",
+ "Email domain must contain a dot or be 'localhost'",
+ )
+ else:
+ # Re-raise other validation errors
+ raise PydanticCustomError(
+ "value_error",
+ f"Invalid email address: {error_msg}",
+ )
+
+
+class UserDTO(BaseModel):
+ """User data transfer object."""
+
+ user_id: str = Field(description="Unique user identifier")
+ email: str = Field(description="User email address")
+ display_name: str | None = Field(default=None, description="Display name")
+ is_admin: bool = Field(default=False, description="Whether user has admin privileges")
+ is_active: bool = Field(default=True, description="Whether user account is active")
+ created_at: datetime = Field(description="When the user was created")
+ updated_at: datetime = Field(description="When the user was last updated")
+ last_login_at: datetime | None = Field(default=None, description="When user last logged in")
+
+ @field_validator("email")
+ @classmethod
+ def validate_email(cls, v: str) -> str:
+ """Validate email address, allowing special-use domains."""
+ return validate_email_with_special_domains(v)
+
+
+class UserCreateRequest(BaseModel):
+ """Request to create a new user."""
+
+ email: str = Field(description="User email address")
+ display_name: str | None = Field(default=None, description="Display name")
+ password: str = Field(description="User password")
+ is_admin: bool = Field(default=False, description="Whether user should have admin privileges")
+
+ @field_validator("email")
+ @classmethod
+ def validate_email(cls, v: str) -> str:
+ """Validate email address, allowing special-use domains."""
+ return validate_email_with_special_domains(v)
+
+
+class UserUpdateRequest(BaseModel):
+ """Request to update a user."""
+
+ display_name: str | None = Field(default=None, description="Display name")
+ password: str | None = Field(default=None, description="New password")
+ is_admin: bool | None = Field(default=None, description="Whether user should have admin privileges")
+ is_active: bool | None = Field(default=None, description="Whether user account should be active")
diff --git a/invokeai/app/services/users/users_default.py b/invokeai/app/services/users/users_default.py
new file mode 100644
index 00000000000..6e472882124
--- /dev/null
+++ b/invokeai/app/services/users/users_default.py
@@ -0,0 +1,278 @@
+"""Default SQLite implementation of user service."""
+
+import sqlite3
+from datetime import datetime, timezone
+from uuid import uuid4
+
+from invokeai.app.services.auth.password_utils import hash_password, validate_password_strength, verify_password
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.users.users_base import UserServiceBase
+from invokeai.app.services.users.users_common import UserCreateRequest, UserDTO, UserUpdateRequest
+
+
+class UserService(UserServiceBase):
+ """SQLite-based user service."""
+
+ def __init__(self, db: SqliteDatabase):
+ """Initialize user service.
+
+ Args:
+ db: SQLite database instance
+ """
+ self._db = db
+
+ def create(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Create a new user."""
+ # Validate password strength
+ if strict_password_checking:
+ is_valid, error_msg = validate_password_strength(user_data.password)
+ if not is_valid:
+ raise ValueError(error_msg)
+ elif not user_data.password:
+ raise ValueError("Password cannot be empty")
+
+ # Check if email already exists
+ if self.get_by_email(user_data.email) is not None:
+ raise ValueError(f"User with email {user_data.email} already exists")
+
+ user_id = str(uuid4())
+ password_hash = hash_password(user_data.password)
+
+ with self._db.transaction() as cursor:
+ try:
+ cursor.execute(
+ """
+ INSERT INTO users (user_id, email, display_name, password_hash, is_admin)
+ VALUES (?, ?, ?, ?, ?)
+ """,
+ (user_id, user_data.email, user_data.display_name, password_hash, user_data.is_admin),
+ )
+ except sqlite3.IntegrityError as e:
+ raise ValueError(f"Failed to create user: {e}") from e
+
+ user = self.get(user_id)
+ if user is None:
+ raise RuntimeError("Failed to retrieve created user")
+ return user
+
+ def get(self, user_id: str) -> UserDTO | None:
+ """Get user by ID."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
+ FROM users
+ WHERE user_id = ?
+ """,
+ (user_id,),
+ )
+ row = cursor.fetchone()
+
+ if row is None:
+ return None
+
+ return UserDTO(
+ user_id=row[0],
+ email=row[1],
+ display_name=row[2],
+ is_admin=bool(row[3]),
+ is_active=bool(row[4]),
+ created_at=datetime.fromisoformat(row[5]),
+ updated_at=datetime.fromisoformat(row[6]),
+ last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
+ )
+
+ def get_by_email(self, email: str) -> UserDTO | None:
+ """Get user by email."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
+ FROM users
+ WHERE email = ?
+ """,
+ (email,),
+ )
+ row = cursor.fetchone()
+
+ if row is None:
+ return None
+
+ return UserDTO(
+ user_id=row[0],
+ email=row[1],
+ display_name=row[2],
+ is_admin=bool(row[3]),
+ is_active=bool(row[4]),
+ created_at=datetime.fromisoformat(row[5]),
+ updated_at=datetime.fromisoformat(row[6]),
+ last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
+ )
+
+ def update(self, user_id: str, changes: UserUpdateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Update user."""
+ # Check if user exists
+ user = self.get(user_id)
+ if user is None:
+ raise ValueError(f"User {user_id} not found")
+
+ # Validate password if provided
+ if changes.password is not None:
+ if strict_password_checking:
+ is_valid, error_msg = validate_password_strength(changes.password)
+ if not is_valid:
+ raise ValueError(error_msg)
+ elif not changes.password:
+ raise ValueError("Password cannot be empty")
+
+ # Build update query dynamically based on provided fields
+ updates: list[str] = []
+ params: list[str | bool | int] = []
+
+ if changes.display_name is not None:
+ updates.append("display_name = ?")
+ params.append(changes.display_name)
+
+ if changes.password is not None:
+ updates.append("password_hash = ?")
+ params.append(hash_password(changes.password))
+
+ if changes.is_admin is not None:
+ updates.append("is_admin = ?")
+ params.append(changes.is_admin)
+
+ if changes.is_active is not None:
+ updates.append("is_active = ?")
+ params.append(changes.is_active)
+
+ if not updates:
+ return user
+
+ params.append(user_id)
+ query = f"UPDATE users SET {', '.join(updates)} WHERE user_id = ?"
+
+ with self._db.transaction() as cursor:
+ cursor.execute(query, params)
+
+ updated_user = self.get(user_id)
+ if updated_user is None:
+ raise RuntimeError("Failed to retrieve updated user")
+ return updated_user
+
+ def delete(self, user_id: str) -> None:
+ """Delete user."""
+ user = self.get(user_id)
+ if user is None:
+ raise ValueError(f"User {user_id} not found")
+
+ with self._db.transaction() as cursor:
+ cursor.execute("DELETE FROM users WHERE user_id = ?", (user_id,))
+
+ def authenticate(self, email: str, password: str) -> UserDTO | None:
+ """Authenticate user credentials."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT user_id, email, display_name, password_hash, is_admin, is_active, created_at, updated_at, last_login_at
+ FROM users
+ WHERE email = ?
+ """,
+ (email,),
+ )
+ row = cursor.fetchone()
+
+ if row is None:
+ return None
+
+ password_hash = row[3]
+ if not verify_password(password, password_hash):
+ return None
+
+ # Update last login time
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ "UPDATE users SET last_login_at = ? WHERE user_id = ?",
+ (datetime.now(timezone.utc).isoformat(), row[0]),
+ )
+
+ return UserDTO(
+ user_id=row[0],
+ email=row[1],
+ display_name=row[2],
+ is_admin=bool(row[4]),
+ is_active=bool(row[5]),
+ created_at=datetime.fromisoformat(row[6]),
+ updated_at=datetime.fromisoformat(row[7]),
+ last_login_at=datetime.now(timezone.utc),
+ )
+
+ def has_admin(self) -> bool:
+ """Check if any admin user exists."""
+ with self._db.transaction() as cursor:
+ cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = TRUE AND is_active = TRUE")
+ row = cursor.fetchone()
+ count = row[0] if row else 0
+ return bool(count > 0)
+
+ def create_admin(self, user_data: UserCreateRequest, strict_password_checking: bool = True) -> UserDTO:
+ """Create an admin user (for initial setup)."""
+ if self.has_admin():
+ raise ValueError("Admin user already exists")
+
+ # Force is_admin to True
+ admin_data = UserCreateRequest(
+ email=user_data.email,
+ display_name=user_data.display_name,
+ password=user_data.password,
+ is_admin=True,
+ )
+ return self.create(admin_data, strict_password_checking=strict_password_checking)
+
+ def list_users(self, limit: int = 100, offset: int = 0) -> list[UserDTO]:
+ """List all users."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT user_id, email, display_name, is_admin, is_active, created_at, updated_at, last_login_at
+ FROM users
+ ORDER BY created_at DESC
+ LIMIT ? OFFSET ?
+ """,
+ (limit, offset),
+ )
+ rows = cursor.fetchall()
+
+ return [
+ UserDTO(
+ user_id=row[0],
+ email=row[1],
+ display_name=row[2],
+ is_admin=bool(row[3]),
+ is_active=bool(row[4]),
+ created_at=datetime.fromisoformat(row[5]),
+ updated_at=datetime.fromisoformat(row[6]),
+ last_login_at=datetime.fromisoformat(row[7]) if row[7] else None,
+ )
+ for row in rows
+ ]
+
+ def get_admin_email(self) -> str | None:
+ """Get the email address of the first active admin user."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """
+ SELECT email FROM users
+ WHERE is_admin = TRUE AND is_active = TRUE
+ ORDER BY created_at ASC
+ LIMIT 1
+ """,
+ )
+ row = cursor.fetchone()
+ return row[0] if row else None
+
+ def count_admins(self) -> int:
+ """Count active admin users."""
+ with self._db.transaction() as cursor:
+ cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = TRUE AND is_active = TRUE")
+ row = cursor.fetchone()
+ return int(row[0]) if row else 0
diff --git a/invokeai/app/services/virtual_boards/__init__.py b/invokeai/app/services/virtual_boards/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/virtual_boards/virtual_boards_common.py b/invokeai/app/services/virtual_boards/virtual_boards_common.py
new file mode 100644
index 00000000000..e1df5a81ca5
--- /dev/null
+++ b/invokeai/app/services/virtual_boards/virtual_boards_common.py
@@ -0,0 +1,14 @@
+from typing import Optional
+
+from pydantic import BaseModel, Field
+
+
+class VirtualSubBoardDTO(BaseModel):
+ """A virtual sub-board computed from image metadata, not stored in the database."""
+
+ virtual_board_id: str = Field(description="The virtual board ID, e.g. 'by_date:2026-03-18'.")
+ board_name: str = Field(description="The display name of the virtual sub-board, e.g. '2026-03-18'.")
+ date: str = Field(description="The ISO date string, e.g. '2026-03-18'.")
+ image_count: int = Field(description="The number of general images for this date.")
+ asset_count: int = Field(description="The number of asset images for this date.")
+ cover_image_name: Optional[str] = Field(default=None, description="The most recent image name for this date.")
diff --git a/invokeai/app/services/workflow_records/__init__.py b/invokeai/app/services/workflow_records/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/services/workflow_records/default_workflows/CogView4_TextToImage.json b/invokeai/app/services/workflow_records/default_workflows/CogView4_TextToImage.json
new file mode 100644
index 00000000000..5318ba3e615
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/CogView4_TextToImage.json
@@ -0,0 +1,343 @@
+{
+ "name": "Text to Image - CogView4",
+ "author": "",
+ "description": "Generate an image from a prompt with CogView4.",
+ "version": "",
+ "contact": "",
+ "tags": "CogView4, Text to Image",
+ "notes": "",
+ "exposedFields": [],
+ "meta": { "category": "default", "version": "3.0.0" },
+ "id": "default_0e405a8e-ab5e-4e6c-bd99-b59deabd5591",
+ "form": {
+ "elements": {
+ "container-XSINSu999B": {
+ "id": "container-XSINSu999B",
+ "data": {
+ "layout": "column",
+ "children": [
+ "heading-N0TXlsboP5",
+ "text-PVw8AvXCTz",
+ "divider-5wmCOm9mqG",
+ "node-field-gPil4XSw8L",
+ "node-field-T2oYYNrAzH",
+ "node-field-SRj6Dn28lm"
+ ]
+ },
+ "type": "container"
+ },
+ "node-field-gPil4XSw8L": {
+ "id": "node-field-gPil4XSw8L",
+ "type": "node-field",
+ "parentId": "container-XSINSu999B",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "a4569d8b-6a43-44b9-8919-4ceec6682904",
+ "fieldName": "prompt"
+ },
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ },
+ "showDescription": false
+ }
+ },
+ "node-field-T2oYYNrAzH": {
+ "id": "node-field-T2oYYNrAzH",
+ "type": "node-field",
+ "parentId": "container-XSINSu999B",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "acb26944-1208-4016-9929-ab8dd0860573",
+ "fieldName": "prompt"
+ },
+ "settings": {
+ "type": "string-field-config",
+ "component": "textarea"
+ },
+ "showDescription": false
+ }
+ },
+ "node-field-SRj6Dn28lm": {
+ "id": "node-field-SRj6Dn28lm",
+ "type": "node-field",
+ "parentId": "container-XSINSu999B",
+ "data": {
+ "fieldIdentifier": {
+ "nodeId": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "fieldName": "model"
+ },
+ "showDescription": false
+ }
+ },
+ "heading-N0TXlsboP5": {
+ "id": "heading-N0TXlsboP5",
+ "parentId": "container-XSINSu999B",
+ "type": "heading",
+ "data": { "content": "Text to Image - CogView4" }
+ },
+ "text-PVw8AvXCTz": {
+ "id": "text-PVw8AvXCTz",
+ "parentId": "container-XSINSu999B",
+ "type": "text",
+ "data": { "content": "Generate an image from a prompt with CogView4." }
+ },
+ "divider-5wmCOm9mqG": {
+ "id": "divider-5wmCOm9mqG",
+ "parentId": "container-XSINSu999B",
+ "type": "divider"
+ }
+ },
+ "rootElementId": "container-XSINSu999B"
+ },
+ "nodes": [
+ {
+ "id": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "type": "invocation",
+ "data": {
+ "id": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "cogview4_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": { "x": -52.193850056888095, "y": 282.4721422789611 }
+ },
+ {
+ "id": "a4569d8b-6a43-44b9-8919-4ceec6682904",
+ "type": "invocation",
+ "data": {
+ "id": "a4569d8b-6a43-44b9-8919-4ceec6682904",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "cogview4_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "description": "",
+ "value": "A whimsical stuffed gnome sits on a golden sandy beach, its plush fabric slightly textured and well-worn. The gnome has a round, cheerful face with a fluffy white beard, a bulbous nose, and a tall, slightly floppy red hat with a few decorative stitching details. It wears a tiny blue vest over a soft, earthy-toned tunic, and its stubby arms grasp a ripe yellow banana with a few brown speckles. The ocean waves gently roll onto the shore in the background, with turquoise water reflecting the warm glow of the late afternoon sun. A few scattered seashells and driftwood pieces are near the gnome, while a colorful beach umbrella and footprints in the sand hint at a lively beach scene. The sky is a soft pastel blend of pink, orange, and light blue, with wispy clouds stretching across the horizon.\n"
+ },
+ "glm_encoder": {
+ "name": "glm_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": { "x": 328.9380683664592, "y": 305.11768986950995 }
+ },
+ {
+ "id": "acb26944-1208-4016-9929-ab8dd0860573",
+ "type": "invocation",
+ "data": {
+ "id": "acb26944-1208-4016-9929-ab8dd0860573",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "cogview4_text_encoder",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "description": "",
+ "value": ""
+ },
+ "glm_encoder": {
+ "name": "glm_encoder",
+ "label": "",
+ "description": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": { "x": 334.6799782744916, "y": 496.5882067536601 }
+ },
+ {
+ "id": "cdd72700-463d-4e10-8d76-3e842e4c0b49",
+ "type": "invocation",
+ "data": {
+ "id": "cdd72700-463d-4e10-8d76-3e842e4c0b49",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "cogview4_l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": { "name": "metadata", "label": "", "description": "" },
+ "latents": { "name": "latents", "label": "", "description": "" },
+ "vae": { "name": "vae", "label": "", "description": "" }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": { "x": 1112.027247217991, "y": 294.1351498145327 }
+ },
+ {
+ "id": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "type": "invocation",
+ "data": {
+ "id": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "version": "1.0.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "cogview4_denoise",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "",
+ "description": "",
+ "value": "auto"
+ },
+ "metadata": { "name": "metadata", "label": "", "description": "" },
+ "latents": { "name": "latents", "label": "", "description": "" },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": "",
+ "description": ""
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "description": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "description": "",
+ "value": 1
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": "",
+ "description": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": "",
+ "description": ""
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "description": "",
+ "value": 3.5
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "description": "",
+ "value": 1024
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "description": "",
+ "value": 30
+ },
+ "seed": { "name": "seed", "label": "", "description": "", "value": 0 }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": { "x": 720.8830004638692, "y": 332.66609681908415 }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-7890507c-d346-4d13-bcb4-bc6d4850b2e3vae-cdd72700-463d-4e10-8d76-3e842e4c0b49vae",
+ "type": "default",
+ "source": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "target": "cdd72700-463d-4e10-8d76-3e842e4c0b49",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-7890507c-d346-4d13-bcb4-bc6d4850b2e3glm_encoder-a4569d8b-6a43-44b9-8919-4ceec6682904glm_encoder",
+ "type": "default",
+ "source": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "target": "a4569d8b-6a43-44b9-8919-4ceec6682904",
+ "sourceHandle": "glm_encoder",
+ "targetHandle": "glm_encoder"
+ },
+ {
+ "id": "reactflow__edge-7890507c-d346-4d13-bcb4-bc6d4850b2e3glm_encoder-acb26944-1208-4016-9929-ab8dd0860573glm_encoder",
+ "type": "default",
+ "source": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "target": "acb26944-1208-4016-9929-ab8dd0860573",
+ "sourceHandle": "glm_encoder",
+ "targetHandle": "glm_encoder"
+ },
+ {
+ "id": "reactflow__edge-a4569d8b-6a43-44b9-8919-4ceec6682904conditioning-e75e2ced-284e-4135-81dc-cdf06c7a409dpositive_conditioning",
+ "type": "default",
+ "source": "a4569d8b-6a43-44b9-8919-4ceec6682904",
+ "target": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-acb26944-1208-4016-9929-ab8dd0860573conditioning-e75e2ced-284e-4135-81dc-cdf06c7a409dnegative_conditioning",
+ "type": "default",
+ "source": "acb26944-1208-4016-9929-ab8dd0860573",
+ "target": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-e75e2ced-284e-4135-81dc-cdf06c7a409dlatents-cdd72700-463d-4e10-8d76-3e842e4c0b49latents",
+ "type": "default",
+ "source": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "target": "cdd72700-463d-4e10-8d76-3e842e4c0b49",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-7890507c-d346-4d13-bcb4-bc6d4850b2e3transformer-e75e2ced-284e-4135-81dc-cdf06c7a409dtransformer",
+ "type": "default",
+ "source": "7890507c-d346-4d13-bcb4-bc6d4850b2e3",
+ "target": "e75e2ced-284e-4135-81dc-cdf06c7a409d",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/ESRGAN Upscaling with Canny ControlNet.json b/invokeai/app/services/workflow_records/default_workflows/ESRGAN Upscaling with Canny ControlNet.json
new file mode 100644
index 00000000000..8589b301836
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/ESRGAN Upscaling with Canny ControlNet.json
@@ -0,0 +1,838 @@
+{
+ "id": "default_686bb1d0-d086-4c70-9fa3-2f600b922023",
+ "name": "Upscaler - SD1.5, ESRGAN",
+ "author": "InvokeAI",
+ "description": "Sample workflow for using ESRGAN to upscale with ControlNet with SD1.5",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, upscaling, control",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "771bdf6a-0813-4099-a5d8-921a138754d4",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "fieldName": "model_name"
+ },
+ {
+ "nodeId": "ca1d020c-89a8-4958-880a-016d28775cfa",
+ "fieldName": "control_model"
+ },
+ {
+ "nodeId": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0",
+ "fieldName": "board"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b",
+ "type": "invocation",
+ "data": {
+ "id": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1250,
+ "y": 1200
+ }
+ },
+ {
+ "id": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "type": "invocation",
+ "data": {
+ "id": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "i2l",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1650,
+ "y": 1675
+ }
+ },
+ {
+ "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0",
+ "type": "invocation",
+ "data": {
+ "id": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 2559.4751127537957,
+ "y": 1246.6000376741406
+ }
+ },
+ {
+ "id": "ca1d020c-89a8-4958-880a-016d28775cfa",
+ "type": "invocation",
+ "data": {
+ "id": "ca1d020c-89a8-4958-880a-016d28775cfa",
+ "version": "1.1.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "controlnet",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": "Control Model (select Canny)"
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.95
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0.1
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.9
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1624.7980608333519,
+ "y": 1902.9649340196056
+ }
+ },
+ {
+ "id": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "type": "invocation",
+ "data": {
+ "id": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "version": "1.3.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "canny_image_processor",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "detect_resolution": {
+ "name": "detect_resolution",
+ "label": "",
+ "value": 512
+ },
+ "image_resolution": {
+ "name": "image_resolution",
+ "label": "",
+ "value": 512
+ },
+ "low_threshold": {
+ "name": "low_threshold",
+ "label": "",
+ "value": 100
+ },
+ "high_threshold": {
+ "name": "high_threshold",
+ "label": "",
+ "value": 200
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1200,
+ "y": 1900
+ }
+ },
+ {
+ "id": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "type": "invocation",
+ "data": {
+ "id": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "version": "1.0.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 700,
+ "y": 1375
+ }
+ },
+ {
+ "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74",
+ "type": "invocation",
+ "data": {
+ "id": "e8bf67fe-67de-4227-87eb-79e86afdfc74",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1250,
+ "y": 1500
+ }
+ },
+ {
+ "id": "771bdf6a-0813-4099-a5d8-921a138754d4",
+ "type": "invocation",
+ "data": {
+ "id": "771bdf6a-0813-4099-a5d8-921a138754d4",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Image To Upscale"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 344.5593065887157,
+ "y": 1698.161491368619
+ }
+ },
+ {
+ "id": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "type": "invocation",
+ "data": {
+ "id": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "version": "1.3.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "esrgan",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "model_name": {
+ "name": "model_name",
+ "label": "Upscaler Model",
+ "value": "RealESRGAN_x2plus.pth"
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 400
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 717.3863693661265,
+ "y": 1721.9215053134815
+ }
+ },
+ {
+ "id": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "type": "invocation",
+ "data": {
+ "id": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1650,
+ "y": 1775
+ }
+ },
+ {
+ "id": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "type": "invocation",
+ "data": {
+ "id": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 30
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 7.5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0.65
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "dpmpp_sde_k"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2128.740065979906,
+ "y": 1232.6219060454753
+ }
+ },
+ {
+ "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35",
+ "type": "invocation",
+ "data": {
+ "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 1650,
+ "y": 1600
+ }
+ },
+ {
+ "id": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "type": "invocation",
+ "data": {
+ "id": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "type": "integer_math",
+ "version": "1.0.1",
+ "label": "Get Min of Width & Height",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MIN"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 1
+ }
+ }
+ },
+ "position": {
+ "x": 722.6636820159035,
+ "y": 2088.414119794122
+ }
+ },
+ {
+ "id": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52",
+ "type": "invocation",
+ "data": {
+ "id": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52",
+ "type": "float_to_int",
+ "version": "1.0.1",
+ "label": "To Multiple of 8",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Nearest"
+ }
+ }
+ },
+ "position": {
+ "x": 724.1719300146672,
+ "y": 2135.1501652410816
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "5ca498a4-c8c8-4580-a396-0c984317205d-f50624ce-82bf-41d0-bdf7-8aab11a80d48-collapsed",
+ "type": "collapsed",
+ "source": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48"
+ },
+ {
+ "id": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a-aa9bcef8-aa90-49ea-b162-4bd613f5ea52-collapsed",
+ "type": "collapsed",
+ "source": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "target": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52"
+ },
+ {
+ "id": "eb8f6f8a-c7b1-4914-806e-045ee2717a35-f50624ce-82bf-41d0-bdf7-8aab11a80d48-collapsed",
+ "type": "collapsed",
+ "source": "eb8f6f8a-c7b1-4914-806e-045ee2717a35",
+ "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48"
+ },
+ {
+ "id": "reactflow__edge-771bdf6a-0813-4099-a5d8-921a138754d4image-f7564dd2-9539-47f2-ac13-190804461f4eimage",
+ "type": "default",
+ "source": "771bdf6a-0813-4099-a5d8-921a138754d4",
+ "target": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4eimage-1d887701-df21-4966-ae6e-a7d82307d7bdimage",
+ "type": "default",
+ "source": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "target": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dwidth-f50624ce-82bf-41d0-bdf7-8aab11a80d48width",
+ "type": "default",
+ "source": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dheight-f50624ce-82bf-41d0-bdf7-8aab11a80d48height",
+ "type": "default",
+ "source": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-f50624ce-82bf-41d0-bdf7-8aab11a80d48noise-c3737554-8d87-48ff-a6f8-e71d2867f434noise",
+ "type": "default",
+ "source": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-5ca498a4-c8c8-4580-a396-0c984317205dlatents-c3737554-8d87-48ff-a6f8-e71d2867f434latents",
+ "type": "default",
+ "source": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-e8bf67fe-67de-4227-87eb-79e86afdfc74conditioning-c3737554-8d87-48ff-a6f8-e71d2867f434negative_conditioning",
+ "type": "default",
+ "source": "e8bf67fe-67de-4227-87eb-79e86afdfc74",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16bconditioning-c3737554-8d87-48ff-a6f8-e71d2867f434positive_conditioning",
+ "type": "default",
+ "source": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dclip-63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16bclip",
+ "type": "default",
+ "source": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "target": "63b6ab7e-5b05-4d1b-a3b1-42d8e53ce16b",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dclip-e8bf67fe-67de-4227-87eb-79e86afdfc74clip",
+ "type": "default",
+ "source": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "target": "e8bf67fe-67de-4227-87eb-79e86afdfc74",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-1d887701-df21-4966-ae6e-a7d82307d7bdimage-ca1d020c-89a8-4958-880a-016d28775cfaimage",
+ "type": "default",
+ "source": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "target": "ca1d020c-89a8-4958-880a-016d28775cfa",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-ca1d020c-89a8-4958-880a-016d28775cfacontrol-c3737554-8d87-48ff-a6f8-e71d2867f434control",
+ "type": "default",
+ "source": "ca1d020c-89a8-4958-880a-016d28775cfa",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "control",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-c3737554-8d87-48ff-a6f8-e71d2867f434latents-3ed9b2ef-f4ec-40a7-94db-92e63b583ec0latents",
+ "type": "default",
+ "source": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "target": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dvae-3ed9b2ef-f4ec-40a7-94db-92e63b583ec0vae",
+ "type": "default",
+ "source": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "target": "3ed9b2ef-f4ec-40a7-94db-92e63b583ec0",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4eimage-5ca498a4-c8c8-4580-a396-0c984317205dimage",
+ "type": "default",
+ "source": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "target": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dunet-c3737554-8d87-48ff-a6f8-e71d2867f434unet",
+ "type": "default",
+ "source": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "target": "c3737554-8d87-48ff-a6f8-e71d2867f434",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-d8ace142-c05f-4f1d-8982-88dc7473958dvae-5ca498a4-c8c8-4580-a396-0c984317205dvae",
+ "type": "default",
+ "source": "d8ace142-c05f-4f1d-8982-88dc7473958d",
+ "target": "5ca498a4-c8c8-4580-a396-0c984317205d",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-eb8f6f8a-c7b1-4914-806e-045ee2717a35value-f50624ce-82bf-41d0-bdf7-8aab11a80d48seed",
+ "type": "default",
+ "source": "eb8f6f8a-c7b1-4914-806e-045ee2717a35",
+ "target": "f50624ce-82bf-41d0-bdf7-8aab11a80d48",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4ewidth-9ba14a1f-1675-4118-8b75-81c66c4b9d3aa",
+ "type": "default",
+ "source": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "target": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "sourceHandle": "width",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-f7564dd2-9539-47f2-ac13-190804461f4eheight-9ba14a1f-1675-4118-8b75-81c66c4b9d3ab",
+ "type": "default",
+ "source": "f7564dd2-9539-47f2-ac13-190804461f4e",
+ "target": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "sourceHandle": "height",
+ "targetHandle": "b"
+ },
+ {
+ "id": "reactflow__edge-9ba14a1f-1675-4118-8b75-81c66c4b9d3avalue-aa9bcef8-aa90-49ea-b162-4bd613f5ea52value",
+ "type": "default",
+ "source": "9ba14a1f-1675-4118-8b75-81c66c4b9d3a",
+ "target": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52",
+ "sourceHandle": "value",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-aa9bcef8-aa90-49ea-b162-4bd613f5ea52value-1d887701-df21-4966-ae6e-a7d82307d7bddetect_resolution",
+ "type": "default",
+ "source": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52",
+ "target": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "sourceHandle": "value",
+ "targetHandle": "detect_resolution"
+ },
+ {
+ "id": "reactflow__edge-aa9bcef8-aa90-49ea-b162-4bd613f5ea52value-1d887701-df21-4966-ae6e-a7d82307d7bdimage_resolution",
+ "type": "default",
+ "source": "aa9bcef8-aa90-49ea-b162-4bd613f5ea52",
+ "target": "1d887701-df21-4966-ae6e-a7d82307d7bd",
+ "sourceHandle": "value",
+ "targetHandle": "image_resolution"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/FLUX Image to Image.json b/invokeai/app/services/workflow_records/default_workflows/FLUX Image to Image.json
new file mode 100644
index 00000000000..741e1782dc4
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/FLUX Image to Image.json
@@ -0,0 +1,388 @@
+{
+ "id": "default_cbf0e034-7b54-4b2c-b670-3b1e2e4b4a88",
+ "name": "Image to Image - FLUX",
+ "author": "InvokeAI",
+ "description": "A simple image-to-image workflow using a FLUX dev model. ",
+ "version": "1.1.0",
+ "contact": "",
+ "tags": "flux, image to image",
+ "notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend using FLUX dev models for image-to-image workflows. The image-to-image performance with FLUX schnell models is poor.",
+ "exposedFields": [
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "t5_encoder_model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "clip_embed_model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "fieldName": "image"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "type": "invocation",
+ "data": {
+ "id": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "type": "flux_denoise",
+ "version": "3.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0.04
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": ""
+ },
+ "positive_text_conditioning": {
+ "name": "positive_text_conditioning",
+ "label": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 1024
+ },
+ "num_steps": {
+ "name": "num_steps",
+ "label": "",
+ "value": 30
+ },
+ "guidance": {
+ "name": "guidance",
+ "label": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ }
+ }
+ },
+ "position": {
+ "x": 1176.8139201354052,
+ "y": -244.36724863022368
+ }
+ },
+ {
+ "id": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "type": "invocation",
+ "data": {
+ "id": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "type": "flux_vae_encode",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 732.7680166609682,
+ "y": -24.37398171806909
+ }
+ },
+ {
+ "id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "type": "invocation",
+ "data": {
+ "id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "type": "flux_vae_decode",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 1575.5797431839133,
+ "y": -209.00150975507415
+ }
+ },
+ {
+ "id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "type": "invocation",
+ "data": {
+ "id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "type": "flux_model_loader",
+ "version": "1.0.4",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "Model (dev variant recommended for Image-to-Image)"
+ },
+ "t5_encoder_model": {
+ "name": "t5_encoder_model",
+ "label": ""
+ },
+ "clip_embed_model": {
+ "name": "clip_embed_model",
+ "label": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 328.1809894659957,
+ "y": -90.2241133566946
+ }
+ },
+ {
+ "id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "type": "invocation",
+ "data": {
+ "id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "type": "flux_text_encoder",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "t5_encoder": {
+ "name": "t5_encoder",
+ "label": ""
+ },
+ "t5_max_seq_len": {
+ "name": "t5_max_seq_len",
+ "label": "T5 Max Seq Len",
+ "value": 256
+ },
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": "a cat wearing a birthday hat"
+ }
+ }
+ },
+ "position": {
+ "x": 745.8823365057267,
+ "y": -299.60249175851914
+ }
+ },
+ {
+ "id": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "type": "invocation",
+ "data": {
+ "id": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ }
+ },
+ "position": {
+ "x": 750.4061458984118,
+ "y": 279.2179215371294
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-cd367e62-2b45-4118-b4ba-7c33e2e0b370latents-7e5172eb-48c1-44db-a770-8fd83e1435d1latents",
+ "type": "default",
+ "source": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-4754c534-a5f3-4ad0-9382-7887985e668cvalue-cd367e62-2b45-4118-b4ba-7c33e2e0b370seed",
+ "type": "default",
+ "source": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912bheight-cd367e62-2b45-4118-b4ba-7c33e2e0b370height",
+ "type": "default",
+ "source": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912bwidth-cd367e62-2b45-4118-b4ba-7c33e2e0b370width",
+ "type": "default",
+ "source": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-cd367e62-2b45-4118-b4ba-7c33e2e0b370positive_text_conditioning",
+ "type": "default",
+ "source": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_text_conditioning"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-cd367e62-2b45-4118-b4ba-7c33e2e0b370transformer",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-2981a67c-480f-4237-9384-26b68dbf912blatents-cd367e62-2b45-4118-b4ba-7c33e2e0b370latents",
+ "type": "default",
+ "source": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "target": "cd367e62-2b45-4118-b4ba-7c33e2e0b370",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-2981a67c-480f-4237-9384-26b68dbf912bvae",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "2981a67c-480f-4237-9384-26b68dbf912b",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-7e5172eb-48c1-44db-a770-8fd83e1435d1vae",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "max_seq_len",
+ "targetHandle": "t5_max_seq_len"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "t5_encoder",
+ "targetHandle": "t5_encoder"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Face Detailer with IP-Adapter & Canny (See Note in Details).json b/invokeai/app/services/workflow_records/default_workflows/Face Detailer with IP-Adapter & Canny (See Note in Details).json
new file mode 100644
index 00000000000..1e7753ea2c3
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Face Detailer with IP-Adapter & Canny (See Note in Details).json
@@ -0,0 +1,1435 @@
+{
+ "id": "default_dec5a2e9-f59c-40d9-8869-a056751d79b8",
+ "name": "Face Detailer - SD1.5",
+ "author": "kosmoskatten",
+ "description": "A workflow to add detail to and improve faces. This workflow is most effective when used with a model that creates realistic outputs. ",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, reference image, control",
+ "notes": "Set this image as the blur mask: https://i.imgur.com/Gxi61zP.png",
+ "exposedFields": [
+ {
+ "nodeId": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "2c9bc2a6-6c03-4861-aad4-db884a7682f8",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "f60b6161-8f26-42f6-89ff-545e6011e501",
+ "fieldName": "control_model"
+ },
+ {
+ "nodeId": "22b750db-b85e-486b-b278-ac983e329813",
+ "fieldName": "ip_adapter_model"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "type": "invocation",
+ "data": {
+ "id": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2031.5518710051792,
+ "y": -492.1742944307074
+ }
+ },
+ {
+ "id": "8fe598c6-d447-44fa-a165-4975af77d080",
+ "type": "invocation",
+ "data": {
+ "id": "8fe598c6-d447-44fa-a165-4975af77d080",
+ "version": "1.3.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "canny_image_processor",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "detect_resolution": {
+ "name": "detect_resolution",
+ "label": "",
+ "value": 512
+ },
+ "image_resolution": {
+ "name": "image_resolution",
+ "label": "",
+ "value": 512
+ },
+ "low_threshold": {
+ "name": "low_threshold",
+ "label": "",
+ "value": 100
+ },
+ "high_threshold": {
+ "name": "high_threshold",
+ "label": "",
+ "value": 200
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3519.4131037388597,
+ "y": 576.7946795840575
+ }
+ },
+ {
+ "id": "f60b6161-8f26-42f6-89ff-545e6011e501",
+ "type": "invocation",
+ "data": {
+ "id": "f60b6161-8f26-42f6-89ff-545e6011e501",
+ "version": "1.1.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "controlnet",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": "Control Model (select canny)"
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.5
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.5
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3950,
+ "y": 150
+ }
+ },
+ {
+ "id": "22b750db-b85e-486b-b278-ac983e329813",
+ "type": "invocation",
+ "data": {
+ "id": "22b750db-b85e-486b-b278-ac983e329813",
+ "version": "1.4.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "ip_adapter",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "ip_adapter_model": {
+ "name": "ip_adapter_model",
+ "label": "IP-Adapter Model (select IP Adapter Face)"
+ },
+ "clip_vision_model": {
+ "name": "clip_vision_model",
+ "label": "",
+ "value": "ViT-H"
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "value": 0.5
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "full"
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.8
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3575,
+ "y": -200
+ }
+ },
+ {
+ "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65",
+ "type": "invocation",
+ "data": {
+ "id": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2550,
+ "y": -525
+ }
+ },
+ {
+ "id": "2224ed72-2453-4252-bd89-3085240e0b6f",
+ "type": "invocation",
+ "data": {
+ "id": "2224ed72-2453-4252-bd89-3085240e0b6f",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 4980.1395106966565,
+ "y": -255.9158921745602
+ }
+ },
+ {
+ "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "type": "invocation",
+ "data": {
+ "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "version": "1.1.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "i2l",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3100,
+ "y": -275
+ }
+ },
+ {
+ "id": "44f2c190-eb03-460d-8d11-a94d13b33f19",
+ "type": "invocation",
+ "data": {
+ "id": "44f2c190-eb03-460d-8d11-a94d13b33f19",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2575,
+ "y": -250
+ }
+ },
+ {
+ "id": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "type": "invocation",
+ "data": {
+ "id": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_resize",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": "Blur Mask (see notes!)"
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "value": "lanczos"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4423.179487179487,
+ "y": 482.66666666666674
+ }
+ },
+ {
+ "id": "2c9bc2a6-6c03-4861-aad4-db884a7682f8",
+ "type": "invocation",
+ "data": {
+ "id": "2c9bc2a6-6c03-4861-aad4-db884a7682f8",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2050,
+ "y": -75
+ }
+ },
+ {
+ "id": "9ae34718-a17d-401d-9859-086896c29fca",
+ "type": "invocation",
+ "data": {
+ "id": "9ae34718-a17d-401d-9859-086896c29fca",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "face_off",
+ "inputs": {
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "face_id": {
+ "name": "face_id",
+ "label": "",
+ "value": 0
+ },
+ "minimum_confidence": {
+ "name": "minimum_confidence",
+ "label": "",
+ "value": 0.5
+ },
+ "x_offset": {
+ "name": "x_offset",
+ "label": "",
+ "value": 0
+ },
+ "y_offset": {
+ "name": "y_offset",
+ "label": "",
+ "value": 0
+ },
+ "padding": {
+ "name": "padding",
+ "label": "",
+ "value": 64
+ },
+ "chunk": {
+ "name": "chunk",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2575,
+ "y": 200
+ }
+ },
+ {
+ "id": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "type": "invocation",
+ "data": {
+ "id": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_resize",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "value": "lanczos"
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3000,
+ "y": 0
+ }
+ },
+ {
+ "id": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "type": "invocation",
+ "data": {
+ "id": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 40
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 3
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "Original Image Percent",
+ "value": 0.2
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "dpmpp_2m_sde_k"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4597.554345564559,
+ "y": -265.6421598623905
+ }
+ },
+ {
+ "id": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "type": "invocation",
+ "data": {
+ "id": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 123451234
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4025,
+ "y": -175
+ }
+ },
+ {
+ "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "type": "invocation",
+ "data": {
+ "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "lscale",
+ "inputs": {
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "scale_factor": {
+ "name": "scale_factor",
+ "label": "",
+ "value": 1.5
+ },
+ "mode": {
+ "name": "mode",
+ "label": "",
+ "value": "bilinear"
+ },
+ "antialias": {
+ "name": "antialias",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3075,
+ "y": -175
+ }
+ },
+ {
+ "id": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "type": "invocation",
+ "data": {
+ "id": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_paste",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "base_image": {
+ "name": "base_image",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ },
+ "x": {
+ "name": "x",
+ "label": "",
+ "value": 0
+ },
+ "y": {
+ "name": "y",
+ "label": "",
+ "value": 0
+ },
+ "crop": {
+ "name": "crop",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 6000,
+ "y": -200
+ }
+ },
+ {
+ "id": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "type": "invocation",
+ "data": {
+ "id": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_resize",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "value": "lanczos"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 5500,
+ "y": -225
+ }
+ },
+ {
+ "id": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05",
+ "type": "invocation",
+ "data": {
+ "id": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "float",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Orignal Image Percentage",
+ "value": 0.4
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4025,
+ "y": -75
+ }
+ },
+ {
+ "id": "64712037-92e8-483f-9f6e-87588539c1b8",
+ "type": "invocation",
+ "data": {
+ "id": "64712037-92e8-483f-9f6e-87588539c1b8",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "CFG Main",
+ "notes": "",
+ "type": "float",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "CFG Main",
+ "value": 6
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4035.2678120778373,
+ "y": 13.393127532980124
+ }
+ },
+ {
+ "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9",
+ "type": "invocation",
+ "data": {
+ "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 4025,
+ "y": -275
+ }
+ },
+ {
+ "id": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "type": "invocation",
+ "data": {
+ "id": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_scale",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "scale_factor": {
+ "name": "scale_factor",
+ "label": "",
+ "value": 1.5
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "value": "bicubic"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3079.916484101321,
+ "y": 151.0148192064986
+ }
+ },
+ {
+ "id": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc",
+ "type": "invocation",
+ "data": {
+ "id": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "mask_combine",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "mask1": {
+ "name": "mask1",
+ "label": ""
+ },
+ "mask2": {
+ "name": "mask2",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 5450,
+ "y": 250
+ }
+ },
+ {
+ "id": "77da4e4d-5778-4469-8449-ffed03d54bdb",
+ "type": "invocation",
+ "data": {
+ "id": "77da4e4d-5778-4469-8449-ffed03d54bdb",
+ "version": "1.2.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "img_blur",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "radius": {
+ "name": "radius",
+ "label": "Mask Blue",
+ "value": 150
+ },
+ "blur_type": {
+ "name": "blur_type",
+ "label": "",
+ "value": "gaussian"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 5000,
+ "y": 300
+ }
+ },
+ {
+ "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "type": "invocation",
+ "data": {
+ "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Face Detail Scale",
+ "notes": "The image is cropped to the face and scaled to 512x512. This value can scale even more. Best result with value between 1-2.\n\n1 = 512\n2 = 1024\n\n",
+ "type": "float",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Face Detail Scale",
+ "value": 1.5
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2578.2364832140506,
+ "y": 78.7948456497351
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "f0de6c44-4515-4f79-bcc0-dee111bcfe31-2974e5b3-3d41-4b6f-9953-cd21e8f3a323-collapsed",
+ "type": "collapsed",
+ "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323"
+ },
+ {
+ "id": "de8b1a48-a2e4-42ca-90bb-66058bffd534-2974e5b3-3d41-4b6f-9953-cd21e8f3a323-collapsed",
+ "type": "collapsed",
+ "source": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323"
+ },
+ {
+ "id": "50a8db6a-3796-4522-8547-53275efa4e7d-de8b1a48-a2e4-42ca-90bb-66058bffd534-collapsed",
+ "type": "collapsed",
+ "source": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534"
+ },
+ {
+ "id": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323-35623411-ba3a-4eaa-91fd-1e0fda0a5b42-collapsed",
+ "type": "collapsed",
+ "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42"
+ },
+ {
+ "id": "c865f39f-f830-4ed7-88a5-e935cfe050a9-35623411-ba3a-4eaa-91fd-1e0fda0a5b42-collapsed",
+ "type": "collapsed",
+ "source": "c865f39f-f830-4ed7-88a5-e935cfe050a9",
+ "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42"
+ },
+ {
+ "id": "reactflow__edge-2c9bc2a6-6c03-4861-aad4-db884a7682f8image-9ae34718-a17d-401d-9859-086896c29fcaimage",
+ "type": "default",
+ "source": "2c9bc2a6-6c03-4861-aad4-db884a7682f8",
+ "target": "9ae34718-a17d-401d-9859-086896c29fca",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaimage-50a8db6a-3796-4522-8547-53275efa4e7dimage",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-35623411-ba3a-4eaa-91fd-1e0fda0a5b42noise-bd06261d-a74a-4d1f-8374-745ed6194bc2noise",
+ "type": "default",
+ "source": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-de8b1a48-a2e4-42ca-90bb-66058bffd534latents-2974e5b3-3d41-4b6f-9953-cd21e8f3a323latents",
+ "type": "default",
+ "source": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323latents-bd06261d-a74a-4d1f-8374-745ed6194bc2latents",
+ "type": "default",
+ "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323width-35623411-ba3a-4eaa-91fd-1e0fda0a5b42width",
+ "type": "default",
+ "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-2974e5b3-3d41-4b6f-9953-cd21e8f3a323height-35623411-ba3a-4eaa-91fd-1e0fda0a5b42height",
+ "type": "default",
+ "source": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-2c9bc2a6-6c03-4861-aad4-db884a7682f8image-a7d14545-aa09-4b96-bfc5-40c009af9110base_image",
+ "type": "default",
+ "source": "2c9bc2a6-6c03-4861-aad4-db884a7682f8",
+ "target": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "sourceHandle": "image",
+ "targetHandle": "base_image"
+ },
+ {
+ "id": "reactflow__edge-2224ed72-2453-4252-bd89-3085240e0b6fimage-ff8c23dc-da7c-45b7-b5c9-d984b12f02efimage",
+ "type": "default",
+ "source": "2224ed72-2453-4252-bd89-3085240e0b6f",
+ "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcawidth-ff8c23dc-da7c-45b7-b5c9-d984b12f02efwidth",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaheight-ff8c23dc-da7c-45b7-b5c9-d984b12f02efheight",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcax-a7d14545-aa09-4b96-bfc5-40c009af9110x",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "sourceHandle": "x",
+ "targetHandle": "x"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcay-a7d14545-aa09-4b96-bfc5-40c009af9110y",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "sourceHandle": "y",
+ "targetHandle": "y"
+ },
+ {
+ "id": "reactflow__edge-50a8db6a-3796-4522-8547-53275efa4e7dimage-de8b1a48-a2e4-42ca-90bb-66058bffd534image",
+ "type": "default",
+ "source": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05value-bd06261d-a74a-4d1f-8374-745ed6194bc2denoising_start",
+ "type": "default",
+ "source": "cdfa5ab0-b3e2-43ed-85bb-2ac4aa83bc05",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "value",
+ "targetHandle": "denoising_start"
+ },
+ {
+ "id": "reactflow__edge-64712037-92e8-483f-9f6e-87588539c1b8value-bd06261d-a74a-4d1f-8374-745ed6194bc2cfg_scale",
+ "type": "default",
+ "source": "64712037-92e8-483f-9f6e-87588539c1b8",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "value",
+ "targetHandle": "cfg_scale"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcawidth-c59e815c-1f3a-4e2b-b6b8-66f4b005e955width",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcaheight-c59e815c-1f3a-4e2b-b6b8-66f4b005e955height",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-ff8c23dc-da7c-45b7-b5c9-d984b12f02efimage-a7d14545-aa09-4b96-bfc5-40c009af9110image",
+ "type": "default",
+ "source": "ff8c23dc-da7c-45b7-b5c9-d984b12f02ef",
+ "target": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-bd06261d-a74a-4d1f-8374-745ed6194bc2latents-2224ed72-2453-4252-bd89-3085240e0b6flatents",
+ "type": "default",
+ "source": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "target": "2224ed72-2453-4252-bd89-3085240e0b6f",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-c865f39f-f830-4ed7-88a5-e935cfe050a9value-35623411-ba3a-4eaa-91fd-1e0fda0a5b42seed",
+ "type": "default",
+ "source": "c865f39f-f830-4ed7-88a5-e935cfe050a9",
+ "target": "35623411-ba3a-4eaa-91fd-1e0fda0a5b42",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65conditioning-bd06261d-a74a-4d1f-8374-745ed6194bc2positive_conditioning",
+ "type": "default",
+ "source": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-44f2c190-eb03-460d-8d11-a94d13b33f19conditioning-bd06261d-a74a-4d1f-8374-745ed6194bc2negative_conditioning",
+ "type": "default",
+ "source": "44f2c190-eb03-460d-8d11-a94d13b33f19",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-22b750db-b85e-486b-b278-ac983e329813ip_adapter-bd06261d-a74a-4d1f-8374-745ed6194bc2ip_adapter",
+ "type": "default",
+ "source": "22b750db-b85e-486b-b278-ac983e329813",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "ip_adapter",
+ "targetHandle": "ip_adapter"
+ },
+ {
+ "id": "reactflow__edge-50a8db6a-3796-4522-8547-53275efa4e7dimage-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage",
+ "type": "default",
+ "source": "50a8db6a-3796-4522-8547-53275efa4e7d",
+ "target": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage-22b750db-b85e-486b-b278-ac983e329813image",
+ "type": "default",
+ "source": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "target": "22b750db-b85e-486b-b278-ac983e329813",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-8fe598c6-d447-44fa-a165-4975af77d080image-f60b6161-8f26-42f6-89ff-545e6011e501image",
+ "type": "default",
+ "source": "8fe598c6-d447-44fa-a165-4975af77d080",
+ "target": "f60b6161-8f26-42f6-89ff-545e6011e501",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-4bd4ae80-567f-4366-b8c6-3bb06f4fb46aimage-8fe598c6-d447-44fa-a165-4975af77d080image",
+ "type": "default",
+ "source": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "target": "8fe598c6-d447-44fa-a165-4975af77d080",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-f60b6161-8f26-42f6-89ff-545e6011e501control-bd06261d-a74a-4d1f-8374-745ed6194bc2control",
+ "type": "default",
+ "source": "f60b6161-8f26-42f6-89ff-545e6011e501",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "control",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-c59e815c-1f3a-4e2b-b6b8-66f4b005e955image-381d5b6a-f044-48b0-bc07-6138fbfa8dfcmask2",
+ "type": "default",
+ "source": "c59e815c-1f3a-4e2b-b6b8-66f4b005e955",
+ "target": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc",
+ "sourceHandle": "image",
+ "targetHandle": "mask2"
+ },
+ {
+ "id": "reactflow__edge-381d5b6a-f044-48b0-bc07-6138fbfa8dfcimage-a7d14545-aa09-4b96-bfc5-40c009af9110mask",
+ "type": "default",
+ "source": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc",
+ "target": "a7d14545-aa09-4b96-bfc5-40c009af9110",
+ "sourceHandle": "image",
+ "targetHandle": "mask"
+ },
+ {
+ "id": "reactflow__edge-77da4e4d-5778-4469-8449-ffed03d54bdbimage-381d5b6a-f044-48b0-bc07-6138fbfa8dfcmask1",
+ "type": "default",
+ "source": "77da4e4d-5778-4469-8449-ffed03d54bdb",
+ "target": "381d5b6a-f044-48b0-bc07-6138fbfa8dfc",
+ "sourceHandle": "image",
+ "targetHandle": "mask1"
+ },
+ {
+ "id": "reactflow__edge-9ae34718-a17d-401d-9859-086896c29fcamask-77da4e4d-5778-4469-8449-ffed03d54bdbimage",
+ "type": "default",
+ "source": "9ae34718-a17d-401d-9859-086896c29fca",
+ "target": "77da4e4d-5778-4469-8449-ffed03d54bdb",
+ "sourceHandle": "mask",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-f0de6c44-4515-4f79-bcc0-dee111bcfe31value-2974e5b3-3d41-4b6f-9953-cd21e8f3a323scale_factor",
+ "type": "default",
+ "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "target": "2974e5b3-3d41-4b6f-9953-cd21e8f3a323",
+ "sourceHandle": "value",
+ "targetHandle": "scale_factor"
+ },
+ {
+ "id": "reactflow__edge-f0de6c44-4515-4f79-bcc0-dee111bcfe31value-4bd4ae80-567f-4366-b8c6-3bb06f4fb46ascale_factor",
+ "type": "default",
+ "source": "f0de6c44-4515-4f79-bcc0-dee111bcfe31",
+ "target": "4bd4ae80-567f-4366-b8c6-3bb06f4fb46a",
+ "sourceHandle": "value",
+ "targetHandle": "scale_factor"
+ },
+ {
+ "id": "reactflow__edge-c6359181-6479-40ec-bf3a-b7e8451683b8vae-2224ed72-2453-4252-bd89-3085240e0b6fvae",
+ "type": "default",
+ "source": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "target": "2224ed72-2453-4252-bd89-3085240e0b6f",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-c6359181-6479-40ec-bf3a-b7e8451683b8clip-44f2c190-eb03-460d-8d11-a94d13b33f19clip",
+ "type": "default",
+ "source": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "target": "44f2c190-eb03-460d-8d11-a94d13b33f19",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-c6359181-6479-40ec-bf3a-b7e8451683b8clip-f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65clip",
+ "type": "default",
+ "source": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "target": "f4d15b64-c4a6-42a5-90fc-e4ed07a0ca65",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-c6359181-6479-40ec-bf3a-b7e8451683b8unet-bd06261d-a74a-4d1f-8374-745ed6194bc2unet",
+ "type": "default",
+ "source": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "target": "bd06261d-a74a-4d1f-8374-745ed6194bc2",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-c6359181-6479-40ec-bf3a-b7e8451683b8vae-de8b1a48-a2e4-42ca-90bb-66058bffd534vae",
+ "type": "default",
+ "source": "c6359181-6479-40ec-bf3a-b7e8451683b8",
+ "target": "de8b1a48-a2e4-42ca-90bb-66058bffd534",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Flux Text to Image.json b/invokeai/app/services/workflow_records/default_workflows/Flux Text to Image.json
new file mode 100644
index 00000000000..ef4575813bd
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Flux Text to Image.json
@@ -0,0 +1,324 @@
+{
+ "id": "default_444fe292-896b-44fd-bfc6-c0b5d220fffc",
+ "name": "Text to Image - FLUX",
+ "author": "InvokeAI",
+ "description": "A simple text-to-image workflow using FLUX dev or schnell models.",
+ "version": "1.1.0",
+ "contact": "",
+ "tags": "flux, text to image",
+ "notes": "Prerequisite model downloads: T5 Encoder, CLIP-L Encoder, and FLUX VAE. Quantized and un-quantized versions can be found in the starter models tab within your Model Manager. We recommend 4 steps for FLUX schnell models and 30 steps for FLUX dev models.",
+ "exposedFields": [
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "t5_encoder_model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "clip_embed_model"
+ },
+ {
+ "nodeId": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "fieldName": "prompt"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "type": "invocation",
+ "data": {
+ "id": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "type": "flux_denoise",
+ "version": "3.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": "Denoise Mask"
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": ""
+ },
+ "positive_text_conditioning": {
+ "name": "positive_text_conditioning",
+ "label": ""
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 1024
+ },
+ "num_steps": {
+ "name": "num_steps",
+ "label": "",
+ "value": 4
+ },
+ "guidance": {
+ "name": "guidance",
+ "label": "",
+ "value": 4
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ }
+ }
+ },
+ "position": {
+ "x": 1180.8001377784371,
+ "y": -219.96908055568326
+ }
+ },
+ {
+ "id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "type": "invocation",
+ "data": {
+ "id": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "type": "flux_vae_decode",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 1575.5797431839133,
+ "y": -209.00150975507415
+ }
+ },
+ {
+ "id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "type": "invocation",
+ "data": {
+ "id": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "type": "flux_model_loader",
+ "version": "1.0.4",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ },
+ "t5_encoder_model": {
+ "name": "t5_encoder_model",
+ "label": ""
+ },
+ "clip_embed_model": {
+ "name": "clip_embed_model",
+ "label": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 381.1882713063478,
+ "y": -95.89663532854017
+ }
+ },
+ {
+ "id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "type": "invocation",
+ "data": {
+ "id": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "type": "flux_text_encoder",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "t5_encoder": {
+ "name": "t5_encoder",
+ "label": ""
+ },
+ "t5_max_seq_len": {
+ "name": "t5_max_seq_len",
+ "label": "T5 Max Seq Len",
+ "value": 256
+ },
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": "a cat"
+ }
+ }
+ },
+ "position": {
+ "x": 778.4899149328337,
+ "y": -100.36469216659502
+ }
+ },
+ {
+ "id": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "type": "invocation",
+ "data": {
+ "id": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ }
+ },
+ "position": {
+ "x": 800.9667463219505,
+ "y": 285.8297267547506
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-0940bc54-21fb-4346-bc68-fca5724c2747latents-7e5172eb-48c1-44db-a770-8fd83e1435d1latents",
+ "type": "default",
+ "source": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-4754c534-a5f3-4ad0-9382-7887985e668cvalue-0940bc54-21fb-4346-bc68-fca5724c2747seed",
+ "type": "default",
+ "source": "4754c534-a5f3-4ad0-9382-7887985e668c",
+ "target": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cconditioning-0940bc54-21fb-4346-bc68-fca5724c2747positive_text_conditioning",
+ "type": "default",
+ "source": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "target": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_text_conditioning"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90transformer-0940bc54-21fb-4346-bc68-fca5724c2747transformer",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "0940bc54-21fb-4346-bc68-fca5724c2747",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90vae-7e5172eb-48c1-44db-a770-8fd83e1435d1vae",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "7e5172eb-48c1-44db-a770-8fd83e1435d1",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90max_seq_len-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_max_seq_len",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "max_seq_len",
+ "targetHandle": "t5_max_seq_len"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90t5_encoder-01f674f8-b3d1-4df1-acac-6cb8e0bfb63ct5_encoder",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "t5_encoder",
+ "targetHandle": "t5_encoder"
+ },
+ {
+ "id": "reactflow__edge-f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90clip-01f674f8-b3d1-4df1-acac-6cb8e0bfb63cclip",
+ "type": "default",
+ "source": "f8d9d7c8-9ed7-4bd7-9e42-ab0e89bfac90",
+ "target": "01f674f8-b3d1-4df1-acac-6cb8e0bfb63c",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Multi ControlNet (Canny & Depth).json b/invokeai/app/services/workflow_records/default_workflows/Multi ControlNet (Canny & Depth).json
new file mode 100644
index 00000000000..1c91f4a7b0c
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Multi ControlNet (Canny & Depth).json
@@ -0,0 +1,1004 @@
+{
+ "id": "default_2d05e719-a6b9-4e64-9310-b875d3b2f9d2",
+ "name": "Text to Image - SD1.5, Control",
+ "author": "InvokeAI",
+ "description": "A sample workflow using canny & depth ControlNets to guide the generation process. ",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, control, text to image",
+ "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"
+ },
+ {
+ "nodeId": "d204d184-f209-4fae-a0a1-d152800844e1",
+ "fieldName": "control_model"
+ },
+ {
+ "nodeId": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
+ "fieldName": "control_model"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "9db25398-c869-4a63-8815-c6559341ef12",
+ "type": "invocation",
+ "data": {
+ "id": "9db25398-c869-4a63-8815-c6559341ef12",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 5675,
+ "y": -825
+ }
+ },
+ {
+ "id": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "type": "invocation",
+ "data": {
+ "id": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "version": "1.3.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "canny_image_processor",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "detect_resolution": {
+ "name": "detect_resolution",
+ "label": "",
+ "value": 512
+ },
+ "image_resolution": {
+ "name": "image_resolution",
+ "label": "",
+ "value": 512
+ },
+ "low_threshold": {
+ "name": "low_threshold",
+ "label": "",
+ "value": 100
+ },
+ "high_threshold": {
+ "name": "high_threshold",
+ "label": "",
+ "value": 200
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4095.757337055795,
+ "y": -455.63440891935863
+ }
+ },
+ {
+ "id": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "type": "invocation",
+ "data": {
+ "id": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "version": "1.2.4",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "midas_depth_image_processor",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "a_mult": {
+ "name": "a_mult",
+ "label": "",
+ "value": 2
+ },
+ "bg_th": {
+ "name": "bg_th",
+ "label": "",
+ "value": 0.1
+ },
+ "detect_resolution": {
+ "name": "detect_resolution",
+ "label": "",
+ "value": 512
+ },
+ "image_resolution": {
+ "name": "image_resolution",
+ "label": "",
+ "value": 512
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4082.783145980783,
+ "y": 0.01629251229994111
+ }
+ },
+ {
+ "id": "d204d184-f209-4fae-a0a1-d152800844e1",
+ "type": "invocation",
+ "data": {
+ "id": "d204d184-f209-4fae-a0a1-d152800844e1",
+ "version": "1.1.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "controlnet",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": "Control Model (select canny)"
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 1
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 1
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4479.68542130465,
+ "y": -618.4221638099414
+ }
+ },
+ {
+ "id": "7ce68934-3419-42d4-ac70-82cfc9397306",
+ "type": "invocation",
+ "data": {
+ "id": "7ce68934-3419-42d4-ac70-82cfc9397306",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4075,
+ "y": -1125
+ }
+ },
+ {
+ "id": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "type": "invocation",
+ "data": {
+ "id": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "version": "1.0.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3600,
+ "y": -1000
+ }
+ },
+ {
+ "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
+ "type": "invocation",
+ "data": {
+ "id": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4075,
+ "y": -825
+ }
+ },
+ {
+ "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
+ "type": "invocation",
+ "data": {
+ "id": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
+ "version": "1.1.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "controlnet",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": "Control Model (select depth)"
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 1
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 1
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4477.604342844504,
+ "y": -49.39005411272677
+ }
+ },
+ {
+ "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
+ "type": "invocation",
+ "data": {
+ "id": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Depth Input Image"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3666.135718057363,
+ "y": 186.66887319822808
+ }
+ },
+ {
+ "id": "c4b23e64-7986-40c4-9cad-46327b12e204",
+ "type": "invocation",
+ "data": {
+ "id": "c4b23e64-7986-40c4-9cad-46327b12e204",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Canny Input Image"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3625,
+ "y": -425
+ }
+ },
+ {
+ "id": "ca4d5059-8bfb-447f-b415-da0faba5a143",
+ "type": "invocation",
+ "data": {
+ "id": "ca4d5059-8bfb-447f-b415-da0faba5a143",
+ "version": "1.0.0",
+ "label": "ControlNet Collection",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4875,
+ "y": -575
+ }
+ },
+ {
+ "id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "type": "invocation",
+ "data": {
+ "id": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 10
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 7.5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "euler"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 5274.672987098195,
+ "y": -823.0752416664332
+ }
+ },
+ {
+ "id": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b",
+ "type": "invocation",
+ "data": {
+ "id": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 4875,
+ "y": -675
+ }
+ },
+ {
+ "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce",
+ "type": "invocation",
+ "data": {
+ "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 4875,
+ "y": -750
+ }
+ },
+ {
+ "id": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "type": "invocation",
+ "data": {
+ "id": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "type": "integer_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MIN"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 1
+ }
+ }
+ },
+ "position": {
+ "x": 3673.795544334132,
+ "y": 402.7899296636469
+ }
+ },
+ {
+ "id": "1170017d-4c61-496f-897e-07e44725fc66",
+ "type": "invocation",
+ "data": {
+ "id": "1170017d-4c61-496f-897e-07e44725fc66",
+ "type": "float_to_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Nearest"
+ }
+ }
+ },
+ "position": {
+ "x": 3672.6528854992052,
+ "y": 451.92425956549766
+ }
+ },
+ {
+ "id": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "type": "invocation",
+ "data": {
+ "id": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "type": "integer_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MIN"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 1
+ }
+ }
+ },
+ "position": {
+ "x": 3638.3731204514042,
+ "y": -199.39127634275573
+ }
+ },
+ {
+ "id": "8d481737-42b5-48d5-9ab4-2e18bf3116e2",
+ "type": "invocation",
+ "data": {
+ "id": "8d481737-42b5-48d5-9ab4-2e18bf3116e2",
+ "type": "float_to_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Nearest"
+ }
+ }
+ },
+ "position": {
+ "x": 3640.658438121258,
+ "y": -144.5436522662713
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce-2e77a0a1-db6a-47a2-a8bf-1e003be6423b-collapsed",
+ "type": "collapsed",
+ "source": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce",
+ "target": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b"
+ },
+ {
+ "id": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c-8d481737-42b5-48d5-9ab4-2e18bf3116e2-collapsed",
+ "type": "collapsed",
+ "source": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "target": "8d481737-42b5-48d5-9ab4-2e18bf3116e2"
+ },
+ {
+ "id": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2-1170017d-4c61-496f-897e-07e44725fc66-collapsed",
+ "type": "collapsed",
+ "source": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "target": "1170017d-4c61-496f-897e-07e44725fc66"
+ },
+ {
+ "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-7ce68934-3419-42d4-ac70-82cfc9397306clip",
+ "type": "default",
+ "source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "target": "7ce68934-3419-42d4-ac70-82cfc9397306",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9clip-273e3f96-49ea-4dc5-9d5b-9660390f14e1clip",
+ "type": "default",
+ "source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "target": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-a33199c2-8340-401e-b8a2-42ffa875fc1ccontrol-ca4d5059-8bfb-447f-b415-da0faba5a143item",
+ "type": "default",
+ "source": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
+ "target": "ca4d5059-8bfb-447f-b415-da0faba5a143",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-d204d184-f209-4fae-a0a1-d152800844e1control-ca4d5059-8bfb-447f-b415-da0faba5a143item",
+ "type": "default",
+ "source": "d204d184-f209-4fae-a0a1-d152800844e1",
+ "target": "ca4d5059-8bfb-447f-b415-da0faba5a143",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-8e860e51-5045-456e-bf04-9a62a2a5c49eimage-018b1214-c2af-43a7-9910-fb687c6726d7image",
+ "type": "default",
+ "source": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
+ "target": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-018b1214-c2af-43a7-9910-fb687c6726d7image-a33199c2-8340-401e-b8a2-42ffa875fc1cimage",
+ "type": "default",
+ "source": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "target": "a33199c2-8340-401e-b8a2-42ffa875fc1c",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-c4b23e64-7986-40c4-9cad-46327b12e204image-c826ba5e-9676-4475-b260-07b85e88753cimage",
+ "type": "default",
+ "source": "c4b23e64-7986-40c4-9cad-46327b12e204",
+ "target": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-c826ba5e-9676-4475-b260-07b85e88753cimage-d204d184-f209-4fae-a0a1-d152800844e1image",
+ "type": "default",
+ "source": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "target": "d204d184-f209-4fae-a0a1-d152800844e1",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9vae-9db25398-c869-4a63-8815-c6559341ef12vae",
+ "type": "default",
+ "source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "target": "9db25398-c869-4a63-8815-c6559341ef12",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-ac481b7f-08bf-4a9d-9e0c-3a82ea5243celatents-9db25398-c869-4a63-8815-c6559341ef12latents",
+ "type": "default",
+ "source": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "target": "9db25398-c869-4a63-8815-c6559341ef12",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-ca4d5059-8bfb-447f-b415-da0faba5a143collection-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cecontrol",
+ "type": "default",
+ "source": "ca4d5059-8bfb-447f-b415-da0faba5a143",
+ "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "sourceHandle": "collection",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-54486974-835b-4d81-8f82-05f9f32ce9e9unet-ac481b7f-08bf-4a9d-9e0c-3a82ea5243ceunet",
+ "type": "default",
+ "source": "54486974-835b-4d81-8f82-05f9f32ce9e9",
+ "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-273e3f96-49ea-4dc5-9d5b-9660390f14e1conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cenegative_conditioning",
+ "type": "default",
+ "source": "273e3f96-49ea-4dc5-9d5b-9660390f14e1",
+ "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-7ce68934-3419-42d4-ac70-82cfc9397306conditioning-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cepositive_conditioning",
+ "type": "default",
+ "source": "7ce68934-3419-42d4-ac70-82cfc9397306",
+ "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-2e77a0a1-db6a-47a2-a8bf-1e003be6423bnoise-ac481b7f-08bf-4a9d-9e0c-3a82ea5243cenoise",
+ "type": "default",
+ "source": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b",
+ "target": "ac481b7f-08bf-4a9d-9e0c-3a82ea5243ce",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-8b260b4d-3fd6-44d4-b1be-9f0e43c628cevalue-2e77a0a1-db6a-47a2-a8bf-1e003be6423bseed",
+ "type": "default",
+ "source": "8b260b4d-3fd6-44d4-b1be-9f0e43c628ce",
+ "target": "2e77a0a1-db6a-47a2-a8bf-1e003be6423b",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-8e860e51-5045-456e-bf04-9a62a2a5c49ewidth-5d675ae3-e9c7-418d-96fe-09cd8763f2a2a",
+ "type": "default",
+ "source": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
+ "target": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "sourceHandle": "width",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-8e860e51-5045-456e-bf04-9a62a2a5c49eheight-5d675ae3-e9c7-418d-96fe-09cd8763f2a2b",
+ "type": "default",
+ "source": "8e860e51-5045-456e-bf04-9a62a2a5c49e",
+ "target": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "sourceHandle": "height",
+ "targetHandle": "b"
+ },
+ {
+ "id": "reactflow__edge-5d675ae3-e9c7-418d-96fe-09cd8763f2a2value-1170017d-4c61-496f-897e-07e44725fc66value",
+ "type": "default",
+ "source": "5d675ae3-e9c7-418d-96fe-09cd8763f2a2",
+ "target": "1170017d-4c61-496f-897e-07e44725fc66",
+ "sourceHandle": "value",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-1170017d-4c61-496f-897e-07e44725fc66value-018b1214-c2af-43a7-9910-fb687c6726d7detect_resolution",
+ "type": "default",
+ "source": "1170017d-4c61-496f-897e-07e44725fc66",
+ "target": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "sourceHandle": "value",
+ "targetHandle": "detect_resolution"
+ },
+ {
+ "id": "reactflow__edge-1170017d-4c61-496f-897e-07e44725fc66value-018b1214-c2af-43a7-9910-fb687c6726d7image_resolution",
+ "type": "default",
+ "source": "1170017d-4c61-496f-897e-07e44725fc66",
+ "target": "018b1214-c2af-43a7-9910-fb687c6726d7",
+ "sourceHandle": "value",
+ "targetHandle": "image_resolution"
+ },
+ {
+ "id": "reactflow__edge-c4b23e64-7986-40c4-9cad-46327b12e204width-6ff9f8b4-20e4-4230-8a38-37de9f756e8ca",
+ "type": "default",
+ "source": "c4b23e64-7986-40c4-9cad-46327b12e204",
+ "target": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "sourceHandle": "width",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-c4b23e64-7986-40c4-9cad-46327b12e204height-6ff9f8b4-20e4-4230-8a38-37de9f756e8cb",
+ "type": "default",
+ "source": "c4b23e64-7986-40c4-9cad-46327b12e204",
+ "target": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "sourceHandle": "height",
+ "targetHandle": "b"
+ },
+ {
+ "id": "reactflow__edge-6ff9f8b4-20e4-4230-8a38-37de9f756e8cvalue-8d481737-42b5-48d5-9ab4-2e18bf3116e2value",
+ "type": "default",
+ "source": "6ff9f8b4-20e4-4230-8a38-37de9f756e8c",
+ "target": "8d481737-42b5-48d5-9ab4-2e18bf3116e2",
+ "sourceHandle": "value",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-8d481737-42b5-48d5-9ab4-2e18bf3116e2value-c826ba5e-9676-4475-b260-07b85e88753cdetect_resolution",
+ "type": "default",
+ "source": "8d481737-42b5-48d5-9ab4-2e18bf3116e2",
+ "target": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "sourceHandle": "value",
+ "targetHandle": "detect_resolution"
+ },
+ {
+ "id": "reactflow__edge-8d481737-42b5-48d5-9ab4-2e18bf3116e2value-c826ba5e-9676-4475-b260-07b85e88753cimage_resolution",
+ "type": "default",
+ "source": "8d481737-42b5-48d5-9ab4-2e18bf3116e2",
+ "target": "c826ba5e-9676-4475-b260-07b85e88753c",
+ "sourceHandle": "value",
+ "targetHandle": "image_resolution"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SD1.5.json b/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SD1.5.json
new file mode 100644
index 00000000000..240dc933bf3
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SD1.5.json
@@ -0,0 +1,1406 @@
+{
+ "id": "default_f96e794f-eb3e-4d01-a960-9b4e43402bcf",
+ "name": "Upscaler - SD1.5, MultiDiffusion",
+ "author": "Invoke",
+ "description": "A workflow to upscale an input image with tiled upscaling, using SD1.5 based models.",
+ "version": "1.0.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, upscaling",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "scale"
+ },
+ {
+ "nodeId": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "fieldName": "board"
+ },
+ {
+ "nodeId": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "fieldName": "a"
+ },
+ {
+ "nodeId": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "fieldName": "a"
+ },
+ {
+ "nodeId": "14469dfe-9f49-4a13-89a7-eb4d45794b2b",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "33fe76a0-5efd-4482-a7f0-e2abf1223dc2",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "image_to_image_model"
+ },
+ {
+ "nodeId": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "fieldName": "model"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "33fe76a0-5efd-4482-a7f0-e2abf1223dc2",
+ "type": "invocation",
+ "data": {
+ "id": "33fe76a0-5efd-4482-a7f0-e2abf1223dc2",
+ "type": "compel",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt (Optional)",
+ "value": "blurry painting, art, sketch"
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3550,
+ "y": -2725
+ }
+ },
+ {
+ "id": "14469dfe-9f49-4a13-89a7-eb4d45794b2b",
+ "type": "invocation",
+ "data": {
+ "id": "14469dfe-9f49-4a13-89a7-eb4d45794b2b",
+ "type": "compel",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt (Optional)",
+ "value": "high quality studio lighting, photo"
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3550,
+ "y": -3025
+ }
+ },
+ {
+ "id": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "type": "invocation",
+ "data": {
+ "id": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "type": "main_model_loader",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -3050
+ }
+ },
+ {
+ "id": "71a116e1-c631-48b3-923d-acea4753b887",
+ "type": "invocation",
+ "data": {
+ "id": "71a116e1-c631-48b3-923d-acea4753b887",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.3
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1550
+ }
+ },
+ {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "type": "invocation",
+ "data": {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.025
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1575
+ }
+ },
+ {
+ "id": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "type": "invocation",
+ "data": {
+ "id": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.45
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1200
+ }
+ },
+ {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "type": "invocation",
+ "data": {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.15
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1225
+ }
+ },
+ {
+ "id": "1ed88043-3519-41d5-a895-07944f03de70",
+ "type": "invocation",
+ "data": {
+ "id": "1ed88043-3519-41d5-a895-07944f03de70",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.3
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1650
+ }
+ },
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "type": "invocation",
+ "data": {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.032
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1850
+ }
+ },
+ {
+ "id": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "type": "invocation",
+ "data": {
+ "id": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "type": "spandrel_image_to_image_autoscale",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": "Image to Upscale"
+ },
+ "image_to_image_model": {
+ "name": "image_to_image_model",
+ "label": ""
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 512
+ },
+ "scale": {
+ "name": "scale",
+ "label": "Scale (2x, 4x, 8x, 16x)",
+ "value": 2
+ },
+ "fit_to_multiple_of_8": {
+ "name": "fit_to_multiple_of_8",
+ "label": "",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -4750,
+ "y": -2125
+ }
+ },
+ {
+ "id": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "type": "invocation",
+ "data": {
+ "id": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "type": "model_identifier",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "ControlNet Model - Choose a Tile ControlNet"
+ }
+ }
+ },
+ "position": {
+ "x": -3450,
+ "y": -1450
+ }
+ },
+ {
+ "id": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "type": "invocation",
+ "data": {
+ "id": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2075
+ }
+ },
+ {
+ "id": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "type": "invocation",
+ "data": {
+ "id": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "type": "boolean",
+ "version": "1.0.1",
+ "label": "Tiled Option",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Tiled VAE (Saves VRAM, Color Inconsistency)",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -2475
+ }
+ },
+ {
+ "id": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "type": "invocation",
+ "data": {
+ "id": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "Creativity Input",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "Creativity Control (-10 to 10)",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": -1
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2350
+ }
+ },
+ {
+ "id": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "type": "invocation",
+ "data": {
+ "id": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "DIV"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 100
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -1975
+ }
+ },
+ {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "type": "invocation",
+ "data": {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "A",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 10
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2075
+ }
+ },
+ {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "type": "invocation",
+ "data": {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 4.99
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2025
+ }
+ },
+ {
+ "id": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "type": "invocation",
+ "data": {
+ "id": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "Structural Input",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "Structural Control (-10 to 10)",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 10
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -2100
+ }
+ },
+ {
+ "id": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "type": "invocation",
+ "data": {
+ "id": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "type": "collect",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -2275,
+ "y": -2075
+ }
+ },
+ {
+ "id": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "type": "invocation",
+ "data": {
+ "id": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "type": "controlnet",
+ "version": "1.1.2",
+ "label": "Initial Control (Use Tile)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": ""
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.6
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.5
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -1775
+ }
+ },
+ {
+ "id": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "type": "invocation",
+ "data": {
+ "id": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "type": "unsharp_mask",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "radius": {
+ "name": "radius",
+ "label": "",
+ "value": 2
+ },
+ "strength": {
+ "name": "strength",
+ "label": "",
+ "value": 50
+ }
+ }
+ },
+ "position": {
+ "x": -4400,
+ "y": -2125
+ }
+ },
+ {
+ "id": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "type": "invocation",
+ "data": {
+ "id": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "type": "i2l",
+ "version": "1.1.0",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2125
+ }
+ },
+ {
+ "id": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "type": "invocation",
+ "data": {
+ "id": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "type": "l2i",
+ "version": "1.3.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "Output Board"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -2825
+ }
+ },
+ {
+ "id": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "type": "invocation",
+ "data": {
+ "id": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "type": "tiled_multi_diffusion_denoise_latents",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "tile_height": {
+ "name": "tile_height",
+ "label": "",
+ "value": 768
+ },
+ "tile_width": {
+ "name": "tile_width",
+ "label": "",
+ "value": 768
+ },
+ "tile_overlap": {
+ "name": "tile_overlap",
+ "label": "",
+ "value": 128
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 25
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0.6
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "kdpm_2"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -2825
+ }
+ },
+ {
+ "id": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "type": "invocation",
+ "data": {
+ "id": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "type": "controlnet",
+ "version": "1.1.2",
+ "label": "Second Phase Control (Use Tile)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": ""
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.25
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0.5
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.85
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "Control Mode",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -1325
+ }
+ },
+ {
+ "id": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "type": "invocation",
+ "data": {
+ "id": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "type": "noise",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 3
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2025
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-009b38e3-4e17-4ac5-958c-14891991ae28vae-117f982a-03da-49b1-bf9f-29711160ac02vae",
+ "type": "default",
+ "source": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-009b38e3-4e17-4ac5-958c-14891991ae28vae-c3b60a50-8039-4924-90e3-8c608e1fecb5vae",
+ "type": "default",
+ "source": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-33fe76a0-5efd-4482-a7f0-e2abf1223dc2conditioning-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7anegative_conditioning",
+ "type": "default",
+ "source": "33fe76a0-5efd-4482-a7f0-e2abf1223dc2",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-009b38e3-4e17-4ac5-958c-14891991ae28clip-33fe76a0-5efd-4482-a7f0-e2abf1223dc2clip",
+ "type": "default",
+ "source": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "target": "33fe76a0-5efd-4482-a7f0-e2abf1223dc2",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-14469dfe-9f49-4a13-89a7-eb4d45794b2bconditioning-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7apositive_conditioning",
+ "type": "default",
+ "source": "14469dfe-9f49-4a13-89a7-eb4d45794b2b",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-009b38e3-4e17-4ac5-958c-14891991ae28clip-14469dfe-9f49-4a13-89a7-eb4d45794b2bclip",
+ "type": "default",
+ "source": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "target": "14469dfe-9f49-4a13-89a7-eb4d45794b2b",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-009b38e3-4e17-4ac5-958c-14891991ae28unet-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7aunet",
+ "type": "default",
+ "source": "009b38e3-4e17-4ac5-958c-14891991ae28",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21-75a89685-0f82-40ed-9b88-e583673be9fc-collapsed",
+ "type": "collapsed",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "75a89685-0f82-40ed-9b88-e583673be9fc"
+ },
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21-1ed88043-3519-41d5-a895-07944f03de70-collapsed",
+ "type": "collapsed",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "1ed88043-3519-41d5-a895-07944f03de70"
+ },
+ {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384-c8f5c671-8c87-4d96-a75e-a9937ac6bc03-collapsed",
+ "type": "collapsed",
+ "source": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "target": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03"
+ },
+ {
+ "id": "reactflow__edge-c8f5c671-8c87-4d96-a75e-a9937ac6bc03value-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7adenoising_start",
+ "type": "default",
+ "source": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "value",
+ "targetHandle": "denoising_start"
+ },
+ {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c-49a8cc12-aa19-48c5-b6b3-04e0b603b384-collapsed",
+ "type": "collapsed",
+ "source": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "target": "49a8cc12-aa19-48c5-b6b3-04e0b603b384"
+ },
+ {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc-96e1bcd0-326b-4b67-8b14-239da2440aec-collapsed",
+ "type": "collapsed",
+ "source": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "target": "96e1bcd0-326b-4b67-8b14-239da2440aec"
+ },
+ {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22-71a116e1-c631-48b3-923d-acea4753b887-collapsed",
+ "type": "collapsed",
+ "source": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "target": "71a116e1-c631-48b3-923d-acea4753b887"
+ },
+ {
+ "id": "reactflow__edge-71a116e1-c631-48b3-923d-acea4753b887value-be4082d6-e238-40ea-a9df-fc0d725e8895begin_step_percent",
+ "type": "default",
+ "source": "71a116e1-c631-48b3-923d-acea4753b887",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "value",
+ "targetHandle": "begin_step_percent"
+ },
+ {
+ "id": "reactflow__edge-71a116e1-c631-48b3-923d-acea4753b887value-b78f53b6-2eae-4956-97b4-7e73768d1491end_step_percent",
+ "type": "default",
+ "source": "71a116e1-c631-48b3-923d-acea4753b887",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "value",
+ "targetHandle": "end_step_percent"
+ },
+ {
+ "id": "reactflow__edge-00e2c587-f047-4413-ad15-bd31ea53ce22value-71a116e1-c631-48b3-923d-acea4753b887a",
+ "type": "default",
+ "source": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "target": "71a116e1-c631-48b3-923d-acea4753b887",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-bd094e2f-41e5-4b61-9f7b-56cf337d53favalue-00e2c587-f047-4413-ad15-bd31ea53ce22a",
+ "type": "default",
+ "source": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "target": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-96e1bcd0-326b-4b67-8b14-239da2440aecvalue-be4082d6-e238-40ea-a9df-fc0d725e8895control_weight",
+ "type": "default",
+ "source": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "value",
+ "targetHandle": "control_weight"
+ },
+ {
+ "id": "reactflow__edge-75a89685-0f82-40ed-9b88-e583673be9fcvalue-96e1bcd0-326b-4b67-8b14-239da2440aeca",
+ "type": "default",
+ "source": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "target": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-9b281506-4079-4a3d-ab40-b386156fcd21value-75a89685-0f82-40ed-9b88-e583673be9fca",
+ "type": "default",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-1ed88043-3519-41d5-a895-07944f03de70value-b78f53b6-2eae-4956-97b4-7e73768d1491control_weight",
+ "type": "default",
+ "source": "1ed88043-3519-41d5-a895-07944f03de70",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "value",
+ "targetHandle": "control_weight"
+ },
+ {
+ "id": "reactflow__edge-9b281506-4079-4a3d-ab40-b386156fcd21value-1ed88043-3519-41d5-a895-07944f03de70a",
+ "type": "default",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "1ed88043-3519-41d5-a895-07944f03de70",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-bd094e2f-41e5-4b61-9f7b-56cf337d53favalue-9b281506-4079-4a3d-ab40-b386156fcd21a",
+ "type": "default",
+ "source": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "target": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeheight-8923451b-5a27-4395-b7f2-dce875fca6f5height",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebewidth-8923451b-5a27-4395-b7f2-dce875fca6f5width",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-b78f53b6-2eae-4956-97b4-7e73768d1491image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-be4082d6-e238-40ea-a9df-fc0d725e8895image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-117f982a-03da-49b1-bf9f-29711160ac02image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-011039f6-04cf-4607-8eb1-3304eb819c8cimage-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage",
+ "type": "default",
+ "source": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "target": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-f936ebb3-6902-4df9-a775-6a68bac2da70model-be4082d6-e238-40ea-a9df-fc0d725e8895control_model",
+ "type": "default",
+ "source": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "model",
+ "targetHandle": "control_model"
+ },
+ {
+ "id": "reactflow__edge-f936ebb3-6902-4df9-a775-6a68bac2da70model-b78f53b6-2eae-4956-97b4-7e73768d1491control_model",
+ "type": "default",
+ "source": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "model",
+ "targetHandle": "control_model"
+ },
+ {
+ "id": "reactflow__edge-00239057-20d4-4cd2-a010-28727b256ea2value-8923451b-5a27-4395-b7f2-dce875fca6f5seed",
+ "type": "default",
+ "source": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-094bc4ed-5c68-4342-84f4-51056c755796value-c3b60a50-8039-4924-90e3-8c608e1fecb5tiled",
+ "type": "default",
+ "source": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "value",
+ "targetHandle": "tiled"
+ },
+ {
+ "id": "reactflow__edge-094bc4ed-5c68-4342-84f4-51056c755796value-117f982a-03da-49b1-bf9f-29711160ac02tiled",
+ "type": "default",
+ "source": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "value",
+ "targetHandle": "tiled"
+ },
+ {
+ "id": "reactflow__edge-1dd915a3-6756-48ed-b68b-ee3b4bd06c1dvalue-14e65dbe-4249-4b25-9a63-3a10cfaeb61ca",
+ "type": "default",
+ "source": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "target": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-49a8cc12-aa19-48c5-b6b3-04e0b603b384value-c8f5c671-8c87-4d96-a75e-a9937ac6bc03a",
+ "type": "default",
+ "source": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "target": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-14e65dbe-4249-4b25-9a63-3a10cfaeb61cvalue-49a8cc12-aa19-48c5-b6b3-04e0b603b384a",
+ "type": "default",
+ "source": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "target": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-6636a27a-f130-4a13-b3e5-50b44e4a566fcollection-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7acontrol",
+ "type": "default",
+ "source": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "collection",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-b78f53b6-2eae-4956-97b4-7e73768d1491control-6636a27a-f130-4a13-b3e5-50b44e4a566fitem",
+ "type": "default",
+ "source": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "target": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-be4082d6-e238-40ea-a9df-fc0d725e8895control-6636a27a-f130-4a13-b3e5-50b44e4a566fitem",
+ "type": "default",
+ "source": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "target": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7alatents-c3b60a50-8039-4924-90e3-8c608e1fecb5latents",
+ "type": "default",
+ "source": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-117f982a-03da-49b1-bf9f-29711160ac02latents-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7alatents",
+ "type": "default",
+ "source": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-8923451b-5a27-4395-b7f2-dce875fca6f5noise-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7anoise",
+ "type": "default",
+ "source": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SDXL.json b/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SDXL.json
new file mode 100644
index 00000000000..8b57bf46b6c
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/MultiDiffusion SDXL.json
@@ -0,0 +1,1624 @@
+{
+ "id": "default_35658541-6d41-4a20-8ec5-4bf2561faed0",
+ "name": "Upscaler - SDXL, MultiDiffusion",
+ "author": "Invoke",
+ "description": "A workflow to upscale an input image with tiled upscaling, using SDXL based models.",
+ "version": "1.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sdxl, upscaling",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "scale"
+ },
+ {
+ "nodeId": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "fieldName": "board"
+ },
+ {
+ "nodeId": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "fieldName": "a"
+ },
+ {
+ "nodeId": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "fieldName": "a"
+ },
+ {
+ "nodeId": "c26bff37-4f12-482f-ba45-3a5d729b4c4f",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "f5ca24ee-21c5-4c8c-8d3c-371b5079b086",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3a",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "fieldName": "image_to_image_model"
+ },
+ {
+ "nodeId": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "fieldName": "model"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "71a116e1-c631-48b3-923d-acea4753b887",
+ "type": "invocation",
+ "data": {
+ "id": "71a116e1-c631-48b3-923d-acea4753b887",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.3
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1550
+ }
+ },
+ {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "type": "invocation",
+ "data": {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.025
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1575
+ }
+ },
+ {
+ "id": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "type": "invocation",
+ "data": {
+ "id": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.45
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1200
+ }
+ },
+ {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "type": "invocation",
+ "data": {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.15
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1225
+ }
+ },
+ {
+ "id": "1ed88043-3519-41d5-a895-07944f03de70",
+ "type": "invocation",
+ "data": {
+ "id": "1ed88043-3519-41d5-a895-07944f03de70",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.3
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1650
+ }
+ },
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "type": "invocation",
+ "data": {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.032
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -1850
+ }
+ },
+ {
+ "id": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "type": "invocation",
+ "data": {
+ "id": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "type": "spandrel_image_to_image_autoscale",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": "Image to Upscale"
+ },
+ "image_to_image_model": {
+ "name": "image_to_image_model",
+ "label": "",
+ "value": {
+ "key": "38bb1a29-8ede-42ba-b77f-64b3478896eb",
+ "hash": "blake3:e52fdbee46a484ebe9b3b20ea0aac0a35a453ab6d0d353da00acfd35ce7a91ed",
+ "name": "4xNomosWebPhoto_esrgan",
+ "base": "sdxl",
+ "type": "spandrel_image_to_image"
+ }
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 512
+ },
+ "scale": {
+ "name": "scale",
+ "label": "Scale (2x, 4x, 8x, 16x)",
+ "value": 2
+ },
+ "fit_to_multiple_of_8": {
+ "name": "fit_to_multiple_of_8",
+ "label": "",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -4750,
+ "y": -2125
+ }
+ },
+ {
+ "id": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "type": "invocation",
+ "data": {
+ "id": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "type": "model_identifier",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "ControlNet Model - Choose a Tile ControlNet"
+ }
+ }
+ },
+ "position": {
+ "x": -3450,
+ "y": -1450
+ }
+ },
+ {
+ "id": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "type": "invocation",
+ "data": {
+ "id": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false,
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2075
+ }
+ },
+ {
+ "id": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "type": "invocation",
+ "data": {
+ "id": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "type": "boolean",
+ "version": "1.0.1",
+ "label": "Tiled Option",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Tiled VAE (Saves VRAM, Color Inconsistency)",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -2475
+ }
+ },
+ {
+ "id": "f5ca24ee-21c5-4c8c-8d3c-371b5079b086",
+ "type": "invocation",
+ "data": {
+ "id": "f5ca24ee-21c5-4c8c-8d3c-371b5079b086",
+ "type": "string",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Negative Prompt (Optional)",
+ "value": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2525
+ }
+ },
+ {
+ "id": "c26bff37-4f12-482f-ba45-3a5d729b4c4f",
+ "type": "invocation",
+ "data": {
+ "id": "c26bff37-4f12-482f-ba45-3a5d729b4c4f",
+ "type": "string",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Positive Prompt (Optional)",
+ "value": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2825
+ }
+ },
+ {
+ "id": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "type": "invocation",
+ "data": {
+ "id": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "Creativity Input",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "Creativity Control (-10 to 10)",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": -1
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2125
+ }
+ },
+ {
+ "id": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "type": "invocation",
+ "data": {
+ "id": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "DIV"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 100
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -1975
+ }
+ },
+ {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "type": "invocation",
+ "data": {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "A",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 10
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2075
+ }
+ },
+ {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "type": "invocation",
+ "data": {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 4.99
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2025
+ }
+ },
+ {
+ "id": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "type": "invocation",
+ "data": {
+ "id": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "type": "float_math",
+ "version": "1.0.1",
+ "label": "Structural Input",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "Structural Control (-10 to 10)",
+ "value": 0
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 10
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -2100
+ }
+ },
+ {
+ "id": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "type": "invocation",
+ "data": {
+ "id": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "type": "collect",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -2275,
+ "y": -2075
+ }
+ },
+ {
+ "id": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "type": "invocation",
+ "data": {
+ "id": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "type": "controlnet",
+ "version": "1.1.2",
+ "label": "Initial Control (Use Tile)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": ""
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.6
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.5
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -1775
+ }
+ },
+ {
+ "id": "27215391-b20e-412a-b854-7fa5927f5437",
+ "type": "invocation",
+ "data": {
+ "id": "27215391-b20e-412a-b854-7fa5927f5437",
+ "type": "sdxl_compel_prompt",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "style": {
+ "name": "style",
+ "label": "",
+ "value": ""
+ },
+ "original_width": {
+ "name": "original_width",
+ "label": "",
+ "value": 4096
+ },
+ "original_height": {
+ "name": "original_height",
+ "label": "",
+ "value": 4096
+ },
+ "crop_top": {
+ "name": "crop_top",
+ "label": "",
+ "value": 0
+ },
+ "crop_left": {
+ "name": "crop_left",
+ "label": "",
+ "value": 0
+ },
+ "target_width": {
+ "name": "target_width",
+ "label": "",
+ "value": 1024
+ },
+ "target_height": {
+ "name": "target_height",
+ "label": "",
+ "value": 1024
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "clip2": {
+ "name": "clip2",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2300
+ }
+ },
+ {
+ "id": "100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3a",
+ "type": "invocation",
+ "data": {
+ "id": "100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3a",
+ "type": "vae_loader",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "vae_model": {
+ "name": "vae_model",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2575
+ }
+ },
+ {
+ "id": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "type": "invocation",
+ "data": {
+ "id": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "type": "sdxl_model_loader",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": "SDXL Model"
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2825
+ }
+ },
+ {
+ "id": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "type": "invocation",
+ "data": {
+ "id": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "type": "sdxl_compel_prompt",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "style": {
+ "name": "style",
+ "label": "",
+ "value": ""
+ },
+ "original_width": {
+ "name": "original_width",
+ "label": "",
+ "value": 4096
+ },
+ "original_height": {
+ "name": "original_height",
+ "label": "",
+ "value": 4096
+ },
+ "crop_top": {
+ "name": "crop_top",
+ "label": "",
+ "value": 0
+ },
+ "crop_left": {
+ "name": "crop_left",
+ "label": "",
+ "value": 0
+ },
+ "target_width": {
+ "name": "target_width",
+ "label": "",
+ "value": 1024
+ },
+ "target_height": {
+ "name": "target_height",
+ "label": "",
+ "value": 1024
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "clip2": {
+ "name": "clip2",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3500,
+ "y": -2600
+ }
+ },
+ {
+ "id": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "type": "invocation",
+ "data": {
+ "id": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "type": "unsharp_mask",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "radius": {
+ "name": "radius",
+ "label": "",
+ "value": 2
+ },
+ "strength": {
+ "name": "strength",
+ "label": "",
+ "value": 50
+ }
+ }
+ },
+ "position": {
+ "x": -4400,
+ "y": -2125
+ }
+ },
+ {
+ "id": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "type": "invocation",
+ "data": {
+ "id": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "type": "i2l",
+ "version": "1.1.0",
+ "label": "",
+ "notes": "",
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2125
+ }
+ },
+ {
+ "id": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "type": "invocation",
+ "data": {
+ "id": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "type": "l2i",
+ "version": "1.3.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": "Output Board"
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -2825
+ }
+ },
+ {
+ "id": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "type": "invocation",
+ "data": {
+ "id": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "type": "tiled_multi_diffusion_denoise_latents",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "tile_height": {
+ "name": "tile_height",
+ "label": "",
+ "value": 1024
+ },
+ "tile_width": {
+ "name": "tile_width",
+ "label": "",
+ "value": 1024
+ },
+ "tile_overlap": {
+ "name": "tile_overlap",
+ "label": "",
+ "value": 128
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 25
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0.6
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "kdpm_2"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -3050,
+ "y": -2825
+ }
+ },
+ {
+ "id": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "type": "invocation",
+ "data": {
+ "id": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "type": "controlnet",
+ "version": "1.1.2",
+ "label": "Second Phase Control (Use Tile)",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": ""
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 0.25
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0.5
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 0.85
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "Control Mode",
+ "value": "balanced"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ }
+ },
+ "position": {
+ "x": -2675,
+ "y": -1325
+ }
+ },
+ {
+ "id": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "type": "invocation",
+ "data": {
+ "id": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "type": "noise",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 3
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ }
+ },
+ "position": {
+ "x": -4025,
+ "y": -2025
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21-75a89685-0f82-40ed-9b88-e583673be9fc-collapsed",
+ "type": "collapsed",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "75a89685-0f82-40ed-9b88-e583673be9fc"
+ },
+ {
+ "id": "9b281506-4079-4a3d-ab40-b386156fcd21-1ed88043-3519-41d5-a895-07944f03de70-collapsed",
+ "type": "collapsed",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "1ed88043-3519-41d5-a895-07944f03de70"
+ },
+ {
+ "id": "49a8cc12-aa19-48c5-b6b3-04e0b603b384-c8f5c671-8c87-4d96-a75e-a9937ac6bc03-collapsed",
+ "type": "collapsed",
+ "source": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "target": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03"
+ },
+ {
+ "id": "reactflow__edge-c8f5c671-8c87-4d96-a75e-a9937ac6bc03value-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7adenoising_start",
+ "type": "default",
+ "source": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "value",
+ "targetHandle": "denoising_start"
+ },
+ {
+ "id": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c-49a8cc12-aa19-48c5-b6b3-04e0b603b384-collapsed",
+ "type": "collapsed",
+ "source": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "target": "49a8cc12-aa19-48c5-b6b3-04e0b603b384"
+ },
+ {
+ "id": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d-14e65dbe-4249-4b25-9a63-3a10cfaeb61c-collapsed",
+ "type": "collapsed",
+ "source": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "target": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c"
+ },
+ {
+ "id": "75a89685-0f82-40ed-9b88-e583673be9fc-96e1bcd0-326b-4b67-8b14-239da2440aec-collapsed",
+ "type": "collapsed",
+ "source": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "target": "96e1bcd0-326b-4b67-8b14-239da2440aec"
+ },
+ {
+ "id": "00e2c587-f047-4413-ad15-bd31ea53ce22-71a116e1-c631-48b3-923d-acea4753b887-collapsed",
+ "type": "collapsed",
+ "source": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "target": "71a116e1-c631-48b3-923d-acea4753b887"
+ },
+ {
+ "id": "reactflow__edge-71a116e1-c631-48b3-923d-acea4753b887value-be4082d6-e238-40ea-a9df-fc0d725e8895begin_step_percent",
+ "type": "default",
+ "source": "71a116e1-c631-48b3-923d-acea4753b887",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "value",
+ "targetHandle": "begin_step_percent"
+ },
+ {
+ "id": "reactflow__edge-71a116e1-c631-48b3-923d-acea4753b887value-b78f53b6-2eae-4956-97b4-7e73768d1491end_step_percent",
+ "type": "default",
+ "source": "71a116e1-c631-48b3-923d-acea4753b887",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "value",
+ "targetHandle": "end_step_percent"
+ },
+ {
+ "id": "reactflow__edge-00e2c587-f047-4413-ad15-bd31ea53ce22value-71a116e1-c631-48b3-923d-acea4753b887a",
+ "type": "default",
+ "source": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "target": "71a116e1-c631-48b3-923d-acea4753b887",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-bd094e2f-41e5-4b61-9f7b-56cf337d53favalue-00e2c587-f047-4413-ad15-bd31ea53ce22a",
+ "type": "default",
+ "source": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "target": "00e2c587-f047-4413-ad15-bd31ea53ce22",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-96e1bcd0-326b-4b67-8b14-239da2440aecvalue-be4082d6-e238-40ea-a9df-fc0d725e8895control_weight",
+ "type": "default",
+ "source": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "value",
+ "targetHandle": "control_weight"
+ },
+ {
+ "id": "reactflow__edge-75a89685-0f82-40ed-9b88-e583673be9fcvalue-96e1bcd0-326b-4b67-8b14-239da2440aeca",
+ "type": "default",
+ "source": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "target": "96e1bcd0-326b-4b67-8b14-239da2440aec",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-9b281506-4079-4a3d-ab40-b386156fcd21value-75a89685-0f82-40ed-9b88-e583673be9fca",
+ "type": "default",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "75a89685-0f82-40ed-9b88-e583673be9fc",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-1ed88043-3519-41d5-a895-07944f03de70value-b78f53b6-2eae-4956-97b4-7e73768d1491control_weight",
+ "type": "default",
+ "source": "1ed88043-3519-41d5-a895-07944f03de70",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "value",
+ "targetHandle": "control_weight"
+ },
+ {
+ "id": "reactflow__edge-9b281506-4079-4a3d-ab40-b386156fcd21value-1ed88043-3519-41d5-a895-07944f03de70a",
+ "type": "default",
+ "source": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "target": "1ed88043-3519-41d5-a895-07944f03de70",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-bd094e2f-41e5-4b61-9f7b-56cf337d53favalue-9b281506-4079-4a3d-ab40-b386156fcd21a",
+ "type": "default",
+ "source": "bd094e2f-41e5-4b61-9f7b-56cf337d53fa",
+ "target": "9b281506-4079-4a3d-ab40-b386156fcd21",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeheight-8923451b-5a27-4395-b7f2-dce875fca6f5height",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebewidth-8923451b-5a27-4395-b7f2-dce875fca6f5width",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-b78f53b6-2eae-4956-97b4-7e73768d1491image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-be4082d6-e238-40ea-a9df-fc0d725e8895image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage-117f982a-03da-49b1-bf9f-29711160ac02image",
+ "type": "default",
+ "source": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-011039f6-04cf-4607-8eb1-3304eb819c8cimage-041c59cc-f9e4-4dc9-8b31-84648c5f3ebeimage",
+ "type": "default",
+ "source": "011039f6-04cf-4607-8eb1-3304eb819c8c",
+ "target": "041c59cc-f9e4-4dc9-8b31-84648c5f3ebe",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-f936ebb3-6902-4df9-a775-6a68bac2da70model-be4082d6-e238-40ea-a9df-fc0d725e8895control_model",
+ "type": "default",
+ "source": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "target": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "sourceHandle": "model",
+ "targetHandle": "control_model"
+ },
+ {
+ "id": "reactflow__edge-f936ebb3-6902-4df9-a775-6a68bac2da70model-b78f53b6-2eae-4956-97b4-7e73768d1491control_model",
+ "type": "default",
+ "source": "f936ebb3-6902-4df9-a775-6a68bac2da70",
+ "target": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "sourceHandle": "model",
+ "targetHandle": "control_model"
+ },
+ {
+ "id": "reactflow__edge-00239057-20d4-4cd2-a010-28727b256ea2value-8923451b-5a27-4395-b7f2-dce875fca6f5seed",
+ "type": "default",
+ "source": "00239057-20d4-4cd2-a010-28727b256ea2",
+ "target": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-094bc4ed-5c68-4342-84f4-51056c755796value-c3b60a50-8039-4924-90e3-8c608e1fecb5tiled",
+ "type": "default",
+ "source": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "value",
+ "targetHandle": "tiled"
+ },
+ {
+ "id": "reactflow__edge-094bc4ed-5c68-4342-84f4-51056c755796value-117f982a-03da-49b1-bf9f-29711160ac02tiled",
+ "type": "default",
+ "source": "094bc4ed-5c68-4342-84f4-51056c755796",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "value",
+ "targetHandle": "tiled"
+ },
+ {
+ "id": "reactflow__edge-f5ca24ee-21c5-4c8c-8d3c-371b5079b086value-27215391-b20e-412a-b854-7fa5927f5437style",
+ "type": "default",
+ "source": "f5ca24ee-21c5-4c8c-8d3c-371b5079b086",
+ "target": "27215391-b20e-412a-b854-7fa5927f5437",
+ "sourceHandle": "value",
+ "targetHandle": "style"
+ },
+ {
+ "id": "reactflow__edge-f5ca24ee-21c5-4c8c-8d3c-371b5079b086value-27215391-b20e-412a-b854-7fa5927f5437prompt",
+ "type": "default",
+ "source": "f5ca24ee-21c5-4c8c-8d3c-371b5079b086",
+ "target": "27215391-b20e-412a-b854-7fa5927f5437",
+ "sourceHandle": "value",
+ "targetHandle": "prompt"
+ },
+ {
+ "id": "reactflow__edge-c26bff37-4f12-482f-ba45-3a5d729b4c4fvalue-6142b69a-323f-4ecd-a7e5-67dc61349c51style",
+ "type": "default",
+ "source": "c26bff37-4f12-482f-ba45-3a5d729b4c4f",
+ "target": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "sourceHandle": "value",
+ "targetHandle": "style"
+ },
+ {
+ "id": "reactflow__edge-c26bff37-4f12-482f-ba45-3a5d729b4c4fvalue-6142b69a-323f-4ecd-a7e5-67dc61349c51prompt",
+ "type": "default",
+ "source": "c26bff37-4f12-482f-ba45-3a5d729b4c4f",
+ "target": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "sourceHandle": "value",
+ "targetHandle": "prompt"
+ },
+ {
+ "id": "reactflow__edge-1dd915a3-6756-48ed-b68b-ee3b4bd06c1dvalue-14e65dbe-4249-4b25-9a63-3a10cfaeb61ca",
+ "type": "default",
+ "source": "1dd915a3-6756-48ed-b68b-ee3b4bd06c1d",
+ "target": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-49a8cc12-aa19-48c5-b6b3-04e0b603b384value-c8f5c671-8c87-4d96-a75e-a9937ac6bc03a",
+ "type": "default",
+ "source": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "target": "c8f5c671-8c87-4d96-a75e-a9937ac6bc03",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-14e65dbe-4249-4b25-9a63-3a10cfaeb61cvalue-49a8cc12-aa19-48c5-b6b3-04e0b603b384a",
+ "type": "default",
+ "source": "14e65dbe-4249-4b25-9a63-3a10cfaeb61c",
+ "target": "49a8cc12-aa19-48c5-b6b3-04e0b603b384",
+ "sourceHandle": "value",
+ "targetHandle": "a",
+ "hidden": true
+ },
+ {
+ "id": "reactflow__edge-6636a27a-f130-4a13-b3e5-50b44e4a566fcollection-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7acontrol",
+ "type": "default",
+ "source": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "collection",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-b78f53b6-2eae-4956-97b4-7e73768d1491control-6636a27a-f130-4a13-b3e5-50b44e4a566fitem",
+ "type": "default",
+ "source": "b78f53b6-2eae-4956-97b4-7e73768d1491",
+ "target": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-be4082d6-e238-40ea-a9df-fc0d725e8895control-6636a27a-f130-4a13-b3e5-50b44e4a566fitem",
+ "type": "default",
+ "source": "be4082d6-e238-40ea-a9df-fc0d725e8895",
+ "target": "6636a27a-f130-4a13-b3e5-50b44e4a566f",
+ "sourceHandle": "control",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fdclip2-27215391-b20e-412a-b854-7fa5927f5437clip2",
+ "type": "default",
+ "source": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "target": "27215391-b20e-412a-b854-7fa5927f5437",
+ "sourceHandle": "clip2",
+ "targetHandle": "clip2"
+ },
+ {
+ "id": "reactflow__edge-e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fdclip-27215391-b20e-412a-b854-7fa5927f5437clip",
+ "type": "default",
+ "source": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "target": "27215391-b20e-412a-b854-7fa5927f5437",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fdclip2-6142b69a-323f-4ecd-a7e5-67dc61349c51clip2",
+ "type": "default",
+ "source": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "target": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "sourceHandle": "clip2",
+ "targetHandle": "clip2"
+ },
+ {
+ "id": "reactflow__edge-6142b69a-323f-4ecd-a7e5-67dc61349c51conditioning-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7apositive_conditioning",
+ "type": "default",
+ "source": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-27215391-b20e-412a-b854-7fa5927f5437conditioning-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7anegative_conditioning",
+ "type": "default",
+ "source": "27215391-b20e-412a-b854-7fa5927f5437",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fdunet-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7aunet",
+ "type": "default",
+ "source": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3avae-117f982a-03da-49b1-bf9f-29711160ac02vae",
+ "type": "default",
+ "source": "100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3a",
+ "target": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3avae-c3b60a50-8039-4924-90e3-8c608e1fecb5vae",
+ "type": "default",
+ "source": "100b3143-b3fb-4ff3-bb3c-8d4d3f89ae3a",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fdclip-6142b69a-323f-4ecd-a7e5-67dc61349c51clip",
+ "type": "default",
+ "source": "e277e4b7-01cd-4daa-86ab-7bfa3cdcd9fd",
+ "target": "6142b69a-323f-4ecd-a7e5-67dc61349c51",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7alatents-c3b60a50-8039-4924-90e3-8c608e1fecb5latents",
+ "type": "default",
+ "source": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "target": "c3b60a50-8039-4924-90e3-8c608e1fecb5",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-117f982a-03da-49b1-bf9f-29711160ac02latents-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7alatents",
+ "type": "default",
+ "source": "117f982a-03da-49b1-bf9f-29711160ac02",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-8923451b-5a27-4395-b7f2-dce875fca6f5noise-8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7anoise",
+ "type": "default",
+ "source": "8923451b-5a27-4395-b7f2-dce875fca6f5",
+ "target": "8dba0d37-cd2e-4fe5-ae9f-5464b85a8a7a",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Prompt from File.json b/invokeai/app/services/workflow_records/default_workflows/Prompt from File.json
new file mode 100644
index 00000000000..747213e140b
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Prompt from File.json
@@ -0,0 +1,516 @@
+{
+ "id": "default_d7a1c60f-ca2f-4f90-9e33-75a826ca6d8f",
+ "name": "Text to Image - SD1.5, Prompt from File",
+ "author": "InvokeAI",
+ "description": "Sample workflow using Prompt from File node",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, text to image",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "fieldName": "file_path"
+ },
+ {
+ "nodeId": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "fieldName": "pre_prompt"
+ },
+ {
+ "nodeId": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "fieldName": "post_prompt"
+ },
+ {
+ "nodeId": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
+ "fieldName": "board"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
+ "type": "invocation",
+ "data": {
+ "id": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2037.861329274915,
+ "y": -329.8393457509562
+ }
+ },
+ {
+ "id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
+ "type": "invocation",
+ "data": {
+ "id": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 925,
+ "y": -275
+ }
+ },
+ {
+ "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "type": "invocation",
+ "data": {
+ "id": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "version": "1.0.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 0,
+ "y": -375
+ }
+ },
+ {
+ "id": "c2eaf1ba-5708-4679-9e15-945b8b432692",
+ "type": "invocation",
+ "data": {
+ "id": "c2eaf1ba-5708-4679-9e15-945b8b432692",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 925,
+ "y": -200
+ }
+ },
+ {
+ "id": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "type": "invocation",
+ "data": {
+ "id": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "Prompts from File",
+ "notes": "",
+ "type": "prompt_from_file",
+ "inputs": {
+ "file_path": {
+ "name": "file_path",
+ "label": "Prompts File Path",
+ "value": ""
+ },
+ "pre_prompt": {
+ "name": "pre_prompt",
+ "label": "",
+ "value": ""
+ },
+ "post_prompt": {
+ "name": "post_prompt",
+ "label": "",
+ "value": ""
+ },
+ "start_line": {
+ "name": "start_line",
+ "label": "",
+ "value": 1
+ },
+ "max_prompts": {
+ "name": "max_prompts",
+ "label": "",
+ "value": 1
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 475,
+ "y": -400
+ }
+ },
+ {
+ "id": "1b89067c-3f6b-42c8-991f-e3055789b251",
+ "type": "invocation",
+ "data": {
+ "id": "1b89067c-3f6b-42c8-991f-e3055789b251",
+ "version": "1.1.0",
+ "label": "",
+ "notes": "",
+ "type": "iterate",
+ "inputs": {
+ "collection": {
+ "name": "collection",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 925,
+ "y": -400
+ }
+ },
+ {
+ "id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "type": "invocation",
+ "data": {
+ "id": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 925,
+ "y": 25
+ }
+ },
+ {
+ "id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
+ "type": "invocation",
+ "data": {
+ "id": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 925,
+ "y": -50
+ }
+ },
+ {
+ "id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "type": "invocation",
+ "data": {
+ "id": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 30
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 7.5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "euler"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1570.9941088179146,
+ "y": -407.6505491604564
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "1b89067c-3f6b-42c8-991f-e3055789b251-fc9d0e35-a6de-4a19-84e1-c72497c823f6-collapsed",
+ "type": "collapsed",
+ "source": "1b89067c-3f6b-42c8-991f-e3055789b251",
+ "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6"
+ },
+ {
+ "id": "reactflow__edge-1b7e0df8-8589-4915-a4ea-c0088f15d642collection-1b89067c-3f6b-42c8-991f-e3055789b251collection",
+ "type": "default",
+ "source": "1b7e0df8-8589-4915-a4ea-c0088f15d642",
+ "target": "1b89067c-3f6b-42c8-991f-e3055789b251",
+ "sourceHandle": "collection",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-fc9d0e35-a6de-4a19-84e1-c72497c823f6clip",
+ "type": "default",
+ "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-1b89067c-3f6b-42c8-991f-e3055789b251item-fc9d0e35-a6de-4a19-84e1-c72497c823f6prompt",
+ "type": "default",
+ "source": "1b89067c-3f6b-42c8-991f-e3055789b251",
+ "target": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
+ "sourceHandle": "item",
+ "targetHandle": "prompt"
+ },
+ {
+ "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426clip-c2eaf1ba-5708-4679-9e15-945b8b432692clip",
+ "type": "default",
+ "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "target": "c2eaf1ba-5708-4679-9e15-945b8b432692",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5value-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77seed",
+ "type": "default",
+ "source": "dfc20e07-7aef-4fc0-a3a1-7bf68ec6a4e5",
+ "target": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-fc9d0e35-a6de-4a19-84e1-c72497c823f6conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5epositive_conditioning",
+ "type": "default",
+ "source": "fc9d0e35-a6de-4a19-84e1-c72497c823f6",
+ "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-c2eaf1ba-5708-4679-9e15-945b8b432692conditioning-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enegative_conditioning",
+ "type": "default",
+ "source": "c2eaf1ba-5708-4679-9e15-945b8b432692",
+ "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77noise-2fb1577f-0a56-4f12-8711-8afcaaaf1d5enoise",
+ "type": "default",
+ "source": "0eb5f3f5-1b91-49eb-9ef0-41d67c7eae77",
+ "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426unet-2fb1577f-0a56-4f12-8711-8afcaaaf1d5eunet",
+ "type": "default",
+ "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "target": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-2fb1577f-0a56-4f12-8711-8afcaaaf1d5elatents-491ec988-3c77-4c37-af8a-39a0c4e7a2a1latents",
+ "type": "default",
+ "source": "2fb1577f-0a56-4f12-8711-8afcaaaf1d5e",
+ "target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-d6353b7f-b447-4e17-8f2e-80a88c91d426vae-491ec988-3c77-4c37-af8a-39a0c4e7a2a1vae",
+ "type": "default",
+ "source": "d6353b7f-b447-4e17-8f2e-80a88c91d426",
+ "target": "491ec988-3c77-4c37-af8a-39a0c4e7a2a1",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/README.md b/invokeai/app/services/workflow_records/default_workflows/README.md
new file mode 100644
index 00000000000..a70cc14c879
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/README.md
@@ -0,0 +1,19 @@
+# Default Workflows
+
+Workflows placed in this directory will be synced to the `workflow_library` as
+_default workflows_ on app startup.
+
+- Default workflows must have an id that starts with "default\_". The ID must be retained when the workflow is updated. You may need to do this manually.
+- Default workflows are not editable by users. If they are loaded and saved,
+ they will save as a copy of the default workflow.
+- Default workflows must have the `meta.category` property set to `"default"`.
+ An exception will be raised during sync if this is not set correctly.
+- Default workflows appear on the "Default Workflows" tab of the Workflow
+ Library.
+- Default workflows should not reference any resources that are user-created or installed. That includes images and models. For example, if a default workflow references Juggernaut as an SDXL model, when a user loads the workflow, even if they have a version of Juggernaut installed, it will have a different UUID. They may see a warning. So, it's best to ship default workflows without any references to these types of resources.
+
+After adding or updating default workflows, you **must** start the app up and
+load them to ensure:
+
+- The workflow loads without warning or errors
+- The workflow runs successfully
diff --git a/invokeai/app/services/workflow_records/default_workflows/SD3.5 Text to Image.json b/invokeai/app/services/workflow_records/default_workflows/SD3.5 Text to Image.json
new file mode 100644
index 00000000000..64d2a3ef779
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/SD3.5 Text to Image.json
@@ -0,0 +1,375 @@
+{
+ "id": "default_dbe46d95-22aa-43fb-9c16-94400d0ce2fd",
+ "name": "Text to Image - SD3.5",
+ "author": "InvokeAI",
+ "description": "Sample text to image workflow for Stable Diffusion 3.5",
+ "version": "1.0.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "SD3.5, text to image",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "fieldName": "prompt"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "type": "invocation",
+ "data": {
+ "id": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "type": "sd3_model_loader",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ },
+ "t5_encoder_model": {
+ "name": "t5_encoder_model",
+ "label": ""
+ },
+ "clip_l_model": {
+ "name": "clip_l_model",
+ "label": ""
+ },
+ "clip_g_model": {
+ "name": "clip_g_model",
+ "label": ""
+ },
+ "vae_model": {
+ "name": "vae_model",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": -55.58689609637031,
+ "y": -111.53602444662268
+ }
+ },
+ {
+ "id": "f7e394ac-6394-4096-abcb-de0d346506b3",
+ "type": "invocation",
+ "data": {
+ "id": "f7e394ac-6394-4096-abcb-de0d346506b3",
+ "type": "rand_int",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false,
+ "nodePack": "invokeai",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ }
+ },
+ "position": {
+ "x": 470.45870147220353,
+ "y": 350.3141781644303
+ }
+ },
+ {
+ "id": "9eb72af0-dd9e-4ec5-ad87-d65e3c01f48b",
+ "type": "invocation",
+ "data": {
+ "id": "9eb72af0-dd9e-4ec5-ad87-d65e3c01f48b",
+ "type": "sd3_l2i",
+ "version": "1.3.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ }
+ }
+ },
+ "position": {
+ "x": 1192.3097009334897,
+ "y": -366.0994675072209
+ }
+ },
+ {
+ "id": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "type": "invocation",
+ "data": {
+ "id": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "type": "sd3_text_encoder",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "clip_l": {
+ "name": "clip_l",
+ "label": ""
+ },
+ "clip_g": {
+ "name": "clip_g",
+ "label": ""
+ },
+ "t5_encoder": {
+ "name": "t5_encoder",
+ "label": ""
+ },
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ }
+ }
+ },
+ "position": {
+ "x": 408.16054647924784,
+ "y": 65.06415352118786
+ }
+ },
+ {
+ "id": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "type": "invocation",
+ "data": {
+ "id": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "type": "sd3_text_encoder",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "clip_l": {
+ "name": "clip_l",
+ "label": ""
+ },
+ "clip_g": {
+ "name": "clip_g",
+ "label": ""
+ },
+ "t5_encoder": {
+ "name": "t5_encoder",
+ "label": ""
+ },
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ }
+ }
+ },
+ "position": {
+ "x": 378.9283412440941,
+ "y": -302.65777497352553
+ }
+ },
+ {
+ "id": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "type": "invocation",
+ "data": {
+ "id": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "type": "sd3_denoise",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true,
+ "nodePack": "invokeai",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "transformer": {
+ "name": "transformer",
+ "label": ""
+ },
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 3.5
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 1024
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 30
+ },
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ }
+ }
+ },
+ "position": {
+ "x": 813.7814762740603,
+ "y": -142.20529727605867
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4cvae-9eb72af0-dd9e-4ec5-ad87-d65e3c01f48bvae",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "9eb72af0-dd9e-4ec5-ad87-d65e3c01f48b",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4ct5_encoder-3b4f7f27-cfc0-4373-a009-99c5290d0cd6t5_encoder",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "sourceHandle": "t5_encoder",
+ "targetHandle": "t5_encoder"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4ct5_encoder-e17d34e7-6ed1-493c-9a85-4fcd291cb084t5_encoder",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "sourceHandle": "t5_encoder",
+ "targetHandle": "t5_encoder"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4cclip_g-3b4f7f27-cfc0-4373-a009-99c5290d0cd6clip_g",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "sourceHandle": "clip_g",
+ "targetHandle": "clip_g"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4cclip_g-e17d34e7-6ed1-493c-9a85-4fcd291cb084clip_g",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "sourceHandle": "clip_g",
+ "targetHandle": "clip_g"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4cclip_l-3b4f7f27-cfc0-4373-a009-99c5290d0cd6clip_l",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "sourceHandle": "clip_l",
+ "targetHandle": "clip_l"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4cclip_l-e17d34e7-6ed1-493c-9a85-4fcd291cb084clip_l",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "sourceHandle": "clip_l",
+ "targetHandle": "clip_l"
+ },
+ {
+ "id": "reactflow__edge-3f22f668-0e02-4fde-a2bb-c339586ceb4ctransformer-c7539f7b-7ac5-49b9-93eb-87ede611409ftransformer",
+ "type": "default",
+ "source": "3f22f668-0e02-4fde-a2bb-c339586ceb4c",
+ "target": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "sourceHandle": "transformer",
+ "targetHandle": "transformer"
+ },
+ {
+ "id": "reactflow__edge-f7e394ac-6394-4096-abcb-de0d346506b3value-c7539f7b-7ac5-49b9-93eb-87ede611409fseed",
+ "type": "default",
+ "source": "f7e394ac-6394-4096-abcb-de0d346506b3",
+ "target": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-c7539f7b-7ac5-49b9-93eb-87ede611409flatents-9eb72af0-dd9e-4ec5-ad87-d65e3c01f48blatents",
+ "type": "default",
+ "source": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "target": "9eb72af0-dd9e-4ec5-ad87-d65e3c01f48b",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-e17d34e7-6ed1-493c-9a85-4fcd291cb084conditioning-c7539f7b-7ac5-49b9-93eb-87ede611409fpositive_conditioning",
+ "type": "default",
+ "source": "e17d34e7-6ed1-493c-9a85-4fcd291cb084",
+ "target": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-3b4f7f27-cfc0-4373-a009-99c5290d0cd6conditioning-c7539f7b-7ac5-49b9-93eb-87ede611409fnegative_conditioning",
+ "type": "default",
+ "source": "3b4f7f27-cfc0-4373-a009-99c5290d0cd6",
+ "target": "c7539f7b-7ac5-49b9-93eb-87ede611409f",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Image - SD1.5.json b/invokeai/app/services/workflow_records/default_workflows/Text to Image - SD1.5.json
new file mode 100644
index 00000000000..a6b4ddf6dfb
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Image - SD1.5.json
@@ -0,0 +1,420 @@
+{
+ "id": "default_7dde3e36-d78f-4152-9eea-00ef9c8124ed",
+ "name": "Text to Image - SD1.5",
+ "author": "InvokeAI",
+ "description": "Sample text to image workflow for Stable Diffusion 1.5/2",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "SD1.5, text to image",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
+ "fieldName": "board"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
+ "type": "invocation",
+ "data": {
+ "id": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 1800,
+ "y": 25
+ }
+ },
+ {
+ "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
+ "type": "invocation",
+ "data": {
+ "id": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "Positive Compel Prompt",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "value": "Super cute tiger cub, national geographic award-winning photograph"
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1000,
+ "y": 25
+ }
+ },
+ {
+ "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "type": "invocation",
+ "data": {
+ "id": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "version": "1.0.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 600,
+ "y": 25
+ }
+ },
+ {
+ "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
+ "type": "invocation",
+ "data": {
+ "id": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "Negative Compel Prompt",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1000,
+ "y": 350
+ }
+ },
+ {
+ "id": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "type": "invocation",
+ "data": {
+ "id": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 768
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 600,
+ "y": 325
+ }
+ },
+ {
+ "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "type": "invocation",
+ "data": {
+ "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Random Seed",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 600,
+ "y": 275
+ }
+ },
+ {
+ "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "type": "invocation",
+ "data": {
+ "id": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 30
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 7.5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "dpmpp_sde_k"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1400,
+ "y": 25
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed",
+ "type": "default",
+ "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "target": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-7d8bf987-284f-413a-b2fd-d825445a5d6cclip",
+ "type": "default",
+ "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "target": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8clip-93dc02a4-d05b-48ed-b99c-c9b616af3402clip",
+ "type": "default",
+ "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "target": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-eea2702a-19fb-45b5-9d75-56b4211ec03cnoise",
+ "type": "default",
+ "source": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-7d8bf987-284f-413a-b2fd-d825445a5d6cconditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cpositive_conditioning",
+ "type": "default",
+ "source": "7d8bf987-284f-413a-b2fd-d825445a5d6c",
+ "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-93dc02a4-d05b-48ed-b99c-c9b616af3402conditioning-eea2702a-19fb-45b5-9d75-56b4211ec03cnegative_conditioning",
+ "type": "default",
+ "source": "93dc02a4-d05b-48ed-b99c-c9b616af3402",
+ "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8unet-eea2702a-19fb-45b5-9d75-56b4211ec03cunet",
+ "type": "default",
+ "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "target": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-eea2702a-19fb-45b5-9d75-56b4211ec03clatents-58c957f5-0d01-41fc-a803-b2bbf0413d4flatents",
+ "type": "default",
+ "source": "eea2702a-19fb-45b5-9d75-56b4211ec03c",
+ "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-c8d55139-f380-4695-b7f2-8b3d1e1e3db8vae-58c957f5-0d01-41fc-a803-b2bbf0413d4fvae",
+ "type": "default",
+ "source": "c8d55139-f380-4695-b7f2-8b3d1e1e3db8",
+ "target": "58c957f5-0d01-41fc-a803-b2bbf0413d4f",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Image - SDXL.json b/invokeai/app/services/workflow_records/default_workflows/Text to Image - SDXL.json
new file mode 100644
index 00000000000..391ff46e9e5
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Image - SDXL.json
@@ -0,0 +1,704 @@
+{
+ "id": "default_5e8b008d-c697-45d0-8883-085a954c6ace",
+ "name": "Text to Image - SDXL",
+ "author": "InvokeAI",
+ "description": "Sample text to image workflow for SDXL",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "SDXL, text to image",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "ade2c0d3-0384-4157-b39b-29ce429cfa15",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "719dabe8-8297-4749-aea1-37be301cd425",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8",
+ "fieldName": "vae_model"
+ },
+ {
+ "nodeId": "63e91020-83b2-4f35-b174-ad9692aabb48",
+ "fieldName": "board"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8",
+ "type": "invocation",
+ "data": {
+ "id": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "type": "vae_loader",
+ "inputs": {
+ "vae_model": {
+ "name": "vae_model",
+ "label": "VAE (use the FP16 model)"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 375,
+ "y": -225
+ }
+ },
+ {
+ "id": "63e91020-83b2-4f35-b174-ad9692aabb48",
+ "type": "invocation",
+ "data": {
+ "id": "63e91020-83b2-4f35-b174-ad9692aabb48",
+ "version": "1.3.0",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": false
+ },
+ "position": {
+ "x": 1475,
+ "y": -500
+ }
+ },
+ {
+ "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "type": "invocation",
+ "data": {
+ "id": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "SDXL Positive Compel Prompt",
+ "notes": "",
+ "type": "sdxl_compel_prompt",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "value": ""
+ },
+ "style": {
+ "name": "style",
+ "label": "Positive Style",
+ "value": ""
+ },
+ "original_width": {
+ "name": "original_width",
+ "label": "",
+ "value": 1024
+ },
+ "original_height": {
+ "name": "original_height",
+ "label": "",
+ "value": 1024
+ },
+ "crop_top": {
+ "name": "crop_top",
+ "label": "",
+ "value": 0
+ },
+ "crop_left": {
+ "name": "crop_left",
+ "label": "",
+ "value": 0
+ },
+ "target_width": {
+ "name": "target_width",
+ "label": "",
+ "value": 1024
+ },
+ "target_height": {
+ "name": "target_height",
+ "label": "",
+ "value": 1024
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "clip2": {
+ "name": "clip2",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": -175
+ }
+ },
+ {
+ "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "type": "invocation",
+ "data": {
+ "id": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "version": "1.0.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "sdxl_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 375,
+ "y": -500
+ }
+ },
+ {
+ "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "type": "invocation",
+ "data": {
+ "id": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "version": "1.2.0",
+ "nodePack": "invokeai",
+ "label": "SDXL Negative Compel Prompt",
+ "notes": "",
+ "type": "sdxl_compel_prompt",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "value": ""
+ },
+ "style": {
+ "name": "style",
+ "label": "Negative Style",
+ "value": ""
+ },
+ "original_width": {
+ "name": "original_width",
+ "label": "",
+ "value": 1024
+ },
+ "original_height": {
+ "name": "original_height",
+ "label": "",
+ "value": 1024
+ },
+ "crop_top": {
+ "name": "crop_top",
+ "label": "",
+ "value": 0
+ },
+ "crop_left": {
+ "name": "crop_left",
+ "label": "",
+ "value": 0
+ },
+ "target_width": {
+ "name": "target_width",
+ "label": "",
+ "value": 1024
+ },
+ "target_height": {
+ "name": "target_height",
+ "label": "",
+ "value": 1024
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "clip2": {
+ "name": "clip2",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": 200
+ }
+ },
+ {
+ "id": "3774ec24-a69e-4254-864c-097d07a6256f",
+ "type": "invocation",
+ "data": {
+ "id": "3774ec24-a69e-4254-864c-097d07a6256f",
+ "version": "1.0.1",
+ "label": "Positive Style Concat",
+ "notes": "",
+ "type": "string_join",
+ "inputs": {
+ "string_left": {
+ "name": "string_left",
+ "label": "",
+ "value": ""
+ },
+ "string_right": {
+ "name": "string_right",
+ "label": "Positive Style Concat",
+ "value": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": -225
+ }
+ },
+ {
+ "id": "719dabe8-8297-4749-aea1-37be301cd425",
+ "type": "invocation",
+ "data": {
+ "id": "719dabe8-8297-4749-aea1-37be301cd425",
+ "version": "1.0.1",
+ "label": "Negative Prompt",
+ "notes": "",
+ "type": "string",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Negative Prompt",
+ "value": "photograph"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": -125
+ }
+ },
+ {
+ "id": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "type": "invocation",
+ "data": {
+ "id": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "version": "1.0.2",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 1024
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 1024
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 375,
+ "y": 0
+ }
+ },
+ {
+ "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "type": "invocation",
+ "data": {
+ "id": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "version": "1.0.1",
+ "nodePack": "invokeai",
+ "label": "Random Seed",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 375,
+ "y": -50
+ }
+ },
+ {
+ "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "type": "invocation",
+ "data": {
+ "id": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "version": "1.5.3",
+ "nodePack": "invokeai",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 32
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 6
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "dpmpp_2m_sde_k"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 1125,
+ "y": -500
+ }
+ },
+ {
+ "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15",
+ "type": "invocation",
+ "data": {
+ "id": "ade2c0d3-0384-4157-b39b-29ce429cfa15",
+ "version": "1.0.1",
+ "label": "Positive Prompt",
+ "notes": "",
+ "type": "string",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Positive Prompt",
+ "value": "Super cute tiger cub, fierce, traditional chinese watercolor"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": -500
+ }
+ },
+ {
+ "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650",
+ "type": "invocation",
+ "data": {
+ "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650",
+ "version": "1.0.1",
+ "label": "Negative Style Concat",
+ "notes": "",
+ "type": "string_join",
+ "inputs": {
+ "string_left": {
+ "name": "string_left",
+ "label": "",
+ "value": ""
+ },
+ "string_right": {
+ "name": "string_right",
+ "label": "Negative Style Prompt",
+ "value": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 750,
+ "y": 150
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "3774ec24-a69e-4254-864c-097d07a6256f-faf965a4-7530-427b-b1f3-4ba6505c2a08-collapsed",
+ "type": "collapsed",
+ "source": "3774ec24-a69e-4254-864c-097d07a6256f",
+ "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08"
+ },
+ {
+ "id": "ad8fa655-3a76-43d0-9c02-4d7644dea650-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204-collapsed",
+ "type": "collapsed",
+ "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650",
+ "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204"
+ },
+ {
+ "id": "reactflow__edge-ea94bc37-d995-4a83-aa99-4af42479f2f2value-55705012-79b9-4aac-9f26-c0b10309785bseed",
+ "type": "default",
+ "source": "ea94bc37-d995-4a83-aa99-4af42479f2f2",
+ "target": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-faf965a4-7530-427b-b1f3-4ba6505c2a08clip",
+ "type": "default",
+ "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-faf965a4-7530-427b-b1f3-4ba6505c2a08clip2",
+ "type": "default",
+ "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "sourceHandle": "clip2",
+ "targetHandle": "clip2"
+ },
+ {
+ "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip",
+ "type": "default",
+ "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22clip2-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204clip2",
+ "type": "default",
+ "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "sourceHandle": "clip2",
+ "targetHandle": "clip2"
+ },
+ {
+ "id": "reactflow__edge-30d3289c-773c-4152-a9d2-bd8a99c8fd22unet-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbunet",
+ "type": "default",
+ "source": "30d3289c-773c-4152-a9d2-bd8a99c8fd22",
+ "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-faf965a4-7530-427b-b1f3-4ba6505c2a08conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbpositive_conditioning",
+ "type": "default",
+ "source": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204conditioning-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnegative_conditioning",
+ "type": "default",
+ "source": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-55705012-79b9-4aac-9f26-c0b10309785bnoise-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfbnoise",
+ "type": "default",
+ "source": "55705012-79b9-4aac-9f26-c0b10309785b",
+ "target": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-50a36525-3c0a-4cc5-977c-e4bfc3fd6dfblatents-63e91020-83b2-4f35-b174-ad9692aabb48latents",
+ "type": "default",
+ "source": "50a36525-3c0a-4cc5-977c-e4bfc3fd6dfb",
+ "target": "63e91020-83b2-4f35-b174-ad9692aabb48",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-0093692f-9cf4-454d-a5b8-62f0e3eb3bb8vae-63e91020-83b2-4f35-b174-ad9692aabb48vae",
+ "type": "default",
+ "source": "0093692f-9cf4-454d-a5b8-62f0e3eb3bb8",
+ "target": "63e91020-83b2-4f35-b174-ad9692aabb48",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-faf965a4-7530-427b-b1f3-4ba6505c2a08prompt",
+ "type": "default",
+ "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15",
+ "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "sourceHandle": "value",
+ "targetHandle": "prompt"
+ },
+ {
+ "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204prompt",
+ "type": "default",
+ "source": "719dabe8-8297-4749-aea1-37be301cd425",
+ "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "sourceHandle": "value",
+ "targetHandle": "prompt"
+ },
+ {
+ "id": "reactflow__edge-719dabe8-8297-4749-aea1-37be301cd425value-ad8fa655-3a76-43d0-9c02-4d7644dea650string_left",
+ "type": "default",
+ "source": "719dabe8-8297-4749-aea1-37be301cd425",
+ "target": "ad8fa655-3a76-43d0-9c02-4d7644dea650",
+ "sourceHandle": "value",
+ "targetHandle": "string_left"
+ },
+ {
+ "id": "reactflow__edge-ad8fa655-3a76-43d0-9c02-4d7644dea650value-3193ad09-a7c2-4bf4-a3a9-1c61cc33a204style",
+ "type": "default",
+ "source": "ad8fa655-3a76-43d0-9c02-4d7644dea650",
+ "target": "3193ad09-a7c2-4bf4-a3a9-1c61cc33a204",
+ "sourceHandle": "value",
+ "targetHandle": "style"
+ },
+ {
+ "id": "reactflow__edge-ade2c0d3-0384-4157-b39b-29ce429cfa15value-3774ec24-a69e-4254-864c-097d07a6256fstring_left",
+ "type": "default",
+ "source": "ade2c0d3-0384-4157-b39b-29ce429cfa15",
+ "target": "3774ec24-a69e-4254-864c-097d07a6256f",
+ "sourceHandle": "value",
+ "targetHandle": "string_left"
+ },
+ {
+ "id": "reactflow__edge-3774ec24-a69e-4254-864c-097d07a6256fvalue-faf965a4-7530-427b-b1f3-4ba6505c2a08style",
+ "type": "default",
+ "source": "3774ec24-a69e-4254-864c-097d07a6256f",
+ "target": "faf965a4-7530-427b-b1f3-4ba6505c2a08",
+ "sourceHandle": "value",
+ "targetHandle": "style"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/default_workflows/Text to Image with LoRA.json b/invokeai/app/services/workflow_records/default_workflows/Text to Image with LoRA.json
new file mode 100644
index 00000000000..ca1b0bc8793
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Text to Image with LoRA.json
@@ -0,0 +1,476 @@
+{
+ "id": "default_e71d153c-2089-43c7-bd2c-f61f37d4c1c1",
+ "name": "Text to Image - SD1.5, LoRA",
+ "author": "InvokeAI",
+ "description": "Simple text to image workflow with a LoRA",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, text to image, lora",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "fieldName": "lora"
+ },
+ {
+ "nodeId": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "fieldName": "weight"
+ },
+ {
+ "nodeId": "c3fa6872-2599-4a82-a596-b3446a66cf8b",
+ "fieldName": "prompt"
+ },
+ {
+ "nodeId": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "fieldName": "width"
+ },
+ {
+ "nodeId": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "fieldName": "height"
+ },
+ {
+ "nodeId": "a9683c0a-6b1f-4a5e-8187-c57e764b3400",
+ "fieldName": "board"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400",
+ "type": "invocation",
+ "data": {
+ "id": "a9683c0a-6b1f-4a5e-8187-c57e764b3400",
+ "version": "1.3.0",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": true
+ },
+ "position": {
+ "x": 4450,
+ "y": -550
+ }
+ },
+ {
+ "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b",
+ "type": "invocation",
+ "data": {
+ "id": "c3fa6872-2599-4a82-a596-b3446a66cf8b",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "value": "super cute tiger cub"
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3425,
+ "y": -575
+ }
+ },
+ {
+ "id": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "type": "invocation",
+ "data": {
+ "id": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "type": "lora_loader",
+ "inputs": {
+ "lora": {
+ "name": "lora",
+ "label": ""
+ },
+ "weight": {
+ "name": "weight",
+ "label": "LoRA Weight",
+ "value": 1
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2975,
+ "y": -600
+ }
+ },
+ {
+ "id": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "type": "invocation",
+ "data": {
+ "id": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 2500,
+ "y": -600
+ }
+ },
+ {
+ "id": "85b77bb2-c67a-416a-b3e8-291abe746c44",
+ "type": "invocation",
+ "data": {
+ "id": "85b77bb2-c67a-416a-b3e8-291abe746c44",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Negative Prompt",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3425,
+ "y": -300
+ }
+ },
+ {
+ "id": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "type": "invocation",
+ "data": {
+ "id": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "version": "1.5.3",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 30
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 7.5
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "euler"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3975,
+ "y": -575
+ }
+ },
+ {
+ "id": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "type": "invocation",
+ "data": {
+ "id": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 768
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": 3425,
+ "y": 75
+ }
+ },
+ {
+ "id": "6fd74a17-6065-47a5-b48b-f4e2b8fa7953",
+ "type": "invocation",
+ "data": {
+ "id": "6fd74a17-6065-47a5-b48b-f4e2b8fa7953",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": 3425,
+ "y": 0
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "6fd74a17-6065-47a5-b48b-f4e2b8fa7953-ea18915f-2c5b-4569-b725-8e9e9122e8d3-collapsed",
+ "type": "collapsed",
+ "source": "6fd74a17-6065-47a5-b48b-f4e2b8fa7953",
+ "target": "ea18915f-2c5b-4569-b725-8e9e9122e8d3"
+ },
+ {
+ "id": "reactflow__edge-24e9d7ed-4836-4ec4-8f9e-e747721f9818clip-c41e705b-f2e3-4d1a-83c4-e34bb9344966clip",
+ "type": "default",
+ "source": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "target": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-c41e705b-f2e3-4d1a-83c4-e34bb9344966clip-c3fa6872-2599-4a82-a596-b3446a66cf8bclip",
+ "type": "default",
+ "source": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "target": "c3fa6872-2599-4a82-a596-b3446a66cf8b",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-24e9d7ed-4836-4ec4-8f9e-e747721f9818unet-c41e705b-f2e3-4d1a-83c4-e34bb9344966unet",
+ "type": "default",
+ "source": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "target": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-c41e705b-f2e3-4d1a-83c4-e34bb9344966unet-ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63unet",
+ "type": "default",
+ "source": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "target": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ },
+ {
+ "id": "reactflow__edge-85b77bb2-c67a-416a-b3e8-291abe746c44conditioning-ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63negative_conditioning",
+ "type": "default",
+ "source": "85b77bb2-c67a-416a-b3e8-291abe746c44",
+ "target": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-c3fa6872-2599-4a82-a596-b3446a66cf8bconditioning-ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63positive_conditioning",
+ "type": "default",
+ "source": "c3fa6872-2599-4a82-a596-b3446a66cf8b",
+ "target": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-ea18915f-2c5b-4569-b725-8e9e9122e8d3noise-ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63noise",
+ "type": "default",
+ "source": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "target": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "sourceHandle": "noise",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-6fd74a17-6065-47a5-b48b-f4e2b8fa7953value-ea18915f-2c5b-4569-b725-8e9e9122e8d3seed",
+ "type": "default",
+ "source": "6fd74a17-6065-47a5-b48b-f4e2b8fa7953",
+ "target": "ea18915f-2c5b-4569-b725-8e9e9122e8d3",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63latents-a9683c0a-6b1f-4a5e-8187-c57e764b3400latents",
+ "type": "default",
+ "source": "ad487d0c-dcbb-49c5-bb8e-b28d4cbc5a63",
+ "target": "a9683c0a-6b1f-4a5e-8187-c57e764b3400",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-24e9d7ed-4836-4ec4-8f9e-e747721f9818vae-a9683c0a-6b1f-4a5e-8187-c57e764b3400vae",
+ "type": "default",
+ "source": "24e9d7ed-4836-4ec4-8f9e-e747721f9818",
+ "target": "a9683c0a-6b1f-4a5e-8187-c57e764b3400",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-c41e705b-f2e3-4d1a-83c4-e34bb9344966clip-85b77bb2-c67a-416a-b3e8-291abe746c44clip",
+ "type": "default",
+ "source": "c41e705b-f2e3-4d1a-83c4-e34bb9344966",
+ "target": "85b77bb2-c67a-416a-b3e8-291abe746c44",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/invokeai/app/services/workflow_records/default_workflows/Tiled Upscaling (Beta).json b/invokeai/app/services/workflow_records/default_workflows/Tiled Upscaling (Beta).json
new file mode 100644
index 00000000000..7bc96cd911f
--- /dev/null
+++ b/invokeai/app/services/workflow_records/default_workflows/Tiled Upscaling (Beta).json
@@ -0,0 +1,1805 @@
+{
+ "id": "default_43b0d7f7-6a12-4dcf-a5a4-50c940cbee29",
+ "name": "Upscaler - SD1.5, Tiled",
+ "author": "Invoke",
+ "description": "A workflow to upscale an input image with tiled upscaling. ",
+ "version": "2.1.0",
+ "contact": "invoke@invoke.ai",
+ "tags": "sd1.5, upscaling",
+ "notes": "",
+ "exposedFields": [
+ {
+ "nodeId": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "fieldName": "model"
+ },
+ {
+ "nodeId": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "fieldName": "image"
+ },
+ {
+ "nodeId": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "fieldName": "value"
+ },
+ {
+ "nodeId": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a",
+ "fieldName": "a"
+ },
+ {
+ "nodeId": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "fieldName": "strength"
+ },
+ {
+ "nodeId": "d334f2da-016a-4524-9911-bdab85546888",
+ "fieldName": "end_step_percent"
+ },
+ {
+ "nodeId": "287f134f-da8d-41d1-884e-5940e8f7b816",
+ "fieldName": "ip_adapter_model"
+ },
+ {
+ "nodeId": "d334f2da-016a-4524-9911-bdab85546888",
+ "fieldName": "control_model"
+ }
+ ],
+ "meta": {
+ "version": "3.0.0",
+ "category": "default"
+ },
+ "nodes": [
+ {
+ "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "type": "invocation",
+ "data": {
+ "id": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "version": "1.0.3",
+ "label": "",
+ "notes": "",
+ "type": "main_model_loader",
+ "inputs": {
+ "model": {
+ "name": "model",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4514.466823162653,
+ "y": -1235.7908800002283
+ }
+ },
+ {
+ "id": "287f134f-da8d-41d1-884e-5940e8f7b816",
+ "type": "invocation",
+ "data": {
+ "id": "287f134f-da8d-41d1-884e-5940e8f7b816",
+ "version": "1.4.1",
+ "label": "",
+ "notes": "",
+ "type": "ip_adapter",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "ip_adapter_model": {
+ "name": "ip_adapter_model",
+ "label": "IP-Adapter Model (select ip_adapter_sd15)"
+ },
+ "clip_vision_model": {
+ "name": "clip_vision_model",
+ "label": "",
+ "value": "ViT-H"
+ },
+ "weight": {
+ "name": "weight",
+ "label": "",
+ "value": 0.2
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "full"
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "",
+ "value": 1
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -2855.8555540799207,
+ "y": -183.58854843775742
+ }
+ },
+ {
+ "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c",
+ "type": "invocation",
+ "data": {
+ "id": "b76fe66f-7884-43ad-b72c-fadc81d7a73c",
+ "version": "1.3.0",
+ "label": "",
+ "notes": "",
+ "type": "l2i",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -1999.770193862987,
+ "y": -1075
+ }
+ },
+ {
+ "id": "d334f2da-016a-4524-9911-bdab85546888",
+ "type": "invocation",
+ "data": {
+ "id": "d334f2da-016a-4524-9911-bdab85546888",
+ "version": "1.1.2",
+ "label": "",
+ "notes": "",
+ "type": "controlnet",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "control_model": {
+ "name": "control_model",
+ "label": "Control Model (select control_v11f1e_sd15_tile)"
+ },
+ "control_weight": {
+ "name": "control_weight",
+ "label": "",
+ "value": 1
+ },
+ "begin_step_percent": {
+ "name": "begin_step_percent",
+ "label": "",
+ "value": 0
+ },
+ "end_step_percent": {
+ "name": "end_step_percent",
+ "label": "Structural Control",
+ "value": 1
+ },
+ "control_mode": {
+ "name": "control_mode",
+ "label": "",
+ "value": "more_control"
+ },
+ "resize_mode": {
+ "name": "resize_mode",
+ "label": "",
+ "value": "just_resize"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -2481.9569385477016,
+ "y": -181.06590482739782
+ }
+ },
+ {
+ "id": "338b883c-3728-4f18-b3a6-6e7190c2f850",
+ "type": "invocation",
+ "data": {
+ "id": "338b883c-3728-4f18-b3a6-6e7190c2f850",
+ "version": "1.1.0",
+ "label": "",
+ "notes": "",
+ "type": "i2l",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "vae": {
+ "name": "vae",
+ "label": ""
+ },
+ "tiled": {
+ "name": "tiled",
+ "label": "",
+ "value": false
+ },
+ "tile_size": {
+ "name": "tile_size",
+ "label": "",
+ "value": 0
+ },
+ "fp32": {
+ "name": "fp32",
+ "label": "",
+ "value": false
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -2908.4791167517287,
+ "y": -408.87504820159086
+ }
+ },
+ {
+ "id": "947c3f88-0305-4695-8355-df4abac64b1c",
+ "type": "invocation",
+ "data": {
+ "id": "947c3f88-0305-4695-8355-df4abac64b1c",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4014.4136788915944,
+ "y": -968.5677253775948
+ }
+ },
+ {
+ "id": "9b2d8c58-ce8f-4162-a5a1-48de854040d6",
+ "type": "invocation",
+ "data": {
+ "id": "9b2d8c58-ce8f-4162-a5a1-48de854040d6",
+ "version": "1.2.0",
+ "label": "",
+ "notes": "",
+ "type": "compel",
+ "inputs": {
+ "prompt": {
+ "name": "prompt",
+ "label": "Positive Prompt",
+ "value": ""
+ },
+ "clip": {
+ "name": "clip",
+ "label": ""
+ },
+ "mask": {
+ "name": "mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4014.4136788915944,
+ "y": -1243.5677253775948
+ }
+ },
+ {
+ "id": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a",
+ "type": "invocation",
+ "data": {
+ "id": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a",
+ "version": "1.0.1",
+ "label": "Creativity Input",
+ "notes": "",
+ "type": "float_math",
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "DIV"
+ },
+ "a": {
+ "name": "a",
+ "label": "Creativity",
+ "value": 0.3
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 3.3
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4007.507843708216,
+ "y": -621.6878478530825
+ }
+ },
+ {
+ "id": "7dbb756b-7d79-431c-a46d-d8f7b082c127",
+ "type": "invocation",
+ "data": {
+ "id": "7dbb756b-7d79-431c-a46d-d8f7b082c127",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Floor"
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4470.518114882552,
+ "y": -246.9687512362472
+ }
+ },
+ {
+ "id": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "type": "invocation",
+ "data": {
+ "id": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "type": "image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": "Image to Upscale"
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4485.384246996007,
+ "y": -977.6662925348955
+ }
+ },
+ {
+ "id": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "type": "invocation",
+ "data": {
+ "id": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "type": "img_scale",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "scale_factor": {
+ "name": "scale_factor",
+ "label": "",
+ "value": 3
+ },
+ "resample_mode": {
+ "name": "resample_mode",
+ "label": "",
+ "value": "lanczos"
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4478.200192078582,
+ "y": 3.422855503409039
+ }
+ },
+ {
+ "id": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "type": "invocation",
+ "data": {
+ "id": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "type": "noise",
+ "inputs": {
+ "seed": {
+ "name": "seed",
+ "label": "",
+ "value": 1
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ },
+ "use_cpu": {
+ "name": "use_cpu",
+ "label": "",
+ "value": true
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -3661.44600187038,
+ "y": -86.98974389852648
+ }
+ },
+ {
+ "id": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "type": "invocation",
+ "data": {
+ "id": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "version": "1.1.0",
+ "label": "",
+ "notes": "",
+ "type": "iterate",
+ "inputs": {
+ "collection": {
+ "name": "collection",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -3651.5370216396627,
+ "y": 81.15992554066929
+ }
+ },
+ {
+ "id": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "type": "invocation",
+ "data": {
+ "id": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "tile_to_properties",
+ "inputs": {
+ "tile": {
+ "name": "tile",
+ "label": ""
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -3653.3418661289197,
+ "y": 134.9675219108736
+ }
+ },
+ {
+ "id": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "type": "invocation",
+ "data": {
+ "id": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "type": "img_crop",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "x": {
+ "name": "x",
+ "label": "",
+ "value": 0
+ },
+ "y": {
+ "name": "y",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -3253.380472583465,
+ "y": -29.08699277598673
+ }
+ },
+ {
+ "id": "1011539e-85de-4e02-a003-0b22358491b8",
+ "type": "invocation",
+ "data": {
+ "id": "1011539e-85de-4e02-a003-0b22358491b8",
+ "version": "1.5.3",
+ "label": "",
+ "notes": "",
+ "type": "denoise_latents",
+ "inputs": {
+ "positive_conditioning": {
+ "name": "positive_conditioning",
+ "label": ""
+ },
+ "negative_conditioning": {
+ "name": "negative_conditioning",
+ "label": ""
+ },
+ "noise": {
+ "name": "noise",
+ "label": ""
+ },
+ "steps": {
+ "name": "steps",
+ "label": "",
+ "value": 35
+ },
+ "cfg_scale": {
+ "name": "cfg_scale",
+ "label": "",
+ "value": 4
+ },
+ "denoising_start": {
+ "name": "denoising_start",
+ "label": "",
+ "value": 0.75
+ },
+ "denoising_end": {
+ "name": "denoising_end",
+ "label": "",
+ "value": 1
+ },
+ "scheduler": {
+ "name": "scheduler",
+ "label": "",
+ "value": "unipc"
+ },
+ "unet": {
+ "name": "unet",
+ "label": ""
+ },
+ "control": {
+ "name": "control",
+ "label": ""
+ },
+ "ip_adapter": {
+ "name": "ip_adapter",
+ "label": ""
+ },
+ "t2i_adapter": {
+ "name": "t2i_adapter",
+ "label": ""
+ },
+ "cfg_rescale_multiplier": {
+ "name": "cfg_rescale_multiplier",
+ "label": "",
+ "value": 0
+ },
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "denoise_mask": {
+ "name": "denoise_mask",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -2493.8519134413505,
+ "y": -1006.415909408244
+ }
+ },
+ {
+ "id": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527",
+ "type": "invocation",
+ "data": {
+ "id": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "pair_tile_image",
+ "inputs": {
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "tile": {
+ "name": "tile",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -1528.3086883131245,
+ "y": -847.9775129915614
+ }
+ },
+ {
+ "id": "ca0d20d1-918f-44e0-8fc3-4704dc41f4da",
+ "type": "invocation",
+ "data": {
+ "id": "ca0d20d1-918f-44e0-8fc3-4704dc41f4da",
+ "version": "1.0.0",
+ "label": "",
+ "notes": "",
+ "type": "collect",
+ "inputs": {
+ "item": {
+ "name": "item",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -1528.3086883131245,
+ "y": -647.9775129915615
+ }
+ },
+ {
+ "id": "7cedc866-2095-4bda-aa15-23f15d6273cb",
+ "type": "invocation",
+ "data": {
+ "id": "7cedc866-2095-4bda-aa15-23f15d6273cb",
+ "version": "1.1.1",
+ "label": "",
+ "notes": "",
+ "type": "merge_tiles_to_image",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "tiles_with_images": {
+ "name": "tiles_with_images",
+ "label": ""
+ },
+ "blend_mode": {
+ "name": "blend_mode",
+ "label": "",
+ "value": "Seam"
+ },
+ "blend_amount": {
+ "name": "blend_amount",
+ "label": "",
+ "value": 32
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": -1528.3086883131245,
+ "y": -522.9775129915615
+ }
+ },
+ {
+ "id": "234192f1-ee96-49be-a5d1-bad4c52a9012",
+ "type": "invocation",
+ "data": {
+ "id": "234192f1-ee96-49be-a5d1-bad4c52a9012",
+ "version": "1.2.2",
+ "label": "",
+ "notes": "",
+ "type": "save_image",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ }
+ },
+ "isOpen": true,
+ "isIntermediate": false,
+ "useCache": false
+ },
+ "position": {
+ "x": -1128.3086883131245,
+ "y": -522.9775129915615
+ }
+ },
+ {
+ "id": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "type": "invocation",
+ "data": {
+ "id": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "version": "1.0.2",
+ "label": "",
+ "notes": "",
+ "type": "crop_latents",
+ "inputs": {
+ "latents": {
+ "name": "latents",
+ "label": ""
+ },
+ "x": {
+ "name": "x",
+ "label": "",
+ "value": 0
+ },
+ "y": {
+ "name": "y",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 0
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 0
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -3253.7161754850986,
+ "y": -78.2819050861178
+ }
+ },
+ {
+ "id": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "type": "invocation",
+ "data": {
+ "id": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "version": "1.1.1",
+ "label": "",
+ "notes": "",
+ "type": "calculate_image_tiles_even_split",
+ "inputs": {
+ "image_width": {
+ "name": "image_width",
+ "label": "",
+ "value": 1024
+ },
+ "image_height": {
+ "name": "image_height",
+ "label": "",
+ "value": 1024
+ },
+ "num_tiles_x": {
+ "name": "num_tiles_x",
+ "label": "",
+ "value": 2
+ },
+ "num_tiles_y": {
+ "name": "num_tiles_y",
+ "label": "",
+ "value": 2
+ },
+ "overlap": {
+ "name": "overlap",
+ "label": "",
+ "value": 128
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4101.266011341878,
+ "y": -49.381989859546415
+ }
+ },
+ {
+ "id": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "type": "invocation",
+ "data": {
+ "id": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "version": "1.0.1",
+ "label": "Scale Factor",
+ "notes": "",
+ "type": "integer",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "Scale Factor",
+ "value": 2
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4476.853041598589,
+ "y": -41.810810454906914
+ }
+ },
+ {
+ "id": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea",
+ "type": "invocation",
+ "data": {
+ "id": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Floor"
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4472.251829335153,
+ "y": -287.93974602686
+ }
+ },
+ {
+ "id": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "type": "invocation",
+ "data": {
+ "id": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "version": "1.2.2",
+ "label": "Compatibility Cropping Mo8",
+ "notes": "",
+ "type": "img_crop",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "x": {
+ "name": "x",
+ "label": "",
+ "value": 0
+ },
+ "y": {
+ "name": "y",
+ "label": "",
+ "value": 0
+ },
+ "width": {
+ "name": "width",
+ "label": "",
+ "value": 512
+ },
+ "height": {
+ "name": "height",
+ "label": "",
+ "value": 512
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4470.138475621539,
+ "y": -201.36850691108262
+ }
+ },
+ {
+ "id": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "type": "invocation",
+ "data": {
+ "id": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "version": "1.2.2",
+ "label": "Sharpening",
+ "notes": "",
+ "type": "unsharp_mask",
+ "inputs": {
+ "board": {
+ "name": "board",
+ "label": ""
+ },
+ "metadata": {
+ "name": "metadata",
+ "label": ""
+ },
+ "image": {
+ "name": "image",
+ "label": ""
+ },
+ "radius": {
+ "name": "radius",
+ "label": "",
+ "value": 2
+ },
+ "strength": {
+ "name": "strength",
+ "label": "Sharpen Strength",
+ "value": 50
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -2904.1636287554056,
+ "y": -339.7161193204281
+ }
+ },
+ {
+ "id": "157d5318-fbc1-43e5-9ed4-5bbeda0594b0",
+ "type": "invocation",
+ "data": {
+ "id": "157d5318-fbc1-43e5-9ed4-5bbeda0594b0",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "float_math",
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "SUB"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 0.8
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 1
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4009.026283214496,
+ "y": -574.9200068395512
+ }
+ },
+ {
+ "id": "43515ab9-b46b-47db-bb46-7e0273c01d1a",
+ "type": "invocation",
+ "data": {
+ "id": "43515ab9-b46b-47db-bb46-7e0273c01d1a",
+ "version": "1.0.1",
+ "label": "",
+ "notes": "",
+ "type": "rand_int",
+ "inputs": {
+ "low": {
+ "name": "low",
+ "label": "",
+ "value": 0
+ },
+ "high": {
+ "name": "high",
+ "label": "",
+ "value": 2147483647
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": false
+ },
+ "position": {
+ "x": -3658.0647708234524,
+ "y": -136.19433892512953
+ }
+ },
+ {
+ "id": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb",
+ "type": "invocation",
+ "data": {
+ "id": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb",
+ "version": "1.0.1",
+ "label": "Multiple Check",
+ "notes": "",
+ "type": "float_to_int",
+ "inputs": {
+ "value": {
+ "name": "value",
+ "label": "",
+ "value": 0
+ },
+ "multiple": {
+ "name": "multiple",
+ "label": "",
+ "value": 8
+ },
+ "method": {
+ "name": "method",
+ "label": "",
+ "value": "Nearest"
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4092.2410416963758,
+ "y": -180.31086509172079
+ }
+ },
+ {
+ "id": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "type": "invocation",
+ "data": {
+ "id": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "version": "1.0.1",
+ "label": "Pixel Summation",
+ "notes": "",
+ "type": "float_math",
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "ADD"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 1
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4096.902679890686,
+ "y": -279.75914657034684
+ }
+ },
+ {
+ "id": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4",
+ "type": "invocation",
+ "data": {
+ "id": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4",
+ "version": "1.0.1",
+ "label": "Overlap Calc",
+ "notes": "",
+ "type": "float_math",
+ "inputs": {
+ "operation": {
+ "name": "operation",
+ "label": "",
+ "value": "MUL"
+ },
+ "a": {
+ "name": "a",
+ "label": "",
+ "value": 1
+ },
+ "b": {
+ "name": "b",
+ "label": "",
+ "value": 0.075
+ }
+ },
+ "isOpen": false,
+ "isIntermediate": true,
+ "useCache": true
+ },
+ "position": {
+ "x": -4095.348800492582,
+ "y": -230.03500583103383
+ }
+ }
+ ],
+ "edges": [
+ {
+ "id": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a-157d5318-fbc1-43e5-9ed4-5bbeda0594b0-collapsed",
+ "type": "collapsed",
+ "source": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a",
+ "target": "157d5318-fbc1-43e5-9ed4-5bbeda0594b0"
+ },
+ {
+ "id": "fad15012-0787-43a8-99dd-27f1518b5bc7-36d25df7-6408-442b-89e2-b9aba11a72c3-collapsed",
+ "type": "collapsed",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3"
+ },
+ {
+ "id": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b-36d25df7-6408-442b-89e2-b9aba11a72c3-collapsed",
+ "type": "collapsed",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3"
+ },
+ {
+ "id": "36d25df7-6408-442b-89e2-b9aba11a72c3-3f99d25c-6b43-44ec-a61a-c7ff91712621-collapsed",
+ "type": "collapsed",
+ "source": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "target": "3f99d25c-6b43-44ec-a61a-c7ff91712621"
+ },
+ {
+ "id": "3f99d25c-6b43-44ec-a61a-c7ff91712621-338b883c-3728-4f18-b3a6-6e7190c2f850-collapsed",
+ "type": "collapsed",
+ "source": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "target": "338b883c-3728-4f18-b3a6-6e7190c2f850"
+ },
+ {
+ "id": "fad15012-0787-43a8-99dd-27f1518b5bc7-b3513fed-ed42-408d-b382-128fdb0de523-collapsed",
+ "type": "collapsed",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "b3513fed-ed42-408d-b382-128fdb0de523"
+ },
+ {
+ "id": "fad15012-0787-43a8-99dd-27f1518b5bc7-1f86c8bf-06f9-4e28-abee-02f46f445ac4-collapsed",
+ "type": "collapsed",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4"
+ },
+ {
+ "id": "86fce904-9dc2-466f-837a-92fe15969b51-fad15012-0787-43a8-99dd-27f1518b5bc7-collapsed",
+ "type": "collapsed",
+ "source": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "target": "fad15012-0787-43a8-99dd-27f1518b5bc7"
+ },
+ {
+ "id": "23546dd5-a0ec-4842-9ad0-3857899b607a-fad15012-0787-43a8-99dd-27f1518b5bc7-collapsed",
+ "type": "collapsed",
+ "source": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "target": "fad15012-0787-43a8-99dd-27f1518b5bc7"
+ },
+ {
+ "id": "1f86c8bf-06f9-4e28-abee-02f46f445ac4-40de95ee-ebb5-43f7-a31a-299e76c8a5d5-collapsed",
+ "type": "collapsed",
+ "source": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "target": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5"
+ },
+ {
+ "id": "86fce904-9dc2-466f-837a-92fe15969b51-1f86c8bf-06f9-4e28-abee-02f46f445ac4-collapsed",
+ "type": "collapsed",
+ "source": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4"
+ },
+ {
+ "id": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb-1f86c8bf-06f9-4e28-abee-02f46f445ac4-collapsed",
+ "type": "collapsed",
+ "source": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4"
+ },
+ {
+ "id": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea-23546dd5-a0ec-4842-9ad0-3857899b607a-collapsed",
+ "type": "collapsed",
+ "source": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea",
+ "target": "23546dd5-a0ec-4842-9ad0-3857899b607a"
+ },
+ {
+ "id": "7dbb756b-7d79-431c-a46d-d8f7b082c127-23546dd5-a0ec-4842-9ad0-3857899b607a-collapsed",
+ "type": "collapsed",
+ "source": "7dbb756b-7d79-431c-a46d-d8f7b082c127",
+ "target": "23546dd5-a0ec-4842-9ad0-3857899b607a"
+ },
+ {
+ "id": "23546dd5-a0ec-4842-9ad0-3857899b607a-f87a3783-ac5c-43f8-8f97-6688a2aefba5-collapsed",
+ "type": "collapsed",
+ "source": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "target": "f87a3783-ac5c-43f8-8f97-6688a2aefba5"
+ },
+ {
+ "id": "f87a3783-ac5c-43f8-8f97-6688a2aefba5-d62d4d15-e03a-4c10-86ba-3e58da98d2a4-collapsed",
+ "type": "collapsed",
+ "source": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "target": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4"
+ },
+ {
+ "id": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4-e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb-collapsed",
+ "type": "collapsed",
+ "source": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4",
+ "target": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb"
+ },
+ {
+ "id": "b3513fed-ed42-408d-b382-128fdb0de523-54dd79ec-fb65-45a6-a5d7-f20109f88b49-collapsed",
+ "type": "collapsed",
+ "source": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49"
+ },
+ {
+ "id": "43515ab9-b46b-47db-bb46-7e0273c01d1a-b3513fed-ed42-408d-b382-128fdb0de523-collapsed",
+ "type": "collapsed",
+ "source": "43515ab9-b46b-47db-bb46-7e0273c01d1a",
+ "target": "b3513fed-ed42-408d-b382-128fdb0de523"
+ },
+ {
+ "id": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b-54dd79ec-fb65-45a6-a5d7-f20109f88b49-collapsed",
+ "type": "collapsed",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49"
+ },
+ {
+ "id": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5-857eb5ce-8e5e-4bda-8a33-3e52e57db67b-collapsed",
+ "type": "collapsed",
+ "source": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "target": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b"
+ },
+ {
+ "id": "reactflow__edge-fad15012-0787-43a8-99dd-27f1518b5bc7width-b3513fed-ed42-408d-b382-128fdb0de523width",
+ "type": "default",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-fad15012-0787-43a8-99dd-27f1518b5bc7height-b3513fed-ed42-408d-b382-128fdb0de523height",
+ "type": "default",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-40de95ee-ebb5-43f7-a31a-299e76c8a5d5item-857eb5ce-8e5e-4bda-8a33-3e52e57db67btile",
+ "type": "default",
+ "source": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "target": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "sourceHandle": "item",
+ "targetHandle": "tile"
+ },
+ {
+ "id": "reactflow__edge-fad15012-0787-43a8-99dd-27f1518b5bc7image-36d25df7-6408-442b-89e2-b9aba11a72c3image",
+ "type": "default",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bcoords_top-36d25df7-6408-442b-89e2-b9aba11a72c3y",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "sourceHandle": "coords_top",
+ "targetHandle": "y"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bcoords_left-36d25df7-6408-442b-89e2-b9aba11a72c3x",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "sourceHandle": "coords_left",
+ "targetHandle": "x"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bwidth-36d25df7-6408-442b-89e2-b9aba11a72c3width",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bheight-36d25df7-6408-442b-89e2-b9aba11a72c3height",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-9b2d8c58-ce8f-4162-a5a1-48de854040d6conditioning-1011539e-85de-4e02-a003-0b22358491b8positive_conditioning",
+ "type": "default",
+ "source": "9b2d8c58-ce8f-4162-a5a1-48de854040d6",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "conditioning",
+ "targetHandle": "positive_conditioning"
+ },
+ {
+ "id": "reactflow__edge-947c3f88-0305-4695-8355-df4abac64b1cconditioning-1011539e-85de-4e02-a003-0b22358491b8negative_conditioning",
+ "type": "default",
+ "source": "947c3f88-0305-4695-8355-df4abac64b1c",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "conditioning",
+ "targetHandle": "negative_conditioning"
+ },
+ {
+ "id": "reactflow__edge-338b883c-3728-4f18-b3a6-6e7190c2f850latents-1011539e-85de-4e02-a003-0b22358491b8latents",
+ "type": "default",
+ "source": "338b883c-3728-4f18-b3a6-6e7190c2f850",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-1011539e-85de-4e02-a003-0b22358491b8latents-b76fe66f-7884-43ad-b72c-fadc81d7a73clatents",
+ "type": "default",
+ "source": "1011539e-85de-4e02-a003-0b22358491b8",
+ "target": "b76fe66f-7884-43ad-b72c-fadc81d7a73c",
+ "sourceHandle": "latents",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-b76fe66f-7884-43ad-b72c-fadc81d7a73cimage-ab6f5dda-4b60-4ddf-99f2-f61fb5937527image",
+ "type": "default",
+ "source": "b76fe66f-7884-43ad-b72c-fadc81d7a73c",
+ "target": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-40de95ee-ebb5-43f7-a31a-299e76c8a5d5item-ab6f5dda-4b60-4ddf-99f2-f61fb5937527tile",
+ "type": "default",
+ "source": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "target": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527",
+ "sourceHandle": "item",
+ "targetHandle": "tile"
+ },
+ {
+ "id": "reactflow__edge-ab6f5dda-4b60-4ddf-99f2-f61fb5937527tile_with_image-ca0d20d1-918f-44e0-8fc3-4704dc41f4daitem",
+ "type": "default",
+ "source": "ab6f5dda-4b60-4ddf-99f2-f61fb5937527",
+ "target": "ca0d20d1-918f-44e0-8fc3-4704dc41f4da",
+ "sourceHandle": "tile_with_image",
+ "targetHandle": "item"
+ },
+ {
+ "id": "reactflow__edge-ca0d20d1-918f-44e0-8fc3-4704dc41f4dacollection-7cedc866-2095-4bda-aa15-23f15d6273cbtiles_with_images",
+ "type": "default",
+ "source": "ca0d20d1-918f-44e0-8fc3-4704dc41f4da",
+ "target": "7cedc866-2095-4bda-aa15-23f15d6273cb",
+ "sourceHandle": "collection",
+ "targetHandle": "tiles_with_images"
+ },
+ {
+ "id": "reactflow__edge-7cedc866-2095-4bda-aa15-23f15d6273cbimage-234192f1-ee96-49be-a5d1-bad4c52a9012image",
+ "type": "default",
+ "source": "7cedc866-2095-4bda-aa15-23f15d6273cb",
+ "target": "234192f1-ee96-49be-a5d1-bad4c52a9012",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-b3513fed-ed42-408d-b382-128fdb0de523noise-54dd79ec-fb65-45a6-a5d7-f20109f88b49latents",
+ "type": "default",
+ "source": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "sourceHandle": "noise",
+ "targetHandle": "latents"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bwidth-54dd79ec-fb65-45a6-a5d7-f20109f88b49width",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "sourceHandle": "width",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bheight-54dd79ec-fb65-45a6-a5d7-f20109f88b49height",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "sourceHandle": "height",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bcoords_left-54dd79ec-fb65-45a6-a5d7-f20109f88b49x",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "sourceHandle": "coords_left",
+ "targetHandle": "x"
+ },
+ {
+ "id": "reactflow__edge-857eb5ce-8e5e-4bda-8a33-3e52e57db67bcoords_top-54dd79ec-fb65-45a6-a5d7-f20109f88b49y",
+ "type": "default",
+ "source": "857eb5ce-8e5e-4bda-8a33-3e52e57db67b",
+ "target": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "sourceHandle": "coords_top",
+ "targetHandle": "y"
+ },
+ {
+ "id": "reactflow__edge-54dd79ec-fb65-45a6-a5d7-f20109f88b49latents-1011539e-85de-4e02-a003-0b22358491b8noise",
+ "type": "default",
+ "source": "54dd79ec-fb65-45a6-a5d7-f20109f88b49",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "latents",
+ "targetHandle": "noise"
+ },
+ {
+ "id": "reactflow__edge-287f134f-da8d-41d1-884e-5940e8f7b816ip_adapter-1011539e-85de-4e02-a003-0b22358491b8ip_adapter",
+ "type": "default",
+ "source": "287f134f-da8d-41d1-884e-5940e8f7b816",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "ip_adapter",
+ "targetHandle": "ip_adapter"
+ },
+ {
+ "id": "reactflow__edge-36d25df7-6408-442b-89e2-b9aba11a72c3image-287f134f-da8d-41d1-884e-5940e8f7b816image",
+ "type": "default",
+ "source": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "target": "287f134f-da8d-41d1-884e-5940e8f7b816",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-1f86c8bf-06f9-4e28-abee-02f46f445ac4tiles-40de95ee-ebb5-43f7-a31a-299e76c8a5d5collection",
+ "type": "default",
+ "source": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "target": "40de95ee-ebb5-43f7-a31a-299e76c8a5d5",
+ "sourceHandle": "tiles",
+ "targetHandle": "collection"
+ },
+ {
+ "id": "reactflow__edge-fad15012-0787-43a8-99dd-27f1518b5bc7width-1f86c8bf-06f9-4e28-abee-02f46f445ac4image_width",
+ "type": "default",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "sourceHandle": "width",
+ "targetHandle": "image_width"
+ },
+ {
+ "id": "reactflow__edge-fad15012-0787-43a8-99dd-27f1518b5bc7height-1f86c8bf-06f9-4e28-abee-02f46f445ac4image_height",
+ "type": "default",
+ "source": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "sourceHandle": "height",
+ "targetHandle": "image_height"
+ },
+ {
+ "id": "reactflow__edge-86fce904-9dc2-466f-837a-92fe15969b51value-fad15012-0787-43a8-99dd-27f1518b5bc7scale_factor",
+ "type": "default",
+ "source": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "target": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "sourceHandle": "value",
+ "targetHandle": "scale_factor"
+ },
+ {
+ "id": "reactflow__edge-86fce904-9dc2-466f-837a-92fe15969b51value-1f86c8bf-06f9-4e28-abee-02f46f445ac4num_tiles_x",
+ "type": "default",
+ "source": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "sourceHandle": "value",
+ "targetHandle": "num_tiles_x"
+ },
+ {
+ "id": "reactflow__edge-86fce904-9dc2-466f-837a-92fe15969b51value-1f86c8bf-06f9-4e28-abee-02f46f445ac4num_tiles_y",
+ "type": "default",
+ "source": "86fce904-9dc2-466f-837a-92fe15969b51",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "sourceHandle": "value",
+ "targetHandle": "num_tiles_y"
+ },
+ {
+ "id": "reactflow__edge-2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5clip-9b2d8c58-ce8f-4162-a5a1-48de854040d6clip",
+ "type": "default",
+ "source": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "target": "9b2d8c58-ce8f-4162-a5a1-48de854040d6",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5clip-947c3f88-0305-4695-8355-df4abac64b1cclip",
+ "type": "default",
+ "source": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "target": "947c3f88-0305-4695-8355-df4abac64b1c",
+ "sourceHandle": "clip",
+ "targetHandle": "clip"
+ },
+ {
+ "id": "reactflow__edge-5ca87ace-edf9-49c7-a424-cd42416b86a7width-f5d9bf3b-2646-4b17-9894-20fd2b4218eavalue",
+ "type": "default",
+ "source": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "target": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea",
+ "sourceHandle": "width",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-5ca87ace-edf9-49c7-a424-cd42416b86a7height-7dbb756b-7d79-431c-a46d-d8f7b082c127value",
+ "type": "default",
+ "source": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "target": "7dbb756b-7d79-431c-a46d-d8f7b082c127",
+ "sourceHandle": "height",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-f5d9bf3b-2646-4b17-9894-20fd2b4218eavalue-23546dd5-a0ec-4842-9ad0-3857899b607awidth",
+ "type": "default",
+ "source": "f5d9bf3b-2646-4b17-9894-20fd2b4218ea",
+ "target": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "sourceHandle": "value",
+ "targetHandle": "width"
+ },
+ {
+ "id": "reactflow__edge-7dbb756b-7d79-431c-a46d-d8f7b082c127value-23546dd5-a0ec-4842-9ad0-3857899b607aheight",
+ "type": "default",
+ "source": "7dbb756b-7d79-431c-a46d-d8f7b082c127",
+ "target": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "sourceHandle": "value",
+ "targetHandle": "height"
+ },
+ {
+ "id": "reactflow__edge-23546dd5-a0ec-4842-9ad0-3857899b607aimage-fad15012-0787-43a8-99dd-27f1518b5bc7image",
+ "type": "default",
+ "source": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "target": "fad15012-0787-43a8-99dd-27f1518b5bc7",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-5ca87ace-edf9-49c7-a424-cd42416b86a7image-23546dd5-a0ec-4842-9ad0-3857899b607aimage",
+ "type": "default",
+ "source": "5ca87ace-edf9-49c7-a424-cd42416b86a7",
+ "target": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-d334f2da-016a-4524-9911-bdab85546888control-1011539e-85de-4e02-a003-0b22358491b8control",
+ "type": "default",
+ "source": "d334f2da-016a-4524-9911-bdab85546888",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "control",
+ "targetHandle": "control"
+ },
+ {
+ "id": "reactflow__edge-36d25df7-6408-442b-89e2-b9aba11a72c3image-3f99d25c-6b43-44ec-a61a-c7ff91712621image",
+ "type": "default",
+ "source": "36d25df7-6408-442b-89e2-b9aba11a72c3",
+ "target": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-3f99d25c-6b43-44ec-a61a-c7ff91712621image-338b883c-3728-4f18-b3a6-6e7190c2f850image",
+ "type": "default",
+ "source": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "target": "338b883c-3728-4f18-b3a6-6e7190c2f850",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-3f99d25c-6b43-44ec-a61a-c7ff91712621image-d334f2da-016a-4524-9911-bdab85546888image",
+ "type": "default",
+ "source": "3f99d25c-6b43-44ec-a61a-c7ff91712621",
+ "target": "d334f2da-016a-4524-9911-bdab85546888",
+ "sourceHandle": "image",
+ "targetHandle": "image"
+ },
+ {
+ "id": "reactflow__edge-b875cae6-d8a3-4fdc-b969-4d53cbd03f9avalue-157d5318-fbc1-43e5-9ed4-5bbeda0594b0b",
+ "type": "default",
+ "source": "b875cae6-d8a3-4fdc-b969-4d53cbd03f9a",
+ "target": "157d5318-fbc1-43e5-9ed4-5bbeda0594b0",
+ "sourceHandle": "value",
+ "targetHandle": "b"
+ },
+ {
+ "id": "reactflow__edge-157d5318-fbc1-43e5-9ed4-5bbeda0594b0value-1011539e-85de-4e02-a003-0b22358491b8denoising_start",
+ "type": "default",
+ "source": "157d5318-fbc1-43e5-9ed4-5bbeda0594b0",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "value",
+ "targetHandle": "denoising_start"
+ },
+ {
+ "id": "reactflow__edge-43515ab9-b46b-47db-bb46-7e0273c01d1avalue-b3513fed-ed42-408d-b382-128fdb0de523seed",
+ "type": "default",
+ "source": "43515ab9-b46b-47db-bb46-7e0273c01d1a",
+ "target": "b3513fed-ed42-408d-b382-128fdb0de523",
+ "sourceHandle": "value",
+ "targetHandle": "seed"
+ },
+ {
+ "id": "reactflow__edge-e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bbvalue-1f86c8bf-06f9-4e28-abee-02f46f445ac4overlap",
+ "type": "default",
+ "source": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb",
+ "target": "1f86c8bf-06f9-4e28-abee-02f46f445ac4",
+ "sourceHandle": "value",
+ "targetHandle": "overlap"
+ },
+ {
+ "id": "reactflow__edge-23546dd5-a0ec-4842-9ad0-3857899b607awidth-f87a3783-ac5c-43f8-8f97-6688a2aefba5a",
+ "type": "default",
+ "source": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "target": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "sourceHandle": "width",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-23546dd5-a0ec-4842-9ad0-3857899b607aheight-f87a3783-ac5c-43f8-8f97-6688a2aefba5b",
+ "type": "default",
+ "source": "23546dd5-a0ec-4842-9ad0-3857899b607a",
+ "target": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "sourceHandle": "height",
+ "targetHandle": "b"
+ },
+ {
+ "id": "reactflow__edge-f87a3783-ac5c-43f8-8f97-6688a2aefba5value-d62d4d15-e03a-4c10-86ba-3e58da98d2a4a",
+ "type": "default",
+ "source": "f87a3783-ac5c-43f8-8f97-6688a2aefba5",
+ "target": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4",
+ "sourceHandle": "value",
+ "targetHandle": "a"
+ },
+ {
+ "id": "reactflow__edge-d62d4d15-e03a-4c10-86ba-3e58da98d2a4value-e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bbvalue",
+ "type": "default",
+ "source": "d62d4d15-e03a-4c10-86ba-3e58da98d2a4",
+ "target": "e9b5a7e1-6e8a-4b95-aa7c-c92ba15080bb",
+ "sourceHandle": "value",
+ "targetHandle": "value"
+ },
+ {
+ "id": "reactflow__edge-2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5vae-b76fe66f-7884-43ad-b72c-fadc81d7a73cvae",
+ "type": "default",
+ "source": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "target": "b76fe66f-7884-43ad-b72c-fadc81d7a73c",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5vae-338b883c-3728-4f18-b3a6-6e7190c2f850vae",
+ "type": "default",
+ "source": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "target": "338b883c-3728-4f18-b3a6-6e7190c2f850",
+ "sourceHandle": "vae",
+ "targetHandle": "vae"
+ },
+ {
+ "id": "reactflow__edge-2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5unet-1011539e-85de-4e02-a003-0b22358491b8unet",
+ "type": "default",
+ "source": "2ff466b8-5e2a-4d8f-923a-a3884c7ecbc5",
+ "target": "1011539e-85de-4e02-a003-0b22358491b8",
+ "sourceHandle": "unet",
+ "targetHandle": "unet"
+ }
+ ]
+}
diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py
new file mode 100644
index 00000000000..c07daa2662e
--- /dev/null
+++ b/invokeai/app/services/workflow_records/workflow_records_base.py
@@ -0,0 +1,103 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+
+from invokeai.app.services.shared.pagination import PaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.workflow_records.workflow_records_common import (
+ WORKFLOW_LIBRARY_DEFAULT_USER_ID,
+ Workflow,
+ WorkflowCategory,
+ WorkflowRecordDTO,
+ WorkflowRecordListItemDTO,
+ WorkflowRecordOrderBy,
+ WorkflowWithoutID,
+)
+
+
+class WorkflowRecordsStorageBase(ABC):
+ """Base class for workflow storage services."""
+
+ @abstractmethod
+ def get(self, workflow_id: str) -> WorkflowRecordDTO:
+ """Get workflow by id."""
+ pass
+
+ @abstractmethod
+ def create(
+ self,
+ workflow: WorkflowWithoutID,
+ user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID,
+ is_public: bool = False,
+ ) -> WorkflowRecordDTO:
+ """Creates a workflow."""
+ pass
+
+ @abstractmethod
+ def update(self, workflow: Workflow, user_id: Optional[str] = None) -> WorkflowRecordDTO:
+ """Updates a workflow. When user_id is provided, the UPDATE is scoped to that user."""
+ pass
+
+ @abstractmethod
+ def delete(self, workflow_id: str, user_id: Optional[str] = None) -> None:
+ """Deletes a workflow. When user_id is provided, the DELETE is scoped to that user."""
+ pass
+
+ @abstractmethod
+ def get_many(
+ self,
+ order_by: WorkflowRecordOrderBy,
+ direction: SQLiteDirection,
+ categories: Optional[list[WorkflowCategory]],
+ page: int,
+ per_page: Optional[int],
+ query: Optional[str],
+ tags: Optional[list[str]],
+ has_been_opened: Optional[bool],
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> PaginatedResults[WorkflowRecordListItemDTO]:
+ """Gets many workflows."""
+ pass
+
+ @abstractmethod
+ def counts_by_category(
+ self,
+ categories: list[WorkflowCategory],
+ has_been_opened: Optional[bool] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> dict[str, int]:
+ """Gets a dictionary of counts for each of the provided categories."""
+ pass
+
+ @abstractmethod
+ def counts_by_tag(
+ self,
+ tags: list[str],
+ categories: Optional[list[WorkflowCategory]] = None,
+ has_been_opened: Optional[bool] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> dict[str, int]:
+ """Gets a dictionary of counts for each of the provided tags."""
+ pass
+
+ @abstractmethod
+ def update_opened_at(self, workflow_id: str, user_id: Optional[str] = None) -> None:
+ """Open a workflow. When user_id is provided, the UPDATE is scoped to that user."""
+ pass
+
+ @abstractmethod
+ def get_all_tags(
+ self,
+ categories: Optional[list[WorkflowCategory]] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> list[str]:
+ """Gets all unique tags from workflows."""
+ pass
+
+ @abstractmethod
+ def update_is_public(self, workflow_id: str, is_public: bool, user_id: Optional[str] = None) -> WorkflowRecordDTO:
+ """Updates the is_public field of a workflow. When user_id is provided, the UPDATE is scoped to that user."""
+ pass
diff --git a/invokeai/app/services/workflow_records/workflow_records_common.py b/invokeai/app/services/workflow_records/workflow_records_common.py
new file mode 100644
index 00000000000..9c505530c90
--- /dev/null
+++ b/invokeai/app/services/workflow_records/workflow_records_common.py
@@ -0,0 +1,137 @@
+import datetime
+from enum import Enum
+from typing import Any, Optional, Union
+
+import semver
+from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, field_validator
+
+from invokeai.app.util.metaenum import MetaEnum
+
+__workflow_meta_version__ = semver.Version.parse("1.0.0")
+
+WORKFLOW_LIBRARY_DEFAULT_USER_ID = "system"
+"""Default user_id for workflows created in single-user mode or migrated from pre-multiuser databases."""
+
+
+class ExposedField(BaseModel):
+ nodeId: str
+ fieldName: str
+
+
+class WorkflowNotFoundError(Exception):
+ """Raised when a workflow is not found"""
+
+
+class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum):
+ """The order by options for workflow records"""
+
+ CreatedAt = "created_at"
+ UpdatedAt = "updated_at"
+ OpenedAt = "opened_at"
+ Name = "name"
+ IsPublic = "is_public"
+
+
+class WorkflowCategory(str, Enum, metaclass=MetaEnum):
+ User = "user"
+ Default = "default"
+
+
+class WorkflowMeta(BaseModel):
+ version: str = Field(description="The version of the workflow schema.")
+ category: WorkflowCategory = Field(description="The category of the workflow (user or default).")
+
+ @field_validator("version")
+ def validate_version(cls, version: str):
+ try:
+ semver.Version.parse(version)
+ return version
+ except Exception:
+ raise ValueError(f"Invalid workflow meta version: {version}")
+
+ def to_semver(self) -> semver.Version:
+ return semver.Version.parse(self.version)
+
+
+class WorkflowWithoutID(BaseModel):
+ name: str = Field(description="The name of the workflow.")
+ author: str = Field(description="The author of the workflow.")
+ description: str = Field(description="The description of the workflow.")
+ version: str = Field(description="The version of the workflow.")
+ contact: str = Field(description="The contact of the workflow.")
+ tags: str = Field(description="The tags of the workflow.")
+ notes: str = Field(description="The notes of the workflow.")
+ exposedFields: list[ExposedField] = Field(description="The exposed fields of the workflow.")
+ meta: WorkflowMeta = Field(description="The meta of the workflow.")
+ # TODO(psyche): nodes, edges and form are very loosely typed - they are strictly modeled and checked on the frontend.
+ nodes: list[dict[str, JsonValue]] = Field(description="The nodes of the workflow.")
+ edges: list[dict[str, JsonValue]] = Field(description="The edges of the workflow.")
+ # TODO(psyche): We have a crapload of workflows that have no form, bc it was added after we introduced workflows.
+ # This is typed as optional to prevent errors when pulling workflows from the DB. The frontend adds a default form if
+ # it is None.
+ form: dict[str, JsonValue] | None = Field(default=None, description="The form of the workflow.")
+
+ model_config = ConfigDict(extra="ignore")
+
+
+WorkflowWithoutIDValidator = TypeAdapter(WorkflowWithoutID)
+
+
+class UnsafeWorkflowWithVersion(BaseModel):
+ """
+ This utility model only requires a workflow to have a valid version string.
+ It is used to validate a workflow version without having to validate the entire workflow.
+ """
+
+ meta: WorkflowMeta = Field(description="The meta of the workflow.")
+
+
+UnsafeWorkflowWithVersionValidator = TypeAdapter(UnsafeWorkflowWithVersion)
+
+
+class Workflow(WorkflowWithoutID):
+ id: str = Field(description="The id of the workflow.")
+
+
+WorkflowValidator = TypeAdapter(Workflow)
+
+
+class WorkflowRecordDTOBase(BaseModel):
+ workflow_id: str = Field(description="The id of the workflow.")
+ name: str = Field(description="The name of the workflow.")
+ created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
+ updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
+ opened_at: Optional[Union[datetime.datetime, str]] = Field(
+ default=None, description="The opened timestamp of the workflow."
+ )
+ user_id: str = Field(description="The id of the user who owns this workflow.")
+ is_public: bool = Field(description="Whether this workflow is shared with all users.")
+
+
+class WorkflowRecordDTO(WorkflowRecordDTOBase):
+ workflow: Workflow = Field(description="The workflow.")
+
+ @classmethod
+ def from_dict(cls, data: dict[str, Any]) -> "WorkflowRecordDTO":
+ data["workflow"] = WorkflowValidator.validate_json(data.get("workflow", ""))
+ return WorkflowRecordDTOValidator.validate_python(data)
+
+
+WorkflowRecordDTOValidator = TypeAdapter(WorkflowRecordDTO)
+
+
+class WorkflowRecordListItemDTO(WorkflowRecordDTOBase):
+ description: str = Field(description="The description of the workflow.")
+ category: WorkflowCategory = Field(description="The description of the workflow.")
+ tags: str = Field(description="The tags of the workflow.")
+
+
+WorkflowRecordListItemDTOValidator = TypeAdapter(WorkflowRecordListItemDTO)
+
+
+class WorkflowRecordWithThumbnailDTO(WorkflowRecordDTO):
+ thumbnail_url: str | None = Field(default=None, description="The URL of the workflow thumbnail.")
+
+
+class WorkflowRecordListItemWithThumbnailDTO(WorkflowRecordListItemDTO):
+ thumbnail_url: str | None = Field(default=None, description="The URL of the workflow thumbnail.")
diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py
new file mode 100644
index 00000000000..a62dbb9dfa8
--- /dev/null
+++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py
@@ -0,0 +1,591 @@
+from pathlib import Path
+from typing import Optional
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.shared.pagination import PaginatedResults
+from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
+from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
+from invokeai.app.services.workflow_records.workflow_records_common import (
+ WORKFLOW_LIBRARY_DEFAULT_USER_ID,
+ Workflow,
+ WorkflowCategory,
+ WorkflowNotFoundError,
+ WorkflowRecordDTO,
+ WorkflowRecordListItemDTO,
+ WorkflowRecordListItemDTOValidator,
+ WorkflowRecordOrderBy,
+ WorkflowValidator,
+ WorkflowWithoutID,
+)
+from invokeai.app.util.misc import uuid_string
+
+SQL_TIME_FORMAT = "%Y-%m-%d %H:%M:%f"
+
+
+class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
+ def __init__(self, db: SqliteDatabase) -> None:
+ super().__init__()
+ self._db = db
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+ self._sync_default_workflows()
+
+ def get(self, workflow_id: str) -> WorkflowRecordDTO:
+ """Gets a workflow by ID. Updates the opened_at column."""
+ with self._db.transaction() as cursor:
+ cursor.execute(
+ """--sql
+ SELECT workflow_id, workflow, name, created_at, updated_at, opened_at, user_id, is_public
+ FROM workflow_library
+ WHERE workflow_id = ?;
+ """,
+ (workflow_id,),
+ )
+ row = cursor.fetchone()
+ if row is None:
+ raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found")
+ return WorkflowRecordDTO.from_dict(dict(row))
+
+ def create(
+ self,
+ workflow: WorkflowWithoutID,
+ user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID,
+ is_public: bool = False,
+ ) -> WorkflowRecordDTO:
+ if workflow.meta.category is WorkflowCategory.Default:
+ raise ValueError("Default workflows cannot be created via this method")
+
+ with self._db.transaction() as cursor:
+ workflow_with_id = Workflow(**workflow.model_dump(), id=uuid_string())
+ cursor.execute(
+ """--sql
+ INSERT OR IGNORE INTO workflow_library (
+ workflow_id,
+ workflow,
+ user_id,
+ is_public
+ )
+ VALUES (?, ?, ?, ?);
+ """,
+ (workflow_with_id.id, workflow_with_id.model_dump_json(), user_id, is_public),
+ )
+ return self.get(workflow_with_id.id)
+
+ def update(self, workflow: Workflow, user_id: Optional[str] = None) -> WorkflowRecordDTO:
+ if workflow.meta.category is WorkflowCategory.Default:
+ raise ValueError("Default workflows cannot be updated")
+
+ with self._db.transaction() as cursor:
+ if user_id is not None:
+ cursor.execute(
+ """--sql
+ UPDATE workflow_library
+ SET workflow = ?
+ WHERE workflow_id = ? AND category = 'user' AND user_id = ?;
+ """,
+ (workflow.model_dump_json(), workflow.id, user_id),
+ )
+ else:
+ cursor.execute(
+ """--sql
+ UPDATE workflow_library
+ SET workflow = ?
+ WHERE workflow_id = ? AND category = 'user';
+ """,
+ (workflow.model_dump_json(), workflow.id),
+ )
+ return self.get(workflow.id)
+
+ def delete(self, workflow_id: str, user_id: Optional[str] = None) -> None:
+ if self.get(workflow_id).workflow.meta.category is WorkflowCategory.Default:
+ raise ValueError("Default workflows cannot be deleted")
+
+ with self._db.transaction() as cursor:
+ if user_id is not None:
+ cursor.execute(
+ """--sql
+ DELETE from workflow_library
+ WHERE workflow_id = ? AND category = 'user' AND user_id = ?;
+ """,
+ (workflow_id, user_id),
+ )
+ else:
+ cursor.execute(
+ """--sql
+ DELETE from workflow_library
+ WHERE workflow_id = ? AND category = 'user';
+ """,
+ (workflow_id,),
+ )
+ return None
+
+ def update_is_public(self, workflow_id: str, is_public: bool, user_id: Optional[str] = None) -> WorkflowRecordDTO:
+ """Updates the is_public field of a workflow and manages the 'shared' tag automatically."""
+ record = self.get(workflow_id)
+ workflow = record.workflow
+
+ # Manage "shared" tag: add when public, remove when private
+ tags_list = [t.strip() for t in workflow.tags.split(",") if t.strip()] if workflow.tags else []
+ if is_public and "shared" not in tags_list:
+ tags_list.append("shared")
+ elif not is_public and "shared" in tags_list:
+ tags_list.remove("shared")
+ updated_tags = ", ".join(tags_list)
+ updated_workflow = workflow.model_copy(update={"tags": updated_tags})
+
+ with self._db.transaction() as cursor:
+ if user_id is not None:
+ cursor.execute(
+ """--sql
+ UPDATE workflow_library
+ SET workflow = ?, is_public = ?
+ WHERE workflow_id = ? AND category = 'user' AND user_id = ?;
+ """,
+ (updated_workflow.model_dump_json(), is_public, workflow_id, user_id),
+ )
+ else:
+ cursor.execute(
+ """--sql
+ UPDATE workflow_library
+ SET workflow = ?, is_public = ?
+ WHERE workflow_id = ? AND category = 'user';
+ """,
+ (updated_workflow.model_dump_json(), is_public, workflow_id),
+ )
+ return self.get(workflow_id)
+
+ def get_many(
+ self,
+ order_by: WorkflowRecordOrderBy,
+ direction: SQLiteDirection,
+ categories: Optional[list[WorkflowCategory]],
+ page: int = 0,
+ per_page: Optional[int] = None,
+ query: Optional[str] = None,
+ tags: Optional[list[str]] = None,
+ has_been_opened: Optional[bool] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> PaginatedResults[WorkflowRecordListItemDTO]:
+ with self._db.transaction() as cursor:
+ # sanitize!
+ assert order_by in WorkflowRecordOrderBy
+ assert direction in SQLiteDirection
+
+ # We will construct the query dynamically based on the query params
+
+ # The main query to get the workflows / counts
+ main_query = """
+ SELECT
+ workflow_id,
+ category,
+ name,
+ description,
+ created_at,
+ updated_at,
+ opened_at,
+ tags,
+ user_id,
+ is_public
+ FROM workflow_library
+ """
+ count_query = "SELECT COUNT(*) FROM workflow_library"
+
+ # Start with an empty list of conditions and params
+ conditions: list[str] = []
+ params: list[str | int] = []
+
+ if categories:
+ # Categories is a list of WorkflowCategory enum values, and a single string in the DB
+
+ # Ensure all categories are valid (is this necessary?)
+ assert all(c in WorkflowCategory for c in categories)
+
+ # Construct a placeholder string for the number of categories
+ placeholders = ", ".join("?" for _ in categories)
+
+ # Construct the condition string & params
+ category_condition = f"category IN ({placeholders})"
+ category_params = [category.value for category in categories]
+
+ conditions.append(category_condition)
+ params.extend(category_params)
+
+ if tags:
+ # Tags is a list of strings, and a single string in the DB
+ # The string in the DB has no guaranteed format
+
+ # Construct a list of conditions for each tag
+ tags_conditions = ["tags LIKE ?" for _ in tags]
+ tags_conditions_joined = " OR ".join(tags_conditions)
+ tags_condition = f"({tags_conditions_joined})"
+
+ # And the params for the tags, case-insensitive
+ tags_params = [f"%{t.strip()}%" for t in tags]
+
+ conditions.append(tags_condition)
+ params.extend(tags_params)
+
+ if has_been_opened:
+ conditions.append("opened_at IS NOT NULL")
+ elif has_been_opened is False:
+ conditions.append("opened_at IS NULL")
+
+ # Ignore whitespace in the query
+ stripped_query = query.strip() if query else None
+ if stripped_query:
+ # Construct a wildcard query for the name, description, and tags
+ wildcard_query = "%" + stripped_query + "%"
+ query_condition = "(name LIKE ? OR description LIKE ? OR tags LIKE ?)"
+
+ conditions.append(query_condition)
+ params.extend([wildcard_query, wildcard_query, wildcard_query])
+
+ if user_id is not None:
+ # Scope to the given user but always include default workflows
+ conditions.append("(user_id = ? OR category = 'default')")
+ params.append(user_id)
+
+ if is_public is True:
+ conditions.append("is_public = TRUE")
+ elif is_public is False:
+ conditions.append("is_public = FALSE")
+
+ if conditions:
+ # If there are conditions, add a WHERE clause and then join the conditions
+ main_query += " WHERE "
+ count_query += " WHERE "
+
+ all_conditions = " AND ".join(conditions)
+ main_query += all_conditions
+ count_query += all_conditions
+
+ # After this point, the query and params differ for the main query and the count query
+ main_params = params.copy()
+ count_params = params.copy()
+
+ # Main query also gets ORDER BY and LIMIT/OFFSET
+ main_query += f" ORDER BY {order_by.value} {direction.value}"
+
+ if per_page:
+ main_query += " LIMIT ? OFFSET ?"
+ main_params.extend([per_page, page * per_page])
+
+ # Put a ring on it
+ main_query += ";"
+ count_query += ";"
+
+ cursor.execute(main_query, main_params)
+ rows = cursor.fetchall()
+ workflows = [WorkflowRecordListItemDTOValidator.validate_python(dict(row)) for row in rows]
+
+ cursor.execute(count_query, count_params)
+ total = cursor.fetchone()[0]
+
+ if per_page:
+ pages = total // per_page + (total % per_page > 0)
+ else:
+ pages = 1 # If no pagination, there is only one page
+
+ return PaginatedResults(
+ items=workflows,
+ page=page,
+ per_page=per_page if per_page else total,
+ pages=pages,
+ total=total,
+ )
+
+ def counts_by_tag(
+ self,
+ tags: list[str],
+ categories: Optional[list[WorkflowCategory]] = None,
+ has_been_opened: Optional[bool] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> dict[str, int]:
+ if not tags:
+ return {}
+
+ with self._db.transaction() as cursor:
+ result: dict[str, int] = {}
+ # Base conditions for categories and selected tags
+ base_conditions: list[str] = []
+ base_params: list[str | int] = []
+
+ # Add category conditions
+ if categories:
+ assert all(c in WorkflowCategory for c in categories)
+ placeholders = ", ".join("?" for _ in categories)
+ base_conditions.append(f"category IN ({placeholders})")
+ base_params.extend([category.value for category in categories])
+
+ if has_been_opened:
+ base_conditions.append("opened_at IS NOT NULL")
+ elif has_been_opened is False:
+ base_conditions.append("opened_at IS NULL")
+
+ if user_id is not None:
+ # Scope to the given user but always include default workflows
+ base_conditions.append("(user_id = ? OR category = 'default')")
+ base_params.append(user_id)
+
+ if is_public is True:
+ base_conditions.append("is_public = TRUE")
+ elif is_public is False:
+ base_conditions.append("is_public = FALSE")
+
+ # For each tag to count, run a separate query
+ for tag in tags:
+ # Start with the base conditions
+ conditions = base_conditions.copy()
+ params = base_params.copy()
+
+ # Add this specific tag condition
+ conditions.append("tags LIKE ?")
+ params.append(f"%{tag.strip()}%")
+
+ # Construct the full query
+ stmt = """--sql
+ SELECT COUNT(*)
+ FROM workflow_library
+ """
+
+ if conditions:
+ stmt += " WHERE " + " AND ".join(conditions)
+
+ cursor.execute(stmt, params)
+ count = cursor.fetchone()[0]
+ result[tag] = count
+
+ return result
+
+ def counts_by_category(
+ self,
+ categories: list[WorkflowCategory],
+ has_been_opened: Optional[bool] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> dict[str, int]:
+ with self._db.transaction() as cursor:
+ result: dict[str, int] = {}
+ # Base conditions for categories
+ base_conditions: list[str] = []
+ base_params: list[str | int] = []
+
+ # Add category conditions
+ if categories:
+ assert all(c in WorkflowCategory for c in categories)
+ placeholders = ", ".join("?" for _ in categories)
+ base_conditions.append(f"category IN ({placeholders})")
+ base_params.extend([category.value for category in categories])
+
+ if has_been_opened:
+ base_conditions.append("opened_at IS NOT NULL")
+ elif has_been_opened is False:
+ base_conditions.append("opened_at IS NULL")
+
+ if user_id is not None:
+ # Scope to the given user but always include default workflows
+ base_conditions.append("(user_id = ? OR category = 'default')")
+ base_params.append(user_id)
+
+ if is_public is True:
+ base_conditions.append("is_public = TRUE")
+ elif is_public is False:
+ base_conditions.append("is_public = FALSE")
+
+ # For each category to count, run a separate query
+ for category in categories:
+ # Start with the base conditions
+ conditions = base_conditions.copy()
+ params = base_params.copy()
+
+ # Add this specific category condition
+ conditions.append("category = ?")
+ params.append(category.value)
+
+ # Construct the full query
+ stmt = """--sql
+ SELECT COUNT(*)
+ FROM workflow_library
+ """
+
+ if conditions:
+ stmt += " WHERE " + " AND ".join(conditions)
+
+ cursor.execute(stmt, params)
+ count = cursor.fetchone()[0]
+ result[category.value] = count
+
+ return result
+
+ def update_opened_at(self, workflow_id: str, user_id: Optional[str] = None) -> None:
+ with self._db.transaction() as cursor:
+ if user_id is not None:
+ cursor.execute(
+ f"""--sql
+ UPDATE workflow_library
+ SET opened_at = STRFTIME('{SQL_TIME_FORMAT}', 'NOW')
+ WHERE workflow_id = ? AND user_id = ?;
+ """,
+ (workflow_id, user_id),
+ )
+ else:
+ cursor.execute(
+ f"""--sql
+ UPDATE workflow_library
+ SET opened_at = STRFTIME('{SQL_TIME_FORMAT}', 'NOW')
+ WHERE workflow_id = ?;
+ """,
+ (workflow_id,),
+ )
+
+ def get_all_tags(
+ self,
+ categories: Optional[list[WorkflowCategory]] = None,
+ user_id: Optional[str] = None,
+ is_public: Optional[bool] = None,
+ ) -> list[str]:
+ with self._db.transaction() as cursor:
+ conditions: list[str] = []
+ params: list[str] = []
+
+ # Only get workflows that have tags
+ conditions.append("tags IS NOT NULL AND tags != ''")
+
+ if categories:
+ assert all(c in WorkflowCategory for c in categories)
+ placeholders = ", ".join("?" for _ in categories)
+ conditions.append(f"category IN ({placeholders})")
+ params.extend([category.value for category in categories])
+
+ if user_id is not None:
+ # Scope to the given user but always include default workflows
+ conditions.append("(user_id = ? OR category = 'default')")
+ params.append(user_id)
+
+ if is_public is True:
+ conditions.append("is_public = TRUE")
+ elif is_public is False:
+ conditions.append("is_public = FALSE")
+
+ stmt = """--sql
+ SELECT DISTINCT tags
+ FROM workflow_library
+ """
+
+ if conditions:
+ stmt += " WHERE " + " AND ".join(conditions)
+
+ cursor.execute(stmt, params)
+ rows = cursor.fetchall()
+
+ # Parse comma-separated tags and collect unique tags
+ all_tags: set[str] = set()
+
+ for row in rows:
+ tags_value = row[0]
+ if tags_value and isinstance(tags_value, str):
+ # Tags are stored as comma-separated string
+ for tag in tags_value.split(","):
+ tag_stripped = tag.strip()
+ if tag_stripped:
+ all_tags.add(tag_stripped)
+
+ return sorted(all_tags)
+
+ def _sync_default_workflows(self) -> None:
+ """Syncs default workflows to the database. Internal use only."""
+
+ """
+ An enhancement might be to only update workflows that have changed. This would require stable
+ default workflow IDs, and properly incrementing the workflow version.
+
+ It's much simpler to just replace them all with whichever workflows are in the directory.
+
+ The downside is that the `updated_at` and `opened_at` timestamps for default workflows are
+ meaningless, as they are overwritten every time the server starts.
+ """
+
+ with self._db.transaction() as cursor:
+ workflows_from_file: list[Workflow] = []
+ workflows_to_update: list[Workflow] = []
+ workflows_to_add: list[Workflow] = []
+ workflows_dir = Path(__file__).parent / Path("default_workflows")
+ workflow_paths = workflows_dir.glob("*.json")
+ for path in workflow_paths:
+ bytes_ = path.read_bytes()
+ workflow_from_file = WorkflowValidator.validate_json(bytes_)
+
+ assert workflow_from_file.id.startswith("default_"), (
+ f'Invalid default workflow ID (must start with "default_"): {workflow_from_file.id}'
+ )
+
+ assert workflow_from_file.meta.category is WorkflowCategory.Default, (
+ f"Invalid default workflow category: {workflow_from_file.meta.category}"
+ )
+
+ workflows_from_file.append(workflow_from_file)
+
+ try:
+ workflow_from_db = self.get(workflow_from_file.id).workflow
+ if workflow_from_file != workflow_from_db:
+ self._invoker.services.logger.debug(
+ f"Updating library workflow {workflow_from_file.name} ({workflow_from_file.id})"
+ )
+ workflows_to_update.append(workflow_from_file)
+ continue
+ except WorkflowNotFoundError:
+ self._invoker.services.logger.debug(
+ f"Adding missing default workflow {workflow_from_file.name} ({workflow_from_file.id})"
+ )
+ workflows_to_add.append(workflow_from_file)
+ continue
+
+ library_workflows_from_db = self.get_many(
+ order_by=WorkflowRecordOrderBy.Name,
+ direction=SQLiteDirection.Ascending,
+ categories=[WorkflowCategory.Default],
+ ).items
+
+ workflows_from_file_ids = [w.id for w in workflows_from_file]
+
+ for w in library_workflows_from_db:
+ if w.workflow_id not in workflows_from_file_ids:
+ self._invoker.services.logger.debug(
+ f"Deleting obsolete default workflow {w.name} ({w.workflow_id})"
+ )
+ # We cannot use the `delete` method here, as it only deletes non-default workflows
+ cursor.execute(
+ """--sql
+ DELETE from workflow_library
+ WHERE workflow_id = ?;
+ """,
+ (w.workflow_id,),
+ )
+
+ for w in workflows_to_add:
+ # We cannot use the `create` method here, as it only creates non-default workflows
+ cursor.execute(
+ """--sql
+ INSERT INTO workflow_library (
+ workflow_id,
+ workflow
+ )
+ VALUES (?, ?);
+ """,
+ (w.id, w.model_dump_json()),
+ )
+
+ for w in workflows_to_update:
+ # We cannot use the `update` method here, as it only updates non-default workflows
+ cursor.execute(
+ """--sql
+ UPDATE workflow_library
+ SET workflow = ?
+ WHERE workflow_id = ?;
+ """,
+ (w.model_dump_json(), w.id),
+ )
diff --git a/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_base.py b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_base.py
new file mode 100644
index 00000000000..f51d200dea1
--- /dev/null
+++ b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_base.py
@@ -0,0 +1,28 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+
+from PIL import Image
+
+
+class WorkflowThumbnailServiceBase(ABC):
+ """Base class for workflow thumbnail services"""
+
+ @abstractmethod
+ def get_path(self, workflow_id: str, with_hash: bool = True) -> Path:
+ """Gets the path to a workflow thumbnail"""
+ pass
+
+ @abstractmethod
+ def get_url(self, workflow_id: str, with_hash: bool = True) -> str | None:
+ """Gets the URL of a workflow thumbnail"""
+ pass
+
+ @abstractmethod
+ def save(self, workflow_id: str, image: Image.Image) -> None:
+ """Saves a workflow thumbnail"""
+ pass
+
+ @abstractmethod
+ def delete(self, workflow_id: str) -> None:
+ """Deletes a workflow thumbnail"""
+ pass
diff --git a/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_common.py b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_common.py
new file mode 100644
index 00000000000..8d124adec33
--- /dev/null
+++ b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_common.py
@@ -0,0 +1,22 @@
+class WorkflowThumbnailFileNotFoundException(Exception):
+ """Raised when a workflow thumbnail file is not found"""
+
+ def __init__(self, message: str = "Workflow thumbnail file not found"):
+ self.message = message
+ super().__init__(self.message)
+
+
+class WorkflowThumbnailFileSaveException(Exception):
+ """Raised when a workflow thumbnail file cannot be saved"""
+
+ def __init__(self, message: str = "Workflow thumbnail file cannot be saved"):
+ self.message = message
+ super().__init__(self.message)
+
+
+class WorkflowThumbnailFileDeleteException(Exception):
+ """Raised when a workflow thumbnail file cannot be deleted"""
+
+ def __init__(self, message: str = "Workflow thumbnail file cannot be deleted"):
+ self.message = message
+ super().__init__(self.message)
diff --git a/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_disk.py b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_disk.py
new file mode 100644
index 00000000000..3fbfa7607fe
--- /dev/null
+++ b/invokeai/app/services/workflow_thumbnails/workflow_thumbnails_disk.py
@@ -0,0 +1,87 @@
+from pathlib import Path
+
+from PIL import Image
+from PIL.Image import Image as PILImageType
+
+from invokeai.app.services.invoker import Invoker
+from invokeai.app.services.workflow_records.workflow_records_common import WorkflowCategory
+from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_base import WorkflowThumbnailServiceBase
+from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_common import (
+ WorkflowThumbnailFileDeleteException,
+ WorkflowThumbnailFileNotFoundException,
+ WorkflowThumbnailFileSaveException,
+)
+from invokeai.app.util.misc import uuid_string
+from invokeai.app.util.thumbnails import make_thumbnail
+
+
+class WorkflowThumbnailFileStorageDisk(WorkflowThumbnailServiceBase):
+ def __init__(self, thumbnails_path: Path):
+ self._workflow_thumbnail_folder = thumbnails_path
+ self._validate_storage_folders()
+
+ def start(self, invoker: Invoker) -> None:
+ self._invoker = invoker
+
+ def get(self, workflow_id: str) -> PILImageType:
+ try:
+ path = self.get_path(workflow_id)
+
+ return Image.open(path)
+ except FileNotFoundError as e:
+ raise WorkflowThumbnailFileNotFoundException from e
+
+ def save(self, workflow_id: str, image: PILImageType) -> None:
+ try:
+ self._validate_storage_folders()
+ image_path = self._workflow_thumbnail_folder / (workflow_id + ".webp")
+ thumbnail = make_thumbnail(image, 256)
+ thumbnail.save(image_path, format="webp")
+
+ except Exception as e:
+ raise WorkflowThumbnailFileSaveException from e
+
+ def get_path(self, workflow_id: str, with_hash: bool = True) -> Path:
+ workflow = self._invoker.services.workflow_records.get(workflow_id).workflow
+ if workflow.meta.category is WorkflowCategory.Default:
+ default_thumbnails_dir = Path(__file__).parent / Path("default_workflow_thumbnails")
+ path = default_thumbnails_dir / (workflow_id + ".png")
+ else:
+ path = self._workflow_thumbnail_folder / (workflow_id + ".webp")
+
+ return path
+
+ def get_url(self, workflow_id: str, with_hash: bool = True) -> str | None:
+ path = self.get_path(workflow_id)
+ if not self._validate_path(path):
+ return
+
+ url = self._invoker.services.urls.get_workflow_thumbnail_url(workflow_id)
+
+ # The image URL never changes, so we must add random query string to it to prevent caching
+ if with_hash:
+ url += f"?{uuid_string()}"
+
+ return url
+
+ def delete(self, workflow_id: str) -> None:
+ try:
+ path = self.get_path(workflow_id)
+
+ if not self._validate_path(path):
+ raise WorkflowThumbnailFileNotFoundException
+
+ path.unlink()
+
+ except WorkflowThumbnailFileNotFoundException as e:
+ raise WorkflowThumbnailFileNotFoundException from e
+ except Exception as e:
+ raise WorkflowThumbnailFileDeleteException from e
+
+ def _validate_path(self, path: Path) -> bool:
+ """Validates the path given for an image."""
+ return path.exists()
+
+ def _validate_storage_folders(self) -> None:
+ """Checks if the required folders exist and create them if they don't"""
+ self._workflow_thumbnail_folder.mkdir(parents=True, exist_ok=True)
diff --git a/invokeai/app/shared/__init__.py b/invokeai/app/shared/__init__.py
new file mode 100644
index 00000000000..3f50fd9fbc2
--- /dev/null
+++ b/invokeai/app/shared/__init__.py
@@ -0,0 +1,5 @@
+"""
+This module contains various classes, functions and models which are shared across the app, particularly by invocations.
+
+Lifting these classes, functions and models into this shared module helps to reduce circular imports.
+"""
diff --git a/invokeai/app/shared/models.py b/invokeai/app/shared/models.py
new file mode 100644
index 00000000000..1a11b480cc5
--- /dev/null
+++ b/invokeai/app/shared/models.py
@@ -0,0 +1,16 @@
+from pydantic import BaseModel, Field
+
+from invokeai.app.invocations.fields import FieldDescriptions
+
+
+class FreeUConfig(BaseModel):
+ """
+ Configuration for the FreeU hyperparameters.
+ - https://huggingface.co/docs/diffusers/main/en/using-diffusers/freeu
+ - https://github.com/ChenyangSi/FreeU
+ """
+
+ s1: float = Field(ge=-1, le=3, description=FieldDescriptions.freeu_s1)
+ s2: float = Field(ge=-1, le=3, description=FieldDescriptions.freeu_s2)
+ b1: float = Field(ge=-1, le=3, description=FieldDescriptions.freeu_b1)
+ b2: float = Field(ge=-1, le=3, description=FieldDescriptions.freeu_b2)
diff --git a/invokeai/app/util/__init__.py b/invokeai/app/util/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/invokeai/app/util/controlnet_utils.py b/invokeai/app/util/controlnet_utils.py
new file mode 100644
index 00000000000..0f14ed7bfb3
--- /dev/null
+++ b/invokeai/app/util/controlnet_utils.py
@@ -0,0 +1,433 @@
+from typing import Any, Literal, Union
+
+import cv2
+import numpy as np
+import torch
+from einops import rearrange
+from PIL import Image
+
+from invokeai.backend.image_util.util import nms, normalize_image_channel_count
+
+CONTROLNET_RESIZE_VALUES = Literal[
+ "just_resize",
+ "crop_resize",
+ "fill_resize",
+ "just_resize_simple",
+]
+CONTROLNET_MODE_VALUES = Literal["balanced", "more_prompt", "more_control", "unbalanced"]
+
+###################################################################
+# Copy of scripts/lvminthin.py from Mikubill/sd-webui-controlnet
+###################################################################
+# High Quality Edge Thinning using Pure Python
+# Written by Lvmin Zhangu
+# 2023 April
+# Stanford University
+# If you use this, please Cite "High Quality Edge Thinning using Pure Python", Lvmin Zhang, In Mikubill/sd-webui-controlnet.
+
+lvmin_kernels_raw = [
+ np.array([[-1, -1, -1], [0, 1, 0], [1, 1, 1]], dtype=np.int32),
+ np.array([[0, -1, -1], [1, 1, -1], [0, 1, 0]], dtype=np.int32),
+]
+
+lvmin_kernels = []
+lvmin_kernels += [np.rot90(x, k=0, axes=(0, 1)) for x in lvmin_kernels_raw]
+lvmin_kernels += [np.rot90(x, k=1, axes=(0, 1)) for x in lvmin_kernels_raw]
+lvmin_kernels += [np.rot90(x, k=2, axes=(0, 1)) for x in lvmin_kernels_raw]
+lvmin_kernels += [np.rot90(x, k=3, axes=(0, 1)) for x in lvmin_kernels_raw]
+
+lvmin_prunings_raw = [
+ np.array([[-1, -1, -1], [-1, 1, -1], [0, 0, -1]], dtype=np.int32),
+ np.array([[-1, -1, -1], [-1, 1, -1], [-1, 0, 0]], dtype=np.int32),
+]
+
+lvmin_prunings = []
+lvmin_prunings += [np.rot90(x, k=0, axes=(0, 1)) for x in lvmin_prunings_raw]
+lvmin_prunings += [np.rot90(x, k=1, axes=(0, 1)) for x in lvmin_prunings_raw]
+lvmin_prunings += [np.rot90(x, k=2, axes=(0, 1)) for x in lvmin_prunings_raw]
+lvmin_prunings += [np.rot90(x, k=3, axes=(0, 1)) for x in lvmin_prunings_raw]
+
+
+def remove_pattern(x, kernel):
+ objects = cv2.morphologyEx(x, cv2.MORPH_HITMISS, kernel)
+ objects = np.where(objects > 127)
+ x[objects] = 0
+ return x, objects[0].shape[0] > 0
+
+
+def thin_one_time(x, kernels):
+ y = x
+ is_done = True
+ for k in kernels:
+ y, has_update = remove_pattern(y, k)
+ if has_update:
+ is_done = False
+ return y, is_done
+
+
+def lvmin_thin(x, prunings=True):
+ y = x
+ for _i in range(32):
+ y, is_done = thin_one_time(y, lvmin_kernels)
+ if is_done:
+ break
+ if prunings:
+ y, _ = thin_one_time(y, lvmin_prunings)
+ return y
+
+
+################################################################################
+# copied from Mikubill/sd-webui-controlnet external_code.py and modified for InvokeAI
+################################################################################
+# FIXME: not using yet, if used in the future will most likely require modification of preprocessors
+def pixel_perfect_resolution(
+ image: np.ndarray,
+ target_H: int,
+ target_W: int,
+ resize_mode: str,
+) -> int:
+ """
+ Calculate the estimated resolution for resizing an image while preserving aspect ratio.
+
+ The function first calculates scaling factors for height and width of the image based on the target
+ height and width. Then, based on the chosen resize mode, it either takes the smaller or the larger
+ scaling factor to estimate the new resolution.
+
+ If the resize mode is OUTER_FIT, the function uses the smaller scaling factor, ensuring the whole image
+ fits within the target dimensions, potentially leaving some empty space.
+
+ If the resize mode is not OUTER_FIT, the function uses the larger scaling factor, ensuring the target
+ dimensions are fully filled, potentially cropping the image.
+
+ After calculating the estimated resolution, the function prints some debugging information.
+
+ Args:
+ image (np.ndarray): A 3D numpy array representing an image. The dimensions represent [height, width, channels].
+ target_H (int): The target height for the image.
+ target_W (int): The target width for the image.
+ resize_mode (ResizeMode): The mode for resizing.
+
+ Returns:
+ int: The estimated resolution after resizing.
+ """
+ raw_H, raw_W, _ = image.shape
+
+ k0 = float(target_H) / float(raw_H)
+ k1 = float(target_W) / float(raw_W)
+
+ if resize_mode == "fill_resize":
+ estimation = min(k0, k1) * float(min(raw_H, raw_W))
+ else: # "crop_resize" or "just_resize" (or possibly "just_resize_simple"?)
+ estimation = max(k0, k1) * float(min(raw_H, raw_W))
+
+ # print(f"Pixel Perfect Computation:")
+ # print(f"resize_mode = {resize_mode}")
+ # print(f"raw_H = {raw_H}")
+ # print(f"raw_W = {raw_W}")
+ # print(f"target_H = {target_H}")
+ # print(f"target_W = {target_W}")
+ # print(f"estimation = {estimation}")
+
+ return int(np.round(estimation))
+
+
+def clone_contiguous(x: np.ndarray[Any, Any]) -> np.ndarray[Any, Any]:
+ """Get a memory-contiguous clone of the given numpy array, as a safety measure and to improve computation efficiency."""
+ return np.ascontiguousarray(x).copy()
+
+
+def np_img_to_torch(np_img: np.ndarray[Any, Any], device: torch.device) -> torch.Tensor:
+ """Convert a numpy image to a PyTorch tensor. The image is normalized to 0-1, rearranged to BCHW format and sent to
+ the specified device."""
+
+ torch_img = torch.from_numpy(np_img)
+ normalized = torch_img.float() / 255.0
+ bchw = rearrange(normalized, "h w c -> 1 c h w")
+ on_device = bchw.to(device)
+ return on_device.clone()
+
+
+def heuristic_resize(np_img: np.ndarray[Any, Any], size: tuple[int, int]) -> np.ndarray[Any, Any]:
+ """Resizes an image using a heuristic to choose the best resizing strategy.
+
+ - If the image appears to be an edge map, special handling will be applied to ensure the edges are not distorted.
+ - Single-pixel edge maps use NMS and thinning to keep the edges as single-pixel lines.
+ - Low-color-count images are resized with nearest-neighbor to preserve color information (for e.g. segmentation maps).
+ - The alpha channel is handled separately to ensure it is resized correctly.
+
+ Args:
+ np_img (np.ndarray): The input image.
+ size (tuple[int, int]): The target size for the image.
+
+ Returns:
+ np.ndarray: The resized image.
+
+ Adapted from https://github.com/Mikubill/sd-webui-controlnet.
+ """
+
+ # Return early if the image is already at the requested size
+ if np_img.shape[0] == size[1] and np_img.shape[1] == size[0]:
+ return np_img
+
+ # If the image has an alpha channel, separate it for special handling later.
+ inpaint_mask = None
+ if np_img.ndim == 3 and np_img.shape[2] == 4:
+ inpaint_mask = np_img[:, :, 3]
+ np_img = np_img[:, :, 0:3]
+
+ new_size_is_smaller = (size[0] * size[1]) < (np_img.shape[0] * np_img.shape[1])
+ new_size_is_bigger = (size[0] * size[1]) > (np_img.shape[0] * np_img.shape[1])
+ unique_color_count = np.unique(np_img.reshape(-1, np_img.shape[2]), axis=0).shape[0]
+ is_one_pixel_edge = False
+ is_binary = False
+
+ if unique_color_count == 2:
+ # If the image has only two colors, it is likely binary. Check if the image has one-pixel edges.
+ is_binary = np.min(np_img) < 16 and np.max(np_img) > 240
+ if is_binary:
+ eroded = cv2.erode(np_img, np.ones(shape=(3, 3), dtype=np.uint8), iterations=1)
+ dilated = cv2.dilate(eroded, np.ones(shape=(3, 3), dtype=np.uint8), iterations=1)
+ one_pixel_edge_count = np.where(dilated < np_img)[0].shape[0]
+ all_edge_count = np.where(np_img > 127)[0].shape[0]
+ is_one_pixel_edge = one_pixel_edge_count * 2 > all_edge_count
+
+ if 2 < unique_color_count < 200:
+ # With a low color count, we assume this is a map where exact colors are important. Near-neighbor preserves
+ # the colors as needed.
+ interpolation = cv2.INTER_NEAREST
+ elif new_size_is_smaller:
+ # This works best for downscaling
+ interpolation = cv2.INTER_AREA
+ else:
+ # Fall back for other cases
+ interpolation = cv2.INTER_CUBIC # Must be CUBIC because we now use nms. NEVER CHANGE THIS
+
+ # This may be further transformed depending on the binary nature of the image.
+ resized = cv2.resize(np_img, size, interpolation=interpolation)
+
+ if inpaint_mask is not None:
+ # Resize the inpaint mask to match the resized image using the same interpolation method.
+ inpaint_mask = cv2.resize(inpaint_mask, size, interpolation=interpolation)
+
+ # If the image is binary, we will perform some additional processing to ensure the edges are preserved.
+ if is_binary:
+ resized = np.mean(resized.astype(np.float32), axis=2).clip(0, 255).astype(np.uint8)
+ if is_one_pixel_edge:
+ # Use NMS and thinning to keep the edges as single-pixel lines.
+ resized = nms(resized)
+ _, resized = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+ resized = lvmin_thin(resized, prunings=new_size_is_bigger)
+ else:
+ _, resized = cv2.threshold(resized, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+ resized = np.stack([resized] * 3, axis=2)
+
+ # Restore the alpha channel if it was present.
+ if inpaint_mask is not None:
+ inpaint_mask = (inpaint_mask > 127).astype(np.float32) * 255.0
+ inpaint_mask = inpaint_mask[:, :, None].clip(0, 255).astype(np.uint8)
+ resized = np.concatenate([resized, inpaint_mask], axis=2)
+
+ return resized
+
+
+# precompute common kernels
+_KERNEL3 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
+# directional masks for NMS
+_DIRS = [
+ np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], np.uint8),
+ np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], np.uint8),
+ np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], np.uint8),
+ np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], np.uint8),
+]
+
+
+def heuristic_resize_fast(np_img: np.ndarray, size: tuple[int, int]) -> np.ndarray:
+ h, w = np_img.shape[:2]
+ # early exit
+ if (w, h) == size:
+ return np_img
+
+ # separate alpha channel
+ img = np_img
+ alpha = None
+ if img.ndim == 3 and img.shape[2] == 4:
+ alpha, img = img[:, :, 3], img[:, :, :3]
+
+ # build small sample for unique‐color & binary detection
+ flat = img.reshape(-1, img.shape[-1])
+ N = flat.shape[0]
+ # include four corners to avoid missing extreme values
+ corners = np.vstack([img[0, 0], img[0, w - 1], img[h - 1, 0], img[h - 1, w - 1]])
+ cnt = min(N, 100_000)
+ samp = np.vstack([corners, flat[np.random.choice(N, cnt, replace=False)]])
+ uc = np.unique(samp, axis=0).shape[0]
+ vmin, vmax = samp.min(), samp.max()
+
+ # detect binary edge map & one‐pixel‐edge case
+ is_binary = uc == 2 and vmin < 16 and vmax > 240
+ one_pixel_edge = False
+ if is_binary:
+ # single gray conversion
+ gray0 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
+ grad = cv2.morphologyEx(gray0, cv2.MORPH_GRADIENT, _KERNEL3)
+ cnt_edge = cv2.countNonZero(grad)
+ cnt_all = cv2.countNonZero((gray0 > 127).astype(np.uint8))
+ one_pixel_edge = (2 * cnt_edge) > cnt_all
+
+ # choose interp for color/seg/grayscale
+ area_new, area_old = size[0] * size[1], w * h
+ if 2 < uc < 200: # segmentation map
+ interp = cv2.INTER_NEAREST
+ elif area_new < area_old:
+ interp = cv2.INTER_AREA
+ else:
+ interp = cv2.INTER_CUBIC
+
+ # single resize pass on RGB
+ resized = cv2.resize(img, size, interpolation=interp)
+
+ if is_binary:
+ # convert to gray & apply NMS via C++ dilate
+ gray_r = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
+ nms = np.zeros_like(gray_r)
+ for K in _DIRS:
+ d = cv2.dilate(gray_r, K)
+ mask = d == gray_r
+ nms[mask] = gray_r[mask]
+
+ # threshold + thinning if needed
+ _, bw = cv2.threshold(nms, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
+ out_bin = cv2.ximgproc.thinning(bw) if one_pixel_edge else bw
+ # restore 3 channels
+ resized = np.stack([out_bin] * 3, axis=2)
+
+ # restore alpha with same interp as RGB for consistency
+ if alpha is not None:
+ am = cv2.resize(alpha, size, interpolation=interp)
+ am = (am > 127).astype(np.uint8) * 255
+ resized = np.dstack((resized, am))
+
+ return resized
+
+
+###########################################################################
+# Copied from detectmap_proc method in scripts/detectmap_proc.py in Mikubill/sd-webui-controlnet
+# modified for InvokeAI
+###########################################################################
+def np_img_resize(
+ np_img: np.ndarray,
+ resize_mode: CONTROLNET_RESIZE_VALUES,
+ h: int,
+ w: int,
+ device: torch.device = torch.device("cpu"),
+) -> tuple[torch.Tensor, np.ndarray[Any, Any]]:
+ np_img = normalize_image_channel_count(np_img)
+
+ if resize_mode == "just_resize": # RESIZE
+ np_img = heuristic_resize_fast(np_img, (w, h))
+ np_img = clone_contiguous(np_img)
+ return np_img_to_torch(np_img, device), np_img
+
+ old_h, old_w, _ = np_img.shape
+ old_w = float(old_w)
+ old_h = float(old_h)
+ k0 = float(h) / old_h
+ k1 = float(w) / old_w
+
+ def safeint(x: Union[int, float]) -> int:
+ return int(np.round(x))
+
+ if resize_mode == "fill_resize": # OUTER_FIT
+ k = min(k0, k1)
+ borders = np.concatenate([np_img[0, :, :], np_img[-1, :, :], np_img[:, 0, :], np_img[:, -1, :]], axis=0)
+ high_quality_border_color = np.median(borders, axis=0).astype(np_img.dtype)
+ if len(high_quality_border_color) == 4:
+ # Inpaint hijack
+ high_quality_border_color[3] = 255
+ high_quality_background = np.tile(high_quality_border_color[None, None], [h, w, 1])
+ np_img = heuristic_resize_fast(np_img, (safeint(old_w * k), safeint(old_h * k)))
+ new_h, new_w, _ = np_img.shape
+ pad_h = max(0, (h - new_h) // 2)
+ pad_w = max(0, (w - new_w) // 2)
+ high_quality_background[pad_h : pad_h + new_h, pad_w : pad_w + new_w] = np_img
+ np_img = high_quality_background
+ np_img = clone_contiguous(np_img)
+ return np_img_to_torch(np_img, device), np_img
+ else: # resize_mode == "crop_resize" (INNER_FIT)
+ k = max(k0, k1)
+ np_img = heuristic_resize_fast(np_img, (safeint(old_w * k), safeint(old_h * k)))
+ new_h, new_w, _ = np_img.shape
+ pad_h = max(0, (new_h - h) // 2)
+ pad_w = max(0, (new_w - w) // 2)
+ np_img = np_img[pad_h : pad_h + h, pad_w : pad_w + w]
+ np_img = clone_contiguous(np_img)
+ return np_img_to_torch(np_img, device), np_img
+
+
+def prepare_control_image(
+ image: Image.Image,
+ width: int,
+ height: int,
+ num_channels: int = 3,
+ device: str | torch.device = "cuda",
+ dtype: torch.dtype = torch.float16,
+ control_mode: CONTROLNET_MODE_VALUES = "balanced",
+ resize_mode: CONTROLNET_RESIZE_VALUES = "just_resize_simple",
+ do_classifier_free_guidance: bool = True,
+) -> torch.Tensor:
+ """Pre-process images for ControlNets or T2I-Adapters.
+
+ Args:
+ image (Image): The PIL image to pre-process.
+ width (int): The target width in pixels.
+ height (int): The target height in pixels.
+ num_channels (int, optional): The target number of image channels. This is achieved by converting the input
+ image to RGB, then naively taking the first `num_channels` channels. The primary use case is converting a
+ RGB image to a single-channel grayscale image. Raises if `num_channels` cannot be achieved. Defaults to 3.
+ device (str | torch.Device, optional): The target device for the output image. Defaults to "cuda".
+ dtype (_type_, optional): The dtype for the output image. Defaults to torch.float16.
+ do_classifier_free_guidance (bool, optional): If True, repeat the output image along the batch dimension.
+ Defaults to True.
+ control_mode (str, optional): Defaults to "balanced".
+ resize_mode (str, optional): Defaults to "just_resize_simple".
+
+ Raises:
+ ValueError: If `resize_mode` is not recognized.
+ ValueError: If `num_channels` is out of range.
+
+ Returns:
+ torch.Tensor: The pre-processed input tensor.
+ """
+ if resize_mode == "just_resize_simple":
+ image = image.convert("RGB")
+ image = image.resize((width, height), resample=Image.LANCZOS)
+ nimage = np.array(image)
+ nimage = nimage[None, :]
+ nimage = np.concatenate([nimage], axis=0)
+ # normalizing RGB values to [0,1] range (in PIL.Image they are [0-255])
+ nimage = np.array(nimage).astype(np.float32) / 255.0
+ nimage = nimage.transpose(0, 3, 1, 2)
+ timage = torch.from_numpy(nimage)
+
+ # use fancy lvmin controlnet resizing
+ elif resize_mode == "just_resize" or resize_mode == "crop_resize" or resize_mode == "fill_resize":
+ nimage = np.array(image)
+ timage, nimage = np_img_resize(
+ np_img=nimage,
+ resize_mode=resize_mode,
+ h=height,
+ w=width,
+ device=torch.device(device),
+ )
+ else:
+ raise ValueError(f"Unsupported resize_mode: '{resize_mode}'.")
+
+ if timage.shape[1] < num_channels or num_channels <= 0:
+ raise ValueError(f"Cannot achieve the target of num_channels={num_channels}.")
+ timage = timage[:, :num_channels, :, :]
+
+ timage = timage.to(device=device, dtype=dtype)
+ cfg_injection = control_mode == "more_control" or control_mode == "unbalanced"
+ if do_classifier_free_guidance and not cfg_injection:
+ timage = torch.cat([timage] * 2)
+ return timage
diff --git a/invokeai/app/util/custom_openapi.py b/invokeai/app/util/custom_openapi.py
new file mode 100644
index 00000000000..f674fa76218
--- /dev/null
+++ b/invokeai/app/util/custom_openapi.py
@@ -0,0 +1,153 @@
+from typing import Any, Callable, Optional
+
+from fastapi import FastAPI
+from fastapi.openapi.utils import get_openapi
+from pydantic.json_schema import models_json_schema
+
+from invokeai.app.invocations.baseinvocation import (
+ InvocationRegistry,
+ UIConfigBase,
+)
+from invokeai.app.invocations.fields import InputFieldJSONSchemaExtra, OutputFieldJSONSchemaExtra
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.app.services.events.events_common import EventBase
+from invokeai.app.services.session_processor.session_processor_common import ProgressImage
+from invokeai.backend.model_manager.configs.factory import AnyModelConfigValidator
+from invokeai.backend.util.logging import InvokeAILogger
+
+logger = InvokeAILogger.get_logger()
+
+
+def move_defs_to_top_level(openapi_schema: dict[str, Any], component_schema: dict[str, Any]) -> None:
+ """Moves a component schema's $defs to the top level of the openapi schema. Useful when generating a schema
+ for a single model that needs to be added back to the top level of the schema. Mutates openapi_schema and
+ component_schema."""
+
+ defs = component_schema.pop("$defs", {})
+ for schema_key, json_schema in defs.items():
+ if schema_key in openapi_schema["components"]["schemas"]:
+ continue
+ openapi_schema["components"]["schemas"][schema_key] = json_schema
+
+
+def normalize_path_defaults(node: Any) -> None:
+ """Recursively normalize `default` strings on schema nodes whose `format` is `path` to use forward slashes.
+
+ Pydantic stringifies `Path` defaults using the host OS's separator, so a default declared as
+ `Path("models/.convert_cache")` serializes to `models\\.convert_cache` on Windows. That OS-dependent drift
+ pollutes diffs whenever schema is regenerated on Windows. We force POSIX form for path-typed defaults.
+ """
+ if isinstance(node, dict):
+ if node.get("format") == "path" and isinstance(node.get("default"), str):
+ node["default"] = node["default"].replace("\\", "/")
+ for v in node.values():
+ normalize_path_defaults(v)
+ elif isinstance(node, list):
+ for v in node:
+ normalize_path_defaults(v)
+
+
+def get_openapi_func(
+ app: FastAPI, post_transform: Optional[Callable[[dict[str, Any]], dict[str, Any]]] = None
+) -> Callable[[], dict[str, Any]]:
+ """Gets the OpenAPI schema generator function.
+
+ Args:
+ app (FastAPI): The FastAPI app to generate the schema for.
+ post_transform (Optional[Callable[[dict[str, Any]], dict[str, Any]]], optional): A function to apply to the
+ generated schema before returning it. Defaults to None.
+
+ Returns:
+ Callable[[], dict[str, Any]]: The OpenAPI schema generator function. When first called, the generated schema is
+ cached in `app.openapi_schema`. On subsequent calls, the cached schema is returned. This caching behaviour
+ matches FastAPI's default schema generation caching.
+ """
+
+ def openapi() -> dict[str, Any]:
+ if app.openapi_schema:
+ return app.openapi_schema
+
+ openapi_schema = get_openapi(
+ title=app.title,
+ description="An API for invoking AI image operations",
+ version="1.0.0",
+ routes=app.routes,
+ separate_input_output_schemas=False, # https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/
+ )
+
+ # We'll create a map of invocation type to output schema to make some types simpler on the client.
+ invocation_output_map_properties: dict[str, Any] = {}
+ invocation_output_map_required: list[str] = []
+
+ # We need to manually add all outputs to the schema - pydantic doesn't add them because they aren't used directly.
+ for output in InvocationRegistry.get_output_classes():
+ json_schema = output.model_json_schema(mode="serialization", ref_template="#/components/schemas/{model}")
+ # Remove output_metadata that is only used on back-end from the schema
+ if "output_meta" in json_schema["properties"]:
+ json_schema["properties"].pop("output_meta")
+
+ move_defs_to_top_level(openapi_schema, json_schema)
+ openapi_schema["components"]["schemas"][output.__name__] = json_schema
+
+ # Technically, invocations are added to the schema by pydantic, but we still need to manually set their output
+ # property, so we'll just do it all manually.
+ for invocation in InvocationRegistry.get_invocation_classes():
+ json_schema = invocation.model_json_schema(
+ mode="serialization", ref_template="#/components/schemas/{model}"
+ )
+ move_defs_to_top_level(openapi_schema, json_schema)
+ output_title = invocation.get_output_annotation().__name__
+ outputs_ref = {"$ref": f"#/components/schemas/{output_title}"}
+ json_schema["output"] = outputs_ref
+ openapi_schema["components"]["schemas"][invocation.__name__] = json_schema
+
+ # Add this invocation and its output to the output map
+ invocation_type = invocation.get_type()
+ invocation_output_map_properties[invocation_type] = json_schema["output"]
+ invocation_output_map_required.append(invocation_type)
+
+ # Add the output map to the schema
+ openapi_schema["components"]["schemas"]["InvocationOutputMap"] = {
+ "type": "object",
+ "properties": dict(sorted(invocation_output_map_properties.items())),
+ "required": sorted(invocation_output_map_required),
+ }
+
+ # Some models don't end up in the schemas as standalone definitions because they aren't used directly in the API.
+ # We need to add them manually here. WARNING: Pydantic can choke if you call `model.model_json_schema()` to get
+ # a schema. This has something to do with schema refs - not totally clear. For whatever reason, using
+ # `models_json_schema` seems to work fine.
+ additional_models = [
+ *EventBase.get_events(),
+ UIConfigBase,
+ InputFieldJSONSchemaExtra,
+ OutputFieldJSONSchemaExtra,
+ ModelIdentifierField,
+ ProgressImage,
+ ]
+
+ additional_schemas = models_json_schema(
+ [(m, "serialization") for m in additional_models],
+ ref_template="#/components/schemas/{model}",
+ )
+ # additional_schemas[1] is a dict of $defs that we need to add to the top level of the schema
+ move_defs_to_top_level(openapi_schema, additional_schemas[1])
+
+ any_model_config_schema = AnyModelConfigValidator.json_schema(
+ mode="serialization",
+ ref_template="#/components/schemas/{model}",
+ )
+ move_defs_to_top_level(openapi_schema, any_model_config_schema)
+ openapi_schema["components"]["schemas"]["AnyModelConfig"] = any_model_config_schema
+
+ if post_transform is not None:
+ openapi_schema = post_transform(openapi_schema)
+
+ normalize_path_defaults(openapi_schema)
+
+ openapi_schema["components"]["schemas"] = dict(sorted(openapi_schema["components"]["schemas"].items()))
+
+ app.openapi_schema = openapi_schema
+ return app.openapi_schema
+
+ return openapi
diff --git a/invokeai/app/util/metaenum.py b/invokeai/app/util/metaenum.py
new file mode 100644
index 00000000000..462238f775e
--- /dev/null
+++ b/invokeai/app/util/metaenum.py
@@ -0,0 +1,15 @@
+from enum import EnumMeta
+
+
+class MetaEnum(EnumMeta):
+ """Metaclass to support additional features in Enums.
+
+ - `in` operator support: `'value' in MyEnum -> bool`
+ """
+
+ def __contains__(cls, item):
+ try:
+ cls(item)
+ except ValueError:
+ return False
+ return True
diff --git a/invokeai/app/util/misc.py b/invokeai/app/util/misc.py
new file mode 100644
index 00000000000..f75683539ac
--- /dev/null
+++ b/invokeai/app/util/misc.py
@@ -0,0 +1,35 @@
+import datetime
+import typing
+import uuid
+
+import numpy as np
+
+
+def get_timestamp() -> int:
+ return int(datetime.datetime.now(datetime.timezone.utc).timestamp())
+
+
+def get_iso_timestamp() -> str:
+ return datetime.datetime.now(datetime.timezone.utc).isoformat()
+
+
+def get_datetime_from_iso_timestamp(iso_timestamp: str) -> datetime.datetime:
+ return datetime.datetime.fromisoformat(iso_timestamp)
+
+
+SEED_MAX = np.iinfo(np.uint32).max
+
+
+def get_random_seed() -> int:
+ rng = np.random.default_rng(seed=None)
+ return int(rng.integers(0, SEED_MAX))
+
+
+def uuid_string() -> str:
+ res = uuid.uuid4()
+ return str(res)
+
+
+def is_optional(value: typing.Any) -> bool:
+ """Checks if a value is typed as Optional. Note that Optional is sugar for Union[x, None]."""
+ return typing.get_origin(value) is typing.Union and type(None) in typing.get_args(value)
diff --git a/invokeai/app/util/model_exclude_null.py b/invokeai/app/util/model_exclude_null.py
new file mode 100644
index 00000000000..6da41039b45
--- /dev/null
+++ b/invokeai/app/util/model_exclude_null.py
@@ -0,0 +1,23 @@
+from typing import Any
+
+from pydantic import BaseModel
+
+"""
+We want to exclude null values from objects that make their way to the client.
+
+Unfortunately there is no built-in way to do this in pydantic, so we need to override the default
+dict method to do this.
+
+From https://github.com/tiangolo/fastapi/discussions/8882#discussioncomment-5154541
+"""
+
+
+class BaseModelExcludeNull(BaseModel):
+ def model_dump(self, *args, **kwargs) -> dict[str, Any]:
+ """
+ Override the default dict method to exclude None values in the response
+ """
+ kwargs.pop("exclude_none", None)
+ return super().model_dump(*args, exclude_none=True, **kwargs)
+
+ pass
diff --git a/invokeai/app/util/profiler.py b/invokeai/app/util/profiler.py
new file mode 100644
index 00000000000..d1ce126b049
--- /dev/null
+++ b/invokeai/app/util/profiler.py
@@ -0,0 +1,67 @@
+import cProfile
+from logging import Logger
+from pathlib import Path
+from typing import Optional
+
+
+class Profiler:
+ """
+ Simple wrapper around cProfile.
+
+ Usage
+ ```
+ # Create a profiler
+ profiler = Profiler(logger, output_dir, "sql_query_perf")
+ # Start a new profile
+ profiler.start("my_profile")
+ # Do stuff
+ profiler.stop()
+ ```
+
+ Visualize a profile as a flamegraph with [snakeviz](https://jiffyclub.github.io/snakeviz/)
+ ```sh
+ snakeviz my_profile.prof
+ ```
+
+ Visualize a profile as directed graph with [graphviz](https://graphviz.org/download/) & [gprof2dot](https://github.com/jrfonseca/gprof2dot)
+ ```sh
+ gprof2dot -f pstats my_profile.prof | dot -Tpng -o my_profile.png
+ # SVG or PDF may be nicer - you can search for function names
+ gprof2dot -f pstats my_profile.prof | dot -Tsvg -o my_profile.svg
+ gprof2dot -f pstats my_profile.prof | dot -Tpdf -o my_profile.pdf
+ ```
+ """
+
+ def __init__(self, logger: Logger, output_dir: Path, prefix: Optional[str] = None) -> None:
+ self._logger = logger.getChild(f"profiler.{prefix}" if prefix else "profiler")
+ self._output_dir = output_dir
+ self._output_dir.mkdir(parents=True, exist_ok=True)
+ self._profiler: Optional[cProfile.Profile] = None
+ self._prefix = prefix
+
+ self.profile_id: Optional[str] = None
+
+ def start(self, profile_id: str) -> None:
+ if self._profiler:
+ self.stop()
+
+ self.profile_id = profile_id
+
+ self._profiler = cProfile.Profile()
+ self._profiler.enable()
+ self._logger.info(f"Started profiling {self.profile_id}.")
+
+ def stop(self) -> Path:
+ if not self._profiler:
+ raise RuntimeError("Profiler not initialized. Call start() first.")
+ self._profiler.disable()
+
+ filename = f"{self._prefix}_{self.profile_id}.prof" if self._prefix else f"{self.profile_id}.prof"
+ path = Path(self._output_dir, filename)
+
+ self._profiler.dump_stats(path)
+ self._logger.info(f"Stopped profiling, profile dumped to {path}.")
+ self._profiler = None
+ self.profile_id = None
+
+ return path
diff --git a/invokeai/app/util/startup_utils.py b/invokeai/app/util/startup_utils.py
new file mode 100644
index 00000000000..08368021cff
--- /dev/null
+++ b/invokeai/app/util/startup_utils.py
@@ -0,0 +1,74 @@
+import logging
+import mimetypes
+import socket
+from pathlib import Path
+
+import torch
+
+
+def find_open_port(port: int) -> int:
+ """Find a port not in use starting at given port"""
+ # Taken from https://waylonwalker.com/python-find-available-port/, thanks Waylon!
+ # https://github.com/WaylonWalker
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.settimeout(1)
+ if s.connect_ex(("localhost", port)) == 0:
+ return find_open_port(port=port + 1)
+ else:
+ return port
+
+
+def check_cudnn(logger: logging.Logger) -> None:
+ """Check for cuDNN issues that could be causing degraded performance."""
+ if torch.backends.cudnn.is_available():
+ try:
+ # Note: At the time of writing (torch 2.2.1), torch.backends.cudnn.version() only raises an error the first
+ # time it is called. Subsequent calls will return the version number without complaining about a mismatch.
+ cudnn_version = torch.backends.cudnn.version()
+ logger.info(f"cuDNN version: {cudnn_version}")
+ except RuntimeError as e:
+ logger.warning(
+ "Encountered a cuDNN version issue. This may result in degraded performance. This issue is usually "
+ "caused by an incompatible cuDNN version installed in your python environment, or on the host "
+ f"system. Full error message:\n{e}"
+ )
+
+
+def invokeai_source_dir() -> Path:
+ # `invokeai.__file__` doesn't always work for editable installs
+ this_module_path = Path(__file__).resolve()
+ # https://youtrack.jetbrains.com/issue/PY-38382/Unresolved-reference-spec-but-this-is-standard-builtin
+ # noinspection PyUnresolvedReferences
+ depth = len(__spec__.parent.split("."))
+ return this_module_path.parents[depth - 1]
+
+
+def enable_dev_reload(custom_nodes_path=None) -> None:
+ """Enable hot reloading on python file changes during development."""
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ try:
+ import jurigged
+ except ImportError as e:
+ raise RuntimeError(
+ 'Can\'t start `--dev_reload` because jurigged is not found; `pip install -e ".[dev]"` to include development dependencies.'
+ ) from e
+ else:
+ paths = [str(invokeai_source_dir() / "*.py")]
+ if custom_nodes_path:
+ paths.append(str(custom_nodes_path / "*.py"))
+ jurigged.watch(pattern=paths, logger=InvokeAILogger.get_logger(name="jurigged").info)
+
+
+def apply_monkeypatches() -> None:
+ """Apply monkeypatches to fix issues with third-party libraries."""
+
+ import invokeai.backend.util.hotfixes # noqa: F401 (monkeypatching on import)
+
+
+def register_mime_types() -> None:
+ """Register additional mime types for windows."""
+ # Fix for windows mimetypes registry entries being borked.
+ # see https://github.com/invoke-ai/InvokeAI/discussions/3684#discussioncomment-6391352
+ mimetypes.add_type("application/javascript", ".js")
+ mimetypes.add_type("text/css", ".css")
diff --git a/invokeai/app/util/step_callback.py b/invokeai/app/util/step_callback.py
new file mode 100644
index 00000000000..08dc9a2265c
--- /dev/null
+++ b/invokeai/app/util/step_callback.py
@@ -0,0 +1,294 @@
+from math import floor
+from typing import Callable, Optional, TypeAlias
+
+import torch
+from PIL import Image
+
+from invokeai.app.services.session_processor.session_processor_common import CanceledException
+from invokeai.backend.model_manager.taxonomy import BaseModelType
+from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
+
+# See scripts/generate_vae_linear_approximation.py for generating these factors.
+
+# fast latents preview matrix for sdxl
+# generated by @StAlKeR7779
+SDXL_LATENT_RGB_FACTORS = [
+ # R G B
+ [0.3816, 0.4930, 0.5320],
+ [-0.3753, 0.1631, 0.1739],
+ [0.1770, 0.3588, -0.2048],
+ [-0.4350, -0.2644, -0.4289],
+]
+SDXL_SMOOTH_MATRIX = [
+ [0.0358, 0.0964, 0.0358],
+ [0.0964, 0.4711, 0.0964],
+ [0.0358, 0.0964, 0.0358],
+]
+
+# origingally adapted from code by @erucipe and @keturn here:
+# https://discuss.huggingface.co/t/decoding-latents-to-rgb-without-upscaling/23204/7
+# these updated numbers for v1.5 are from @torridgristle
+SD1_5_LATENT_RGB_FACTORS = [
+ # R G B
+ [0.3444, 0.1385, 0.0670], # L1
+ [0.1247, 0.4027, 0.1494], # L2
+ [-0.3192, 0.2513, 0.2103], # L3
+ [-0.1307, -0.1874, -0.7445], # L4
+]
+
+SD3_5_LATENT_RGB_FACTORS = [
+ [-0.05240681, 0.03251581, 0.0749016],
+ [-0.0580572, 0.00759826, 0.05729818],
+ [0.16144888, 0.01270368, -0.03768577],
+ [0.14418615, 0.08460266, 0.15941818],
+ [0.04894035, 0.0056485, -0.06686988],
+ [0.05187166, 0.19222395, 0.06261094],
+ [0.1539433, 0.04818359, 0.07103094],
+ [-0.08601796, 0.09013458, 0.10893912],
+ [-0.12398469, -0.06766567, 0.0033688],
+ [-0.0439737, 0.07825329, 0.02258823],
+ [0.03101129, 0.06382551, 0.07753657],
+ [-0.01315361, 0.08554491, -0.08772475],
+ [0.06464487, 0.05914605, 0.13262741],
+ [-0.07863674, -0.02261737, -0.12761454],
+ [-0.09923835, -0.08010759, -0.06264447],
+ [-0.03392309, -0.0804029, -0.06078822],
+]
+
+FLUX_LATENT_RGB_FACTORS = [
+ [-0.0412, 0.0149, 0.0521],
+ [0.0056, 0.0291, 0.0768],
+ [0.0342, -0.0681, -0.0427],
+ [-0.0258, 0.0092, 0.0463],
+ [0.0863, 0.0784, 0.0547],
+ [-0.0017, 0.0402, 0.0158],
+ [0.0501, 0.1058, 0.1152],
+ [-0.0209, -0.0218, -0.0329],
+ [-0.0314, 0.0083, 0.0896],
+ [0.0851, 0.0665, -0.0472],
+ [-0.0534, 0.0238, -0.0024],
+ [0.0452, -0.0026, 0.0048],
+ [0.0892, 0.0831, 0.0881],
+ [-0.1117, -0.0304, -0.0789],
+ [0.0027, -0.0479, -0.0043],
+ [-0.1146, -0.0827, -0.0598],
+]
+
+COGVIEW4_LATENT_RGB_FACTORS = [
+ [0.00408832, -0.00082485, -0.00214816],
+ [0.00084172, 0.00132241, 0.00842067],
+ [-0.00466737, -0.00983181, -0.00699561],
+ [0.03698397, -0.04797235, 0.03585809],
+ [0.00234701, -0.00124326, 0.00080869],
+ [-0.00723903, -0.00388422, -0.00656606],
+ [-0.00970917, -0.00467356, -0.00971113],
+ [0.17292486, -0.03452463, -0.1457515],
+ [0.02330308, 0.02942557, 0.02704329],
+ [-0.00903131, -0.01499841, -0.01432564],
+ [0.01250298, 0.0019407, -0.02168986],
+ [0.01371188, 0.00498283, -0.01302135],
+ [0.42396525, 0.4280575, 0.42148206],
+ [0.00983825, 0.00613302, 0.00610316],
+ [0.00473307, -0.00889551, -0.00915924],
+ [-0.00955853, -0.00980067, -0.00977842],
+]
+
+# Qwen Image uses the same VAE as Wan 2.1 (16-channel).
+# Factors from ComfyUI: https://github.com/comfyanonymous/ComfyUI/blob/master/comfy/latent_formats.py
+QWEN_IMAGE_LATENT_RGB_FACTORS = [
+ [-0.1299, -0.1692, 0.2932],
+ [0.0671, 0.0406, 0.0442],
+ [0.3568, 0.2548, 0.1747],
+ [0.0372, 0.2344, 0.1420],
+ [0.0313, 0.0189, -0.0328],
+ [0.0296, -0.0956, -0.0665],
+ [-0.3477, -0.4059, -0.2925],
+ [0.0166, 0.1902, 0.1975],
+ [-0.0412, 0.0267, -0.1364],
+ [-0.1293, 0.0740, 0.1636],
+ [0.0680, 0.3019, 0.1128],
+ [0.0032, 0.0581, 0.0639],
+ [-0.1251, 0.0927, 0.1699],
+ [0.0060, -0.0633, 0.0005],
+ [0.3477, 0.2275, 0.2950],
+ [0.1984, 0.0913, 0.1861],
+]
+
+QWEN_IMAGE_LATENT_RGB_BIAS = [-0.1835, -0.0868, -0.3360]
+
+# FLUX.2 uses 32 latent channels.
+# Factors from ComfyUI: https://github.com/Comfy-Org/ComfyUI/blob/main/comfy/latent_formats.py
+FLUX2_LATENT_RGB_FACTORS = [
+ # R G B
+ [0.0058, 0.0113, 0.0073],
+ [0.0495, 0.0443, 0.0836],
+ [-0.0099, 0.0096, 0.0644],
+ [0.2144, 0.3009, 0.3652],
+ [0.0166, -0.0039, -0.0054],
+ [0.0157, 0.0103, -0.0160],
+ [-0.0398, 0.0902, -0.0235],
+ [-0.0052, 0.0095, 0.0109],
+ [-0.3527, -0.2712, -0.1666],
+ [-0.0301, -0.0356, -0.0180],
+ [-0.0107, 0.0078, 0.0013],
+ [0.0746, 0.0090, -0.0941],
+ [0.0156, 0.0169, 0.0070],
+ [-0.0034, -0.0040, -0.0114],
+ [0.0032, 0.0181, 0.0080],
+ [-0.0939, -0.0008, 0.0186],
+ [0.0018, 0.0043, 0.0104],
+ [0.0284, 0.0056, -0.0127],
+ [-0.0024, -0.0022, -0.0030],
+ [0.1207, -0.0026, 0.0065],
+ [0.0128, 0.0101, 0.0142],
+ [0.0137, -0.0072, -0.0007],
+ [0.0095, 0.0092, -0.0059],
+ [0.0000, -0.0077, -0.0049],
+ [-0.0465, -0.0204, -0.0312],
+ [0.0095, 0.0012, -0.0066],
+ [0.0290, -0.0034, 0.0025],
+ [0.0220, 0.0169, -0.0048],
+ [-0.0332, -0.0457, -0.0468],
+ [-0.0085, 0.0389, 0.0609],
+ [-0.0076, 0.0003, -0.0043],
+ [-0.0111, -0.0460, -0.0614],
+]
+
+FLUX2_LATENT_RGB_BIAS = [-0.0329, -0.0718, -0.0851]
+
+# Anima uses Wan 2.1 VAE with 16 latent channels.
+# Factors from ComfyUI: https://github.com/Comfy-Org/ComfyUI/blob/main/comfy/latent_formats.py
+ANIMA_LATENT_RGB_FACTORS = [
+ [-0.1299, -0.1692, 0.2932],
+ [0.0671, 0.0406, 0.0442],
+ [0.3568, 0.2548, 0.1747],
+ [0.0372, 0.2344, 0.1420],
+ [0.0313, 0.0189, -0.0328],
+ [0.0296, -0.0956, -0.0665],
+ [-0.3477, -0.4059, -0.2925],
+ [0.0166, 0.1902, 0.1975],
+ [-0.0412, 0.0267, -0.1364],
+ [-0.1293, 0.0740, 0.1636],
+ [0.0680, 0.3019, 0.1128],
+ [0.0032, 0.0581, 0.0639],
+ [-0.1251, 0.0927, 0.1699],
+ [0.0060, -0.0633, 0.0005],
+ [0.3477, 0.2275, 0.2950],
+ [0.1984, 0.0913, 0.1861],
+]
+
+ANIMA_LATENT_RGB_BIAS = [-0.1835, -0.0868, -0.3360]
+
+
+def sample_to_lowres_estimated_image(
+ samples: torch.Tensor,
+ latent_rgb_factors: torch.Tensor,
+ smooth_matrix: Optional[torch.Tensor] = None,
+ latent_rgb_bias: Optional[torch.Tensor] = None,
+):
+ if samples.dim() == 4:
+ samples = samples[0]
+ latent_image = samples.permute(1, 2, 0) @ latent_rgb_factors
+
+ if latent_rgb_bias is not None:
+ latent_image = latent_image + latent_rgb_bias
+
+ if smooth_matrix is not None:
+ latent_image = latent_image.unsqueeze(0).permute(3, 0, 1, 2)
+ latent_image = torch.nn.functional.conv2d(latent_image, smooth_matrix.reshape((1, 1, 3, 3)), padding=1)
+ latent_image = latent_image.permute(1, 2, 3, 0).squeeze(0)
+
+ latents_ubyte = (
+ ((latent_image + 1) / 2).clamp(0, 1).mul(0xFF).byte() # change scale from -1..1 to 0..1 # to 0..255
+ ).cpu()
+
+ return Image.fromarray(latents_ubyte.numpy())
+
+
+def calc_percentage(intermediate_state: PipelineIntermediateState) -> float:
+ """Calculate the percentage of completion of denoising."""
+
+ step = intermediate_state.step
+ total_steps = intermediate_state.total_steps
+ order = intermediate_state.order
+
+ if total_steps == 0:
+ return 0.0
+ if order == 2:
+ # Prevent division by zero when total_steps is 1 or 2
+ denominator = floor(total_steps / 2)
+ if denominator == 0:
+ return 0.0
+ return floor(step / 2) / denominator
+ # order == 1
+ return step / total_steps
+
+
+SignalProgressFunc: TypeAlias = Callable[[str, float | None, Image.Image | None, tuple[int, int] | None], None]
+
+
+def diffusion_step_callback(
+ signal_progress: SignalProgressFunc,
+ intermediate_state: PipelineIntermediateState,
+ base_model: BaseModelType,
+ is_canceled: Callable[[], bool],
+) -> None:
+ if is_canceled():
+ raise CanceledException
+
+ # Some schedulers report not only the noisy latents at the current timestep,
+ # but also their estimate so far of what the de-noised latents will be. Use
+ # that estimate if it is available.
+ if intermediate_state.predicted_original is not None:
+ sample = intermediate_state.predicted_original
+ else:
+ sample = intermediate_state.latents
+
+ smooth_matrix: list[list[float]] | None = None
+ latent_rgb_bias: list[float] | None = None
+ if base_model in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]:
+ latent_rgb_factors = SD1_5_LATENT_RGB_FACTORS
+ elif base_model in [BaseModelType.StableDiffusionXL, BaseModelType.StableDiffusionXLRefiner]:
+ latent_rgb_factors = SDXL_LATENT_RGB_FACTORS
+ smooth_matrix = SDXL_SMOOTH_MATRIX
+ elif base_model == BaseModelType.StableDiffusion3:
+ latent_rgb_factors = SD3_5_LATENT_RGB_FACTORS
+ elif base_model == BaseModelType.CogView4:
+ latent_rgb_factors = COGVIEW4_LATENT_RGB_FACTORS
+ elif base_model == BaseModelType.QwenImage:
+ latent_rgb_factors = QWEN_IMAGE_LATENT_RGB_FACTORS
+ latent_rgb_bias = QWEN_IMAGE_LATENT_RGB_BIAS
+ elif base_model == BaseModelType.Flux:
+ latent_rgb_factors = FLUX_LATENT_RGB_FACTORS
+ elif base_model == BaseModelType.Flux2:
+ latent_rgb_factors = FLUX2_LATENT_RGB_FACTORS
+ latent_rgb_bias = FLUX2_LATENT_RGB_BIAS
+ elif base_model == BaseModelType.ZImage:
+ # Z-Image uses FLUX-compatible VAE with 16 latent channels
+ latent_rgb_factors = FLUX_LATENT_RGB_FACTORS
+ elif base_model == BaseModelType.Anima:
+ # Anima uses Wan 2.1 VAE with 16 latent channels
+ latent_rgb_factors = ANIMA_LATENT_RGB_FACTORS
+ latent_rgb_bias = ANIMA_LATENT_RGB_BIAS
+ else:
+ raise ValueError(f"Unsupported base model: {base_model}")
+
+ latent_rgb_factors_torch = torch.tensor(latent_rgb_factors, dtype=sample.dtype, device=sample.device)
+ smooth_matrix_torch = (
+ torch.tensor(smooth_matrix, dtype=sample.dtype, device=sample.device) if smooth_matrix else None
+ )
+ latent_rgb_bias_torch = (
+ torch.tensor(latent_rgb_bias, dtype=sample.dtype, device=sample.device) if latent_rgb_bias else None
+ )
+ image = sample_to_lowres_estimated_image(
+ samples=sample,
+ latent_rgb_factors=latent_rgb_factors_torch,
+ smooth_matrix=smooth_matrix_torch,
+ latent_rgb_bias=latent_rgb_bias_torch,
+ )
+
+ width = image.width * 8
+ height = image.height * 8
+ percentage = calc_percentage(intermediate_state)
+
+ signal_progress("Denoising", percentage, image, (width, height))
diff --git a/invokeai/app/util/suppress_output.py b/invokeai/app/util/suppress_output.py
new file mode 100644
index 00000000000..d5e69460e27
--- /dev/null
+++ b/invokeai/app/util/suppress_output.py
@@ -0,0 +1,24 @@
+import io
+import sys
+from typing import Any
+
+
+class SuppressOutput:
+ """Context manager to suppress stdout.
+
+ Example:
+ ```
+ with SuppressOutput():
+ print("This will not be printed")
+ ```
+ """
+
+ def __enter__(self):
+ # Save the original stdout
+ self._original_stdout = sys.stdout
+ # Redirect stdout to a dummy StringIO object
+ sys.stdout = io.StringIO()
+
+ def __exit__(self, *args: Any, **kwargs: Any):
+ # Restore stdout
+ sys.stdout = self._original_stdout
diff --git a/invokeai/app/util/t5_model_identifier.py b/invokeai/app/util/t5_model_identifier.py
new file mode 100644
index 00000000000..a0d999920c8
--- /dev/null
+++ b/invokeai/app/util/t5_model_identifier.py
@@ -0,0 +1,26 @@
+from invokeai.app.invocations.model import ModelIdentifierField
+from invokeai.backend.model_manager.taxonomy import BaseModelType, SubModelType
+
+
+def preprocess_t5_encoder_model_identifier(model_identifier: ModelIdentifierField) -> ModelIdentifierField:
+ """A helper function to normalize a T5 encoder model identifier so that T5 models associated with FLUX
+ or SD3 models can be used interchangeably.
+ """
+ if model_identifier.base == BaseModelType.Any:
+ return model_identifier.model_copy(update={"submodel_type": SubModelType.TextEncoder2})
+ elif model_identifier.base == BaseModelType.StableDiffusion3:
+ return model_identifier.model_copy(update={"submodel_type": SubModelType.TextEncoder3})
+ else:
+ raise ValueError(f"Unsupported model base: {model_identifier.base}")
+
+
+def preprocess_t5_tokenizer_model_identifier(model_identifier: ModelIdentifierField) -> ModelIdentifierField:
+ """A helper function to normalize a T5 tokenizer model identifier so that T5 models associated with FLUX
+ or SD3 models can be used interchangeably.
+ """
+ if model_identifier.base == BaseModelType.Any:
+ return model_identifier.model_copy(update={"submodel_type": SubModelType.Tokenizer2})
+ elif model_identifier.base == BaseModelType.StableDiffusion3:
+ return model_identifier.model_copy(update={"submodel_type": SubModelType.Tokenizer3})
+ else:
+ raise ValueError(f"Unsupported model base: {model_identifier.base}")
diff --git a/invokeai/app/util/thumbnails.py b/invokeai/app/util/thumbnails.py
new file mode 100644
index 00000000000..ad722f197e4
--- /dev/null
+++ b/invokeai/app/util/thumbnails.py
@@ -0,0 +1,16 @@
+import os
+
+from PIL import Image
+
+
+def get_thumbnail_name(image_name: str) -> str:
+ """Formats given an image name, returns the appropriate thumbnail image name"""
+ thumbnail_name = os.path.splitext(image_name)[0] + ".webp"
+ return thumbnail_name
+
+
+def make_thumbnail(image: Image.Image, size: int = 256) -> Image.Image:
+ """Makes a thumbnail from a PIL Image"""
+ thumbnail = image.copy()
+ thumbnail.thumbnail(size=(size, size))
+ return thumbnail
diff --git a/invokeai/app/util/ti_utils.py b/invokeai/app/util/ti_utils.py
new file mode 100644
index 00000000000..8f18b14d66b
--- /dev/null
+++ b/invokeai/app/util/ti_utils.py
@@ -0,0 +1,47 @@
+import re
+from typing import List, Tuple
+
+import invokeai.backend.util.logging as logger
+from invokeai.app.services.model_records import UnknownModelException
+from invokeai.app.services.shared.invocation_context import InvocationContext
+from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
+from invokeai.backend.textual_inversion import TextualInversionModelRaw
+
+
+def extract_ti_triggers_from_prompt(prompt: str) -> List[str]:
+ ti_triggers: List[str] = []
+ for trigger in re.findall(r"<[a-zA-Z0-9., _-]+>", prompt):
+ ti_triggers.append(str(trigger))
+ return ti_triggers
+
+
+def generate_ti_list(
+ prompt: str, base: BaseModelType, context: InvocationContext
+) -> List[Tuple[str, TextualInversionModelRaw]]:
+ ti_list: List[Tuple[str, TextualInversionModelRaw]] = []
+ for trigger in extract_ti_triggers_from_prompt(prompt):
+ name_or_key = trigger[1:-1]
+ try:
+ loaded_model = context.models.load(name_or_key)
+ model = loaded_model.model
+ assert isinstance(model, TextualInversionModelRaw)
+ assert loaded_model.config.base == base
+ ti_list.append((name_or_key, model))
+ except UnknownModelException:
+ try:
+ loaded_model = context.models.load_by_attrs(
+ name=name_or_key, base=base, type=ModelType.TextualInversion
+ )
+ model = loaded_model.model
+ assert isinstance(model, TextualInversionModelRaw)
+ assert loaded_model.config.base == base
+ ti_list.append((name_or_key, model))
+ except UnknownModelException:
+ pass
+ except ValueError:
+ logger.warning(f'trigger: "{trigger}" more than one similarly-named textual inversion models')
+ except AssertionError:
+ logger.warning(f'trigger: "{trigger}" not a valid textual inversion model for this graph')
+ except Exception:
+ logger.warning(f'Failed to load TI model for trigger: "{trigger}"')
+ return ti_list
diff --git a/invokeai/app/util/torch_cuda_allocator.py b/invokeai/app/util/torch_cuda_allocator.py
new file mode 100644
index 00000000000..d1c34cd3ceb
--- /dev/null
+++ b/invokeai/app/util/torch_cuda_allocator.py
@@ -0,0 +1,52 @@
+import logging
+import os
+import sys
+
+
+def configure_torch_cuda_allocator(pytorch_cuda_alloc_conf: str, logger: logging.Logger):
+ """Configure the PyTorch CUDA memory allocator. See
+ https://pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf for supported
+ configurations.
+ """
+
+ if "torch" in sys.modules:
+ raise RuntimeError("configure_torch_cuda_allocator() must be called before importing torch.")
+
+ # Log a warning if the PYTORCH_CUDA_ALLOC_CONF environment variable is already set.
+ prev_cuda_alloc_conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", None)
+ if prev_cuda_alloc_conf is not None:
+ if prev_cuda_alloc_conf == pytorch_cuda_alloc_conf:
+ logger.info(
+ f"PYTORCH_CUDA_ALLOC_CONF is already set to '{pytorch_cuda_alloc_conf}'. Skipping configuration."
+ )
+ return
+ else:
+ logger.warning(
+ f"Attempted to configure the PyTorch CUDA memory allocator with '{pytorch_cuda_alloc_conf}', but PYTORCH_CUDA_ALLOC_CONF is already set to "
+ f"'{prev_cuda_alloc_conf}'. Skipping configuration."
+ )
+ return
+
+ # Configure the PyTorch CUDA memory allocator.
+ # NOTE: It is important that this happens before torch is imported.
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = pytorch_cuda_alloc_conf
+
+ import torch
+
+ # Relevant docs: https://pytorch.org/docs/stable/notes/cuda.html#optimizing-memory-usage-with-pytorch-cuda-alloc-conf
+ if not torch.cuda.is_available():
+ raise RuntimeError(
+ "Attempted to configure the PyTorch CUDA memory allocator, but no CUDA devices are available."
+ )
+
+ # Verify that the torch allocator was properly configured.
+ allocator_backend = torch.cuda.get_allocator_backend()
+ expected_backend = "cudaMallocAsync" if "cudaMallocAsync" in pytorch_cuda_alloc_conf else "native"
+ if allocator_backend != expected_backend:
+ raise RuntimeError(
+ f"Failed to configure the PyTorch CUDA memory allocator. Expected backend: '{expected_backend}', but got "
+ f"'{allocator_backend}'. Verify that 1) the pytorch_cuda_alloc_conf is set correctly, and 2) that torch is "
+ "not imported before calling configure_torch_cuda_allocator()."
+ )
+
+ logger.info(f"PyTorch CUDA memory allocator: {torch.cuda.get_allocator_backend()}")
diff --git a/invokeai/app/util/user_management.py b/invokeai/app/util/user_management.py
new file mode 100644
index 00000000000..24b1fe91ab9
--- /dev/null
+++ b/invokeai/app/util/user_management.py
@@ -0,0 +1,579 @@
+"""User management command entry points for InvokeAI.
+
+These functions are registered as console scripts in pyproject.toml and can be
+called from the command line after installing the package:
+
+ invoke-useradd -- add a user
+ invoke-userdel -- delete a user
+ invoke-userlist -- list users
+ invoke-usermod -- modify a user
+"""
+
+import argparse
+import getpass
+import json
+import os
+import sys
+
+_root_help = (
+ "Path to the InvokeAI root directory. If omitted, the root is resolved in this order: "
+ "the $INVOKEAI_ROOT environment variable, the active virtual environment's parent directory, "
+ "or $HOME/invokeai."
+)
+
+# ---------------------------------------------------------------------------
+# useradd
+# ---------------------------------------------------------------------------
+
+
+def _add_user_interactive() -> bool:
+ """Add a user interactively by prompting for details."""
+ from invokeai.app.services.auth.password_utils import validate_password_strength
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_common import UserCreateRequest
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ print("=== Add InvokeAI User ===\n")
+
+ email = input("Email address: ").strip()
+ if not email:
+ print("Error: Email is required")
+ return False
+
+ display_name = input("Display name (optional): ").strip() or None
+
+ while True:
+ password = getpass.getpass("Password: ")
+ password_confirm = getpass.getpass("Confirm password: ")
+
+ if password != password_confirm:
+ print("Error: Passwords do not match. Please try again.\n")
+ continue
+
+ is_valid, error_msg = validate_password_strength(password)
+ if not is_valid:
+ print(f"Error: {error_msg}\n")
+ continue
+
+ break
+
+ is_admin_input = input("Make this user an administrator? (y/N): ").strip().lower()
+ is_admin = is_admin_input in ("y", "yes")
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user_data = UserCreateRequest(email=email, display_name=display_name, password=password, is_admin=is_admin)
+ user = user_service.create(user_data)
+
+ print("\n✅ User created successfully!")
+ print(f" User ID: {user.user_id}")
+ print(f" Email: {user.email}")
+ print(f" Display Name: {user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if user.is_active else 'No'}")
+ return True
+
+ except ValueError as e:
+ print(f"\n❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"\n❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def _add_user_cli(email: str, password: str, display_name: str | None = None, is_admin: bool = False) -> bool:
+ """Add a user via CLI arguments."""
+ from invokeai.app.services.auth.password_utils import validate_password_strength
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_common import UserCreateRequest
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ is_valid, error_msg = validate_password_strength(password)
+ if not is_valid:
+ print(f"❌ Password validation failed: {error_msg}")
+ return False
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user_data = UserCreateRequest(email=email, display_name=display_name, password=password, is_admin=is_admin)
+ user = user_service.create(user_data)
+
+ print("✅ User created successfully!")
+ print(f" User ID: {user.user_id}")
+ print(f" Email: {user.email}")
+ print(f" Display Name: {user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if user.is_active else 'No'}")
+ return True
+
+ except ValueError as e:
+ print(f"❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def useradd() -> None:
+ """Entry point for ``invoke-useradd``."""
+ parser = argparse.ArgumentParser(
+ description="Add a user to the InvokeAI database",
+ epilog="If no arguments are provided, the script will run in interactive mode.",
+ )
+ parser.add_argument("--root", "-r", help=_root_help)
+ parser.add_argument("--email", "-e", help="User email address")
+ parser.add_argument("--password", "-p", help="User password")
+ parser.add_argument("--name", "-n", help="User display name (optional)")
+ parser.add_argument("--admin", "-a", action="store_true", help="Make user an administrator")
+
+ args = parser.parse_args()
+
+ if args.root:
+ os.environ["INVOKEAI_ROOT"] = args.root
+
+ if args.email or args.password:
+ if not args.email or not args.password:
+ print("❌ Error: Both --email and --password are required when using CLI mode")
+ print(" Run without arguments for interactive mode")
+ sys.exit(1)
+ success = _add_user_cli(args.email, args.password, args.name, args.admin)
+ else:
+ success = _add_user_interactive()
+
+ sys.exit(0 if success else 1)
+
+
+# ---------------------------------------------------------------------------
+# userdel
+# ---------------------------------------------------------------------------
+
+
+def _delete_user_interactive() -> bool:
+ """Delete a user interactively by prompting for email."""
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ print("=== Delete InvokeAI User ===\n")
+
+ email = input("Email address of user to delete: ").strip()
+ if not email:
+ print("Error: Email is required")
+ return False
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user = user_service.get_by_email(email)
+ if not user:
+ print(f"\n❌ Error: No user found with email '{email}'")
+ return False
+
+ print("\nUser to delete:")
+ print(f" User ID: {user.user_id}")
+ print(f" Email: {user.email}")
+ print(f" Display Name: {user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if user.is_active else 'No'}")
+
+ confirm = input("\n⚠️ Are you sure you want to delete this user? (yes/no): ").strip().lower()
+ if confirm not in ("yes", "y"):
+ print("Deletion cancelled.")
+ return False
+
+ user_service.delete(user.user_id)
+ print("\n✅ User deleted successfully!")
+ return True
+
+ except ValueError as e:
+ print(f"\n❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"\n❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def _delete_user_cli(email: str, force: bool = False) -> bool:
+ """Delete a user via CLI arguments."""
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user = user_service.get_by_email(email)
+ if not user:
+ print(f"❌ Error: No user found with email '{email}'")
+ return False
+
+ if not force:
+ print("User to delete:")
+ print(f" User ID: {user.user_id}")
+ print(f" Email: {user.email}")
+ print(f" Display Name: {user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if user.is_active else 'No'}")
+
+ confirm = input("\n⚠️ Are you sure you want to delete this user? (yes/no): ").strip().lower()
+ if confirm not in ("yes", "y"):
+ print("Deletion cancelled.")
+ return False
+
+ user_service.delete(user.user_id)
+ print("✅ User deleted successfully!")
+ return True
+
+ except ValueError as e:
+ print(f"❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def userdel() -> None:
+ """Entry point for ``invoke-userdel``."""
+ parser = argparse.ArgumentParser(
+ description="Delete a user from the InvokeAI database",
+ epilog="If no arguments are provided, the script will run in interactive mode.",
+ )
+ parser.add_argument("--root", "-r", help=_root_help)
+ parser.add_argument("--email", "-e", help="User email address")
+ parser.add_argument("--force", "-f", action="store_true", help="Delete without confirmation prompt")
+
+ args = parser.parse_args()
+
+ if args.root:
+ os.environ["INVOKEAI_ROOT"] = args.root
+
+ if args.email:
+ success = _delete_user_cli(args.email, args.force)
+ else:
+ success = _delete_user_interactive()
+
+ sys.exit(0 if success else 1)
+
+
+# ---------------------------------------------------------------------------
+# userlist
+# ---------------------------------------------------------------------------
+
+
+def _list_users_table() -> bool:
+ """List all users in a formatted table."""
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ config = get_config()
+ logger = InvokeAILogger.get_logger(config=config)
+ db = SqliteDatabase(config.db_path, logger)
+ user_service = UserService(db)
+
+ try:
+ users = user_service.list_users()
+
+ if not users:
+ print("No users found in database.")
+ return True
+
+ print("\n=== InvokeAI Users ===\n")
+ print(f"{'User ID':<36} {'Email':<30} {'Display Name':<20} {'Admin':<8} {'Active':<8}")
+ print("-" * 108)
+
+ for user in users:
+ user_id = user.user_id
+ email = user.email[:29] if len(user.email) > 29 else user.email
+ raw_name = user.display_name or ""
+ name = raw_name[:19] if len(raw_name) > 19 else raw_name
+ is_admin = "Yes" if user.is_admin else "No"
+ is_active = "Yes" if user.is_active else "No"
+ print(f"{user_id:<36} {email:<30} {name:<20} {is_admin:<8} {is_active:<8}")
+
+ print(f"\nTotal users: {len(users)}")
+ return True
+
+ except Exception as e:
+ print(f"Error listing users: {e}")
+ return False
+
+
+def _list_users_json() -> bool:
+ """List all users in JSON format."""
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ config = get_config()
+ logger = InvokeAILogger.get_logger(config=config)
+ db = SqliteDatabase(config.db_path, logger)
+ user_service = UserService(db)
+
+ try:
+ users = user_service.list_users()
+
+ users_data = [
+ {
+ "id": user.user_id,
+ "email": user.email,
+ "name": user.display_name,
+ "is_admin": user.is_admin,
+ "is_active": user.is_active,
+ }
+ for user in users
+ ]
+
+ print(json.dumps(users_data, indent=2))
+ return True
+
+ except Exception as e:
+ print(f'{{"error": "{e}"}}', file=sys.stderr)
+ return False
+
+
+def userlist() -> None:
+ """Entry point for ``invoke-userlist``."""
+ parser = argparse.ArgumentParser(
+ description="List users from the InvokeAI database",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ invoke-userlist
+ invoke-userlist --json
+ """,
+ )
+ parser.add_argument("--root", "-r", help=_root_help)
+ parser.add_argument(
+ "--json",
+ action="store_true",
+ help="Output users in JSON format instead of table",
+ )
+
+ args = parser.parse_args()
+
+ if args.root:
+ os.environ["INVOKEAI_ROOT"] = args.root
+
+ success = _list_users_json() if args.json else _list_users_table()
+ sys.exit(0 if success else 1)
+
+
+# ---------------------------------------------------------------------------
+# usermod
+# ---------------------------------------------------------------------------
+
+
+def _modify_user_interactive() -> bool:
+ """Modify a user interactively by prompting for details."""
+ from invokeai.app.services.auth.password_utils import validate_password_strength
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_common import UserUpdateRequest
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ print("=== Modify InvokeAI User ===\n")
+
+ email = input("Email address of user to modify: ").strip()
+ if not email:
+ print("Error: Email is required")
+ return False
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user = user_service.get_by_email(email)
+ if not user:
+ print(f"\n❌ Error: No user found with email '{email}'")
+ return False
+
+ print("\nCurrent user details:")
+ print(f" User ID: {user.user_id}")
+ print(f" Email: {user.email}")
+ print(f" Display Name: {user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if user.is_active else 'No'}")
+
+ print("\n--- What would you like to change? (leave blank to keep current value) ---\n")
+
+ new_name = input(f"New display name [{user.display_name or '(not set)'}]: ").strip()
+ display_name = new_name if new_name else None
+
+ change_password = input("Change password? (y/N): ").strip().lower()
+ password = None
+ if change_password in ("y", "yes"):
+ while True:
+ password = getpass.getpass("New password: ")
+ if not password:
+ print("Keeping existing password.")
+ password = None
+ break
+
+ password_confirm = getpass.getpass("Confirm new password: ")
+
+ if password != password_confirm:
+ print("Error: Passwords do not match. Please try again.\n")
+ continue
+
+ is_valid, error_msg = validate_password_strength(password)
+ if not is_valid:
+ print(f"Error: {error_msg}\n")
+ continue
+
+ break
+
+ change_admin = input("Change admin status? (y/N): ").strip().lower()
+ is_admin = None
+ if change_admin in ("y", "yes"):
+ is_admin_input = (
+ input(f"Make administrator? [current: {'Yes' if user.is_admin else 'No'}] (y/N): ").strip().lower()
+ )
+ is_admin = is_admin_input in ("y", "yes")
+
+ if display_name is None and password is None and is_admin is None:
+ print("\nNo changes requested. User not modified.")
+ return True
+
+ changes = UserUpdateRequest(display_name=display_name, password=password, is_admin=is_admin)
+ updated_user = user_service.update(user.user_id, changes)
+
+ print("\n✅ User updated successfully!")
+ print(f" User ID: {updated_user.user_id}")
+ print(f" Email: {updated_user.email}")
+ print(f" Display Name: {updated_user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if updated_user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if updated_user.is_active else 'No'}")
+ return True
+
+ except ValueError as e:
+ print(f"\n❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"\n❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def _modify_user_cli(
+ email: str,
+ display_name: str | None = None,
+ password: str | None = None,
+ is_admin: bool | None = None,
+) -> bool:
+ """Modify a user via CLI arguments."""
+ from invokeai.app.services.auth.password_utils import validate_password_strength
+ from invokeai.app.services.config import get_config
+ from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
+ from invokeai.app.services.users.users_common import UserUpdateRequest
+ from invokeai.app.services.users.users_default import UserService
+ from invokeai.backend.util.logging import InvokeAILogger
+
+ if password is not None:
+ is_valid, error_msg = validate_password_strength(password)
+ if not is_valid:
+ print(f"❌ Password validation failed: {error_msg}")
+ return False
+
+ try:
+ config = get_config()
+ db = SqliteDatabase(config.db_path, InvokeAILogger.get_logger())
+ user_service = UserService(db)
+
+ user = user_service.get_by_email(email)
+ if not user:
+ print(f"❌ Error: No user found with email '{email}'")
+ return False
+
+ if display_name is None and password is None and is_admin is None:
+ print("❌ Error: No changes specified. Use --name, --password, --admin, or --no-admin")
+ return False
+
+ changes = UserUpdateRequest(display_name=display_name, password=password, is_admin=is_admin)
+ updated_user = user_service.update(user.user_id, changes)
+
+ print("✅ User updated successfully!")
+ print(f" User ID: {updated_user.user_id}")
+ print(f" Email: {updated_user.email}")
+ print(f" Display Name: {updated_user.display_name or '(not set)'}")
+ print(f" Admin: {'Yes' if updated_user.is_admin else 'No'}")
+ print(f" Active: {'Yes' if updated_user.is_active else 'No'}")
+ return True
+
+ except ValueError as e:
+ print(f"❌ Error: {e}")
+ return False
+ except Exception as e:
+ print(f"❌ Unexpected error: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return False
+
+
+def usermod() -> None:
+ """Entry point for ``invoke-usermod``."""
+ parser = argparse.ArgumentParser(
+ description="Modify a user in the InvokeAI database",
+ epilog="If no arguments are provided, the script will run in interactive mode.",
+ )
+ parser.add_argument("--root", "-r", help=_root_help)
+ parser.add_argument("--email", "-e", help="User email address")
+ parser.add_argument("--name", "-n", help="New display name")
+ parser.add_argument("--password", "-p", help="New password")
+
+ admin_group = parser.add_mutually_exclusive_group()
+ admin_group.add_argument("--admin", "-a", action="store_true", help="Grant administrator privileges")
+ admin_group.add_argument("--no-admin", dest="no_admin", action="store_true", help="Remove administrator privileges")
+
+ args = parser.parse_args()
+
+ if args.root:
+ os.environ["INVOKEAI_ROOT"] = args.root
+
+ is_admin = None
+ if args.admin:
+ is_admin = True
+ elif args.no_admin:
+ is_admin = False
+
+ if args.email:
+ success = _modify_user_cli(args.email, args.name, args.password, is_admin)
+ else:
+ success = _modify_user_interactive()
+
+ sys.exit(0 if success else 1)
diff --git a/invokeai/assets/__init__.py b/invokeai/assets/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/assets/a-painting-of-a-fire.png b/invokeai/assets/a-painting-of-a-fire.png
similarity index 100%
rename from assets/a-painting-of-a-fire.png
rename to invokeai/assets/a-painting-of-a-fire.png
diff --git a/assets/a-photograph-of-a-fire.png b/invokeai/assets/a-photograph-of-a-fire.png
similarity index 100%
rename from assets/a-photograph-of-a-fire.png
rename to invokeai/assets/a-photograph-of-a-fire.png
diff --git a/assets/a-shirt-with-a-fire-printed-on-it.png b/invokeai/assets/a-shirt-with-a-fire-printed-on-it.png
similarity index 100%
rename from assets/a-shirt-with-a-fire-printed-on-it.png
rename to invokeai/assets/a-shirt-with-a-fire-printed-on-it.png
diff --git a/assets/a-shirt-with-the-inscription-'fire'.png b/invokeai/assets/a-shirt-with-the-inscription-'fire'.png
similarity index 100%
rename from assets/a-shirt-with-the-inscription-'fire'.png
rename to invokeai/assets/a-shirt-with-the-inscription-'fire'.png
diff --git a/assets/a-watercolor-painting-of-a-fire.png b/invokeai/assets/a-watercolor-painting-of-a-fire.png
similarity index 100%
rename from assets/a-watercolor-painting-of-a-fire.png
rename to invokeai/assets/a-watercolor-painting-of-a-fire.png
diff --git a/assets/birdhouse.png b/invokeai/assets/birdhouse.png
similarity index 100%
rename from assets/birdhouse.png
rename to invokeai/assets/birdhouse.png
diff --git a/data/DejaVuSans.ttf b/invokeai/assets/data/DejaVuSans.ttf
similarity index 100%
rename from data/DejaVuSans.ttf
rename to invokeai/assets/data/DejaVuSans.ttf
diff --git a/data/example_conditioning/superresolution/sample_0.jpg b/invokeai/assets/data/example_conditioning/superresolution/sample_0.jpg
similarity index 100%
rename from data/example_conditioning/superresolution/sample_0.jpg
rename to invokeai/assets/data/example_conditioning/superresolution/sample_0.jpg
diff --git a/data/example_conditioning/text_conditional/sample_0.txt b/invokeai/assets/data/example_conditioning/text_conditional/sample_0.txt
similarity index 100%
rename from data/example_conditioning/text_conditional/sample_0.txt
rename to invokeai/assets/data/example_conditioning/text_conditional/sample_0.txt
diff --git a/data/imagenet_clsidx_to_label.txt b/invokeai/assets/data/imagenet_clsidx_to_label.txt
similarity index 100%
rename from data/imagenet_clsidx_to_label.txt
rename to invokeai/assets/data/imagenet_clsidx_to_label.txt
diff --git a/data/imagenet_train_hr_indices.p b/invokeai/assets/data/imagenet_train_hr_indices.p
similarity index 100%
rename from data/imagenet_train_hr_indices.p
rename to invokeai/assets/data/imagenet_train_hr_indices.p
diff --git a/data/imagenet_val_hr_indices.p b/invokeai/assets/data/imagenet_val_hr_indices.p
similarity index 100%
rename from data/imagenet_val_hr_indices.p
rename to invokeai/assets/data/imagenet_val_hr_indices.p
diff --git a/data/index_synset.yaml b/invokeai/assets/data/index_synset.yaml
similarity index 100%
rename from data/index_synset.yaml
rename to invokeai/assets/data/index_synset.yaml
diff --git a/data/inpainting_examples/6458524847_2f4c361183_k.png b/invokeai/assets/data/inpainting_examples/6458524847_2f4c361183_k.png
similarity index 100%
rename from data/inpainting_examples/6458524847_2f4c361183_k.png
rename to invokeai/assets/data/inpainting_examples/6458524847_2f4c361183_k.png
diff --git a/data/inpainting_examples/6458524847_2f4c361183_k_mask.png b/invokeai/assets/data/inpainting_examples/6458524847_2f4c361183_k_mask.png
similarity index 100%
rename from data/inpainting_examples/6458524847_2f4c361183_k_mask.png
rename to invokeai/assets/data/inpainting_examples/6458524847_2f4c361183_k_mask.png
diff --git a/data/inpainting_examples/8399166846_f6fb4e4b8e_k.png b/invokeai/assets/data/inpainting_examples/8399166846_f6fb4e4b8e_k.png
similarity index 100%
rename from data/inpainting_examples/8399166846_f6fb4e4b8e_k.png
rename to invokeai/assets/data/inpainting_examples/8399166846_f6fb4e4b8e_k.png
diff --git a/data/inpainting_examples/8399166846_f6fb4e4b8e_k_mask.png b/invokeai/assets/data/inpainting_examples/8399166846_f6fb4e4b8e_k_mask.png
similarity index 100%
rename from data/inpainting_examples/8399166846_f6fb4e4b8e_k_mask.png
rename to invokeai/assets/data/inpainting_examples/8399166846_f6fb4e4b8e_k_mask.png
diff --git a/data/inpainting_examples/alex-iby-G_Pk4D9rMLs.png b/invokeai/assets/data/inpainting_examples/alex-iby-G_Pk4D9rMLs.png
similarity index 100%
rename from data/inpainting_examples/alex-iby-G_Pk4D9rMLs.png
rename to invokeai/assets/data/inpainting_examples/alex-iby-G_Pk4D9rMLs.png
diff --git a/data/inpainting_examples/alex-iby-G_Pk4D9rMLs_mask.png b/invokeai/assets/data/inpainting_examples/alex-iby-G_Pk4D9rMLs_mask.png
similarity index 100%
rename from data/inpainting_examples/alex-iby-G_Pk4D9rMLs_mask.png
rename to invokeai/assets/data/inpainting_examples/alex-iby-G_Pk4D9rMLs_mask.png
diff --git a/data/inpainting_examples/bench2.png b/invokeai/assets/data/inpainting_examples/bench2.png
similarity index 100%
rename from data/inpainting_examples/bench2.png
rename to invokeai/assets/data/inpainting_examples/bench2.png
diff --git a/data/inpainting_examples/bench2_mask.png b/invokeai/assets/data/inpainting_examples/bench2_mask.png
similarity index 100%
rename from data/inpainting_examples/bench2_mask.png
rename to invokeai/assets/data/inpainting_examples/bench2_mask.png
diff --git a/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0.png b/invokeai/assets/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0.png
similarity index 100%
rename from data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0.png
rename to invokeai/assets/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0.png
diff --git a/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0_mask.png b/invokeai/assets/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0_mask.png
similarity index 100%
rename from data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0_mask.png
rename to invokeai/assets/data/inpainting_examples/bertrand-gabioud-CpuFzIsHYJ0_mask.png
diff --git a/data/inpainting_examples/billow926-12-Wc-Zgx6Y.png b/invokeai/assets/data/inpainting_examples/billow926-12-Wc-Zgx6Y.png
similarity index 100%
rename from data/inpainting_examples/billow926-12-Wc-Zgx6Y.png
rename to invokeai/assets/data/inpainting_examples/billow926-12-Wc-Zgx6Y.png
diff --git a/data/inpainting_examples/billow926-12-Wc-Zgx6Y_mask.png b/invokeai/assets/data/inpainting_examples/billow926-12-Wc-Zgx6Y_mask.png
similarity index 100%
rename from data/inpainting_examples/billow926-12-Wc-Zgx6Y_mask.png
rename to invokeai/assets/data/inpainting_examples/billow926-12-Wc-Zgx6Y_mask.png
diff --git a/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png b/invokeai/assets/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png
similarity index 100%
rename from data/inpainting_examples/overture-creations-5sI6fQgYIuo.png
rename to invokeai/assets/data/inpainting_examples/overture-creations-5sI6fQgYIuo.png
diff --git a/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png b/invokeai/assets/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png
similarity index 100%
rename from data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png
rename to invokeai/assets/data/inpainting_examples/overture-creations-5sI6fQgYIuo_mask.png
diff --git a/data/inpainting_examples/photo-1583445095369-9c651e7e5d34.png b/invokeai/assets/data/inpainting_examples/photo-1583445095369-9c651e7e5d34.png
similarity index 100%
rename from data/inpainting_examples/photo-1583445095369-9c651e7e5d34.png
rename to invokeai/assets/data/inpainting_examples/photo-1583445095369-9c651e7e5d34.png
diff --git a/data/inpainting_examples/photo-1583445095369-9c651e7e5d34_mask.png b/invokeai/assets/data/inpainting_examples/photo-1583445095369-9c651e7e5d34_mask.png
similarity index 100%
rename from data/inpainting_examples/photo-1583445095369-9c651e7e5d34_mask.png
rename to invokeai/assets/data/inpainting_examples/photo-1583445095369-9c651e7e5d34_mask.png
diff --git a/assets/fire.png b/invokeai/assets/fire.png
similarity index 100%
rename from assets/fire.png
rename to invokeai/assets/fire.png
diff --git a/invokeai/assets/fonts/inter/Inter-Regular.ttf b/invokeai/assets/fonts/inter/Inter-Regular.ttf
new file mode 100755
index 00000000000..012d1b470d9
Binary files /dev/null and b/invokeai/assets/fonts/inter/Inter-Regular.ttf differ
diff --git a/invokeai/assets/fonts/inter/LICENSE.txt b/invokeai/assets/fonts/inter/LICENSE.txt
new file mode 100755
index 00000000000..ff80f8c6156
--- /dev/null
+++ b/invokeai/assets/fonts/inter/LICENSE.txt
@@ -0,0 +1,94 @@
+Copyright (c) 2016-2020 The Inter Project Authors.
+"Inter" is trademark of Rasmus Andersson.
+https://github.com/rsms/inter
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION AND CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/inpainting.png b/invokeai/assets/inpainting.png
similarity index 100%
rename from assets/inpainting.png
rename to invokeai/assets/inpainting.png
diff --git a/assets/modelfigure.png b/invokeai/assets/modelfigure.png
similarity index 100%
rename from assets/modelfigure.png
rename to invokeai/assets/modelfigure.png
diff --git a/assets/rdm-preview.jpg b/invokeai/assets/rdm-preview.jpg
similarity index 100%
rename from assets/rdm-preview.jpg
rename to invokeai/assets/rdm-preview.jpg
diff --git a/assets/reconstruction1.png b/invokeai/assets/reconstruction1.png
similarity index 100%
rename from assets/reconstruction1.png
rename to invokeai/assets/reconstruction1.png
diff --git a/assets/reconstruction2.png b/invokeai/assets/reconstruction2.png
similarity index 100%
rename from assets/reconstruction2.png
rename to invokeai/assets/reconstruction2.png
diff --git a/assets/results.gif b/invokeai/assets/results.gif
similarity index 100%
rename from assets/results.gif
rename to invokeai/assets/results.gif
diff --git a/assets/stable-samples/img2img/mountains-1.png b/invokeai/assets/stable-samples/img2img/mountains-1.png
similarity index 100%
rename from assets/stable-samples/img2img/mountains-1.png
rename to invokeai/assets/stable-samples/img2img/mountains-1.png
diff --git a/assets/stable-samples/img2img/upscaling-in.png b/invokeai/assets/stable-samples/img2img/upscaling-in.png
similarity index 100%
rename from assets/stable-samples/img2img/upscaling-in.png
rename to invokeai/assets/stable-samples/img2img/upscaling-in.png
diff --git a/assets/stable-samples/img2img/upscaling-out.png b/invokeai/assets/stable-samples/img2img/upscaling-out.png
similarity index 100%
rename from assets/stable-samples/img2img/upscaling-out.png
rename to invokeai/assets/stable-samples/img2img/upscaling-out.png
diff --git a/assets/stable-samples/txt2img/000002025.png b/invokeai/assets/stable-samples/txt2img/000002025.png
similarity index 100%
rename from assets/stable-samples/txt2img/000002025.png
rename to invokeai/assets/stable-samples/txt2img/000002025.png
diff --git a/assets/stable-samples/txt2img/000002035.png b/invokeai/assets/stable-samples/txt2img/000002035.png
similarity index 100%
rename from assets/stable-samples/txt2img/000002035.png
rename to invokeai/assets/stable-samples/txt2img/000002035.png
diff --git a/assets/the-earth-is-on-fire,-oil-on-canvas.png b/invokeai/assets/the-earth-is-on-fire,-oil-on-canvas.png
similarity index 100%
rename from assets/the-earth-is-on-fire,-oil-on-canvas.png
rename to invokeai/assets/the-earth-is-on-fire,-oil-on-canvas.png
diff --git a/assets/txt2img-convsample.png b/invokeai/assets/txt2img-convsample.png
similarity index 100%
rename from assets/txt2img-convsample.png
rename to invokeai/assets/txt2img-convsample.png
diff --git a/assets/txt2img-preview.png b/invokeai/assets/txt2img-preview.png
similarity index 100%
rename from assets/txt2img-preview.png
rename to invokeai/assets/txt2img-preview.png
diff --git a/invokeai/backend/__init__.py b/invokeai/backend/__init__.py
new file mode 100644
index 00000000000..9fe97ee525e
--- /dev/null
+++ b/invokeai/backend/__init__.py
@@ -0,0 +1,3 @@
+"""
+Initialization file for invokeai.backend
+"""
diff --git a/invokeai/backend/anima/__init__.py b/invokeai/backend/anima/__init__.py
new file mode 100644
index 00000000000..01a1a952e96
--- /dev/null
+++ b/invokeai/backend/anima/__init__.py
@@ -0,0 +1,6 @@
+"""Anima model backend module.
+
+Anima is a 2B-parameter anime-focused text-to-image model built on NVIDIA's
+Cosmos Predict2 DiT architecture with a custom LLM Adapter that bridges Qwen3
+0.6B text encoder outputs to the DiT backbone.
+"""
diff --git a/invokeai/backend/anima/anima_transformer.py b/invokeai/backend/anima/anima_transformer.py
new file mode 100644
index 00000000000..36c5764e97e
--- /dev/null
+++ b/invokeai/backend/anima/anima_transformer.py
@@ -0,0 +1,1040 @@
+"""Anima transformer model: Cosmos Predict2 MiniTrainDIT + LLM Adapter.
+
+The Anima architecture combines:
+1. MiniTrainDIT: A Cosmos Predict2 DiT backbone with 28 blocks, 2048-dim hidden state,
+ and 3D RoPE positional embeddings.
+2. LLMAdapter: A 6-layer cross-attention transformer that fuses Qwen3 0.6B hidden states
+ with learned T5-XXL token embeddings to produce conditioning for the DiT.
+
+Original source code:
+- MiniTrainDIT backbone and positional embeddings: https://github.com/nvidia-cosmos/cosmos-predict2
+ SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+ SPDX-License-Identifier: Apache-2.0
+- LLMAdapter and Anima wrapper: Clean-room implementation based on
+ https://github.com/hdae/diffusers-anima (Apache-2.0)
+"""
+
+import logging
+import math
+from typing import Optional, Tuple
+
+import torch
+import torch.nn.functional as F
+from einops import rearrange, repeat
+from einops.layers.torch import Rearrange
+from torch import nn
+
+logger = logging.getLogger(__name__)
+
+
+# ============================================================================
+# Positional Embeddings
+# Original source: https://github.com/nvidia-cosmos/cosmos-predict2
+# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. Apache-2.0
+# ============================================================================
+
+
+class VideoRopePosition3DEmb(nn.Module):
+ """3D Rotary Position Embedding for video/image transformers.
+
+ Generates rotary embeddings with separate frequency components for
+ height, width, and temporal dimensions.
+ """
+
+ def __init__(
+ self,
+ *,
+ head_dim: int,
+ len_h: int,
+ len_w: int,
+ len_t: int,
+ base_fps: int = 24,
+ h_extrapolation_ratio: float = 1.0,
+ w_extrapolation_ratio: float = 1.0,
+ t_extrapolation_ratio: float = 1.0,
+ enable_fps_modulation: bool = True,
+ device: Optional[torch.device] = None,
+ **kwargs,
+ ):
+ super().__init__()
+ self.base_fps = base_fps
+ self.max_h = len_h
+ self.max_w = len_w
+ self.enable_fps_modulation = enable_fps_modulation
+
+ dim = head_dim
+ dim_h = dim // 6 * 2
+ dim_w = dim_h
+ dim_t = dim - 2 * dim_h
+ assert dim == dim_h + dim_w + dim_t, f"bad dim: {dim} != {dim_h} + {dim_w} + {dim_t}"
+
+ self.register_buffer(
+ "dim_spatial_range",
+ torch.arange(0, dim_h, 2, device=device)[: (dim_h // 2)].float() / dim_h,
+ persistent=False,
+ )
+ self.register_buffer(
+ "dim_temporal_range",
+ torch.arange(0, dim_t, 2, device=device)[: (dim_t // 2)].float() / dim_t,
+ persistent=False,
+ )
+
+ self.h_ntk_factor = h_extrapolation_ratio ** (dim_h / (dim_h - 2))
+ self.w_ntk_factor = w_extrapolation_ratio ** (dim_w / (dim_w - 2))
+ self.t_ntk_factor = t_extrapolation_ratio ** (dim_t / (dim_t - 2))
+
+ def forward(
+ self,
+ x_B_T_H_W_C: torch.Tensor,
+ fps: Optional[torch.Tensor] = None,
+ device: Optional[torch.device] = None,
+ ) -> torch.Tensor:
+ return self.generate_embeddings(x_B_T_H_W_C.shape, fps=fps, device=device)
+
+ def generate_embeddings(
+ self,
+ B_T_H_W_C: torch.Size,
+ fps: Optional[torch.Tensor] = None,
+ device: Optional[torch.device] = None,
+ dtype: Optional[torch.dtype] = None,
+ ) -> torch.Tensor:
+ h_theta = 10000.0 * self.h_ntk_factor
+ w_theta = 10000.0 * self.w_ntk_factor
+ t_theta = 10000.0 * self.t_ntk_factor
+
+ h_spatial_freqs = 1.0 / (h_theta ** self.dim_spatial_range.to(device=device))
+ w_spatial_freqs = 1.0 / (w_theta ** self.dim_spatial_range.to(device=device))
+ temporal_freqs = 1.0 / (t_theta ** self.dim_temporal_range.to(device=device))
+
+ B, T, H, W, _ = B_T_H_W_C
+ seq = torch.arange(max(H, W, T), dtype=torch.float, device=device)
+
+ half_emb_h = torch.outer(seq[:H].to(device=device), h_spatial_freqs)
+ half_emb_w = torch.outer(seq[:W].to(device=device), w_spatial_freqs)
+
+ if fps is None or self.enable_fps_modulation is False:
+ half_emb_t = torch.outer(seq[:T].to(device=device), temporal_freqs)
+ else:
+ half_emb_t = torch.outer(seq[:T].to(device=device) / fps * self.base_fps, temporal_freqs)
+
+ half_emb_h = torch.stack(
+ [torch.cos(half_emb_h), -torch.sin(half_emb_h), torch.sin(half_emb_h), torch.cos(half_emb_h)], dim=-1
+ )
+ half_emb_w = torch.stack(
+ [torch.cos(half_emb_w), -torch.sin(half_emb_w), torch.sin(half_emb_w), torch.cos(half_emb_w)], dim=-1
+ )
+ half_emb_t = torch.stack(
+ [torch.cos(half_emb_t), -torch.sin(half_emb_t), torch.sin(half_emb_t), torch.cos(half_emb_t)], dim=-1
+ )
+
+ em_T_H_W_D = torch.cat(
+ [
+ repeat(half_emb_t, "t d x -> t h w d x", h=H, w=W),
+ repeat(half_emb_h, "h d x -> t h w d x", t=T, w=W),
+ repeat(half_emb_w, "w d x -> t h w d x", t=T, h=H),
+ ],
+ dim=-2,
+ )
+
+ return rearrange(em_T_H_W_D, "t h w d (i j) -> (t h w) d i j", i=2, j=2).float()
+
+
+def _normalize(x: torch.Tensor, dim: Optional[list[int]] = None, eps: float = 0) -> torch.Tensor:
+ if dim is None:
+ dim = list(range(1, x.ndim))
+ norm = torch.linalg.vector_norm(x, dim=dim, keepdim=True, dtype=torch.float32)
+ norm = torch.add(eps, norm, alpha=math.sqrt(norm.numel() / x.numel()))
+ return x / norm.to(x.dtype)
+
+
+class LearnablePosEmbAxis(nn.Module):
+ """Learnable per-axis positional embeddings."""
+
+ def __init__(
+ self,
+ *,
+ model_channels: int,
+ len_h: int,
+ len_w: int,
+ len_t: int,
+ device: Optional[torch.device] = None,
+ dtype: Optional[torch.dtype] = None,
+ **kwargs,
+ ):
+ super().__init__()
+ self.pos_emb_h = nn.Parameter(torch.empty(len_h, model_channels, device=device, dtype=dtype))
+ self.pos_emb_w = nn.Parameter(torch.empty(len_w, model_channels, device=device, dtype=dtype))
+ self.pos_emb_t = nn.Parameter(torch.empty(len_t, model_channels, device=device, dtype=dtype))
+
+ def forward(
+ self,
+ x_B_T_H_W_C: torch.Tensor,
+ fps: Optional[torch.Tensor] = None,
+ device: Optional[torch.device] = None,
+ dtype: Optional[torch.dtype] = None,
+ ) -> torch.Tensor:
+ return self.generate_embeddings(x_B_T_H_W_C.shape, device=device, dtype=dtype)
+
+ def generate_embeddings(
+ self,
+ B_T_H_W_C: torch.Size,
+ fps: Optional[torch.Tensor] = None,
+ device: Optional[torch.device] = None,
+ dtype: Optional[torch.dtype] = None,
+ ) -> torch.Tensor:
+ B, T, H, W, _ = B_T_H_W_C
+ emb_h_H = self.pos_emb_h[:H].to(device=device, dtype=dtype)
+ emb_w_W = self.pos_emb_w[:W].to(device=device, dtype=dtype)
+ emb_t_T = self.pos_emb_t[:T].to(device=device, dtype=dtype)
+ emb = (
+ repeat(emb_t_T, "t d -> b t h w d", b=B, h=H, w=W)
+ + repeat(emb_h_H, "h d -> b t h w d", b=B, t=T, w=W)
+ + repeat(emb_w_W, "w d -> b t h w d", b=B, t=T, h=H)
+ )
+ return _normalize(emb, dim=-1, eps=1e-6)
+
+
+# ============================================================================
+# Cosmos Predict2 MiniTrainDIT
+# Original source: https://github.com/nvidia-cosmos/cosmos-predict2
+# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. Apache-2.0
+# ============================================================================
+
+
+def apply_rotary_pos_emb_cosmos(t: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
+ """Apply rotary position embeddings in Cosmos format (2x2 rotation matrices)."""
+ t_ = t.reshape(*t.shape[:-1], 2, -1).movedim(-2, -1).unsqueeze(-2).float()
+ t_out = freqs[..., 0] * t_[..., 0] + freqs[..., 1] * t_[..., 1]
+ t_out = t_out.movedim(-1, -2).reshape(*t.shape).type_as(t)
+ return t_out
+
+
+class GPT2FeedForward(nn.Module):
+ def __init__(self, d_model: int, d_ff: int) -> None:
+ super().__init__()
+ self.activation = nn.GELU()
+ self.layer1 = nn.Linear(d_model, d_ff, bias=False)
+ self.layer2 = nn.Linear(d_ff, d_model, bias=False)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ return self.layer2(self.activation(self.layer1(x)))
+
+
+class CosmosAttention(nn.Module):
+ """Multi-head attention for the Cosmos DiT backbone.
+
+ Supports both self-attention and cross-attention with QK normalization
+ and rotary position embeddings.
+ """
+
+ def __init__(
+ self,
+ query_dim: int,
+ context_dim: Optional[int] = None,
+ n_heads: int = 8,
+ head_dim: int = 64,
+ dropout: float = 0.0,
+ ) -> None:
+ super().__init__()
+ self.is_selfattn = context_dim is None
+ context_dim = query_dim if context_dim is None else context_dim
+ inner_dim = head_dim * n_heads
+
+ self.n_heads = n_heads
+ self.head_dim = head_dim
+
+ self.q_proj = nn.Linear(query_dim, inner_dim, bias=False)
+ self.q_norm = nn.RMSNorm(head_dim, eps=1e-6)
+
+ self.k_proj = nn.Linear(context_dim, inner_dim, bias=False)
+ self.k_norm = nn.RMSNorm(head_dim, eps=1e-6)
+
+ self.v_proj = nn.Linear(context_dim, inner_dim, bias=False)
+ self.v_norm = nn.Identity()
+
+ self.output_proj = nn.Linear(inner_dim, query_dim, bias=False)
+ self.output_dropout = nn.Dropout(dropout) if dropout > 1e-4 else nn.Identity()
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ context: Optional[torch.Tensor] = None,
+ rope_emb: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ q = self.q_proj(x)
+ context = x if context is None else context
+ k = self.k_proj(context)
+ v = self.v_proj(context)
+ q, k, v = (rearrange(t, "b ... (h d) -> b ... h d", h=self.n_heads, d=self.head_dim) for t in (q, k, v))
+
+ q = self.q_norm(q)
+ k = self.k_norm(k)
+ v = self.v_norm(v)
+
+ if self.is_selfattn and rope_emb is not None:
+ q = apply_rotary_pos_emb_cosmos(q, rope_emb)
+ k = apply_rotary_pos_emb_cosmos(k, rope_emb)
+
+ # Reshape for scaled_dot_product_attention: (B, heads, seq, dim)
+ in_q_shape = q.shape
+ in_k_shape = k.shape
+ q = rearrange(q, "b ... h d -> b h ... d").reshape(in_q_shape[0], in_q_shape[-2], -1, in_q_shape[-1])
+ k = rearrange(k, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
+ v = rearrange(v, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
+
+ result = F.scaled_dot_product_attention(q, k, v)
+ result = rearrange(result, "b h s d -> b s (h d)")
+ return self.output_dropout(self.output_proj(result))
+
+
+class Timesteps(nn.Module):
+ """Sinusoidal timestep embeddings."""
+
+ def __init__(self, num_channels: int):
+ super().__init__()
+ self.num_channels = num_channels
+
+ def forward(self, timesteps_B_T: torch.Tensor) -> torch.Tensor:
+ assert timesteps_B_T.ndim == 2
+ timesteps = timesteps_B_T.flatten().float()
+ half_dim = self.num_channels // 2
+ exponent = -math.log(10000) * torch.arange(half_dim, dtype=torch.float32, device=timesteps.device) / half_dim
+ emb = timesteps[:, None].float() * torch.exp(exponent)[None, :]
+ emb = torch.cat([torch.cos(emb), torch.sin(emb)], dim=-1)
+ return rearrange(emb, "(b t) d -> b t d", b=timesteps_B_T.shape[0], t=timesteps_B_T.shape[1])
+
+
+class TimestepEmbedding(nn.Module):
+ """Projects sinusoidal timestep embeddings to model dimension."""
+
+ def __init__(self, in_features: int, out_features: int, use_adaln_lora: bool = False):
+ super().__init__()
+ self.use_adaln_lora = use_adaln_lora
+ self.linear_1 = nn.Linear(in_features, out_features, bias=not use_adaln_lora)
+ self.activation = nn.SiLU()
+ if use_adaln_lora:
+ self.linear_2 = nn.Linear(out_features, 3 * out_features, bias=False)
+ else:
+ self.linear_2 = nn.Linear(out_features, out_features, bias=False)
+
+ def forward(self, sample: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
+ emb = self.linear_2(self.activation(self.linear_1(sample)))
+ if self.use_adaln_lora:
+ return sample, emb
+ return emb, None
+
+
+class PatchEmbed(nn.Module):
+ """Patchify input tensor via rearrange + linear projection."""
+
+ def __init__(
+ self,
+ spatial_patch_size: int,
+ temporal_patch_size: int,
+ in_channels: int = 3,
+ out_channels: int = 768,
+ ):
+ super().__init__()
+ self.spatial_patch_size = spatial_patch_size
+ self.temporal_patch_size = temporal_patch_size
+ self.proj = nn.Sequential(
+ Rearrange(
+ "b c (t r) (h m) (w n) -> b t h w (c r m n)",
+ r=temporal_patch_size,
+ m=spatial_patch_size,
+ n=spatial_patch_size,
+ ),
+ nn.Linear(
+ in_channels * spatial_patch_size * spatial_patch_size * temporal_patch_size,
+ out_channels,
+ bias=False,
+ ),
+ )
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ assert x.dim() == 5
+ return self.proj(x)
+
+
+class FinalLayer(nn.Module):
+ """Final AdaLN-modulated output projection."""
+
+ def __init__(
+ self,
+ hidden_size: int,
+ spatial_patch_size: int,
+ temporal_patch_size: int,
+ out_channels: int,
+ use_adaln_lora: bool = False,
+ adaln_lora_dim: int = 256,
+ ):
+ super().__init__()
+ self.layer_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
+ self.linear = nn.Linear(
+ hidden_size, spatial_patch_size * spatial_patch_size * temporal_patch_size * out_channels, bias=False
+ )
+ self.hidden_size = hidden_size
+ self.use_adaln_lora = use_adaln_lora
+
+ if use_adaln_lora:
+ self.adaln_modulation = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(hidden_size, adaln_lora_dim, bias=False),
+ nn.Linear(adaln_lora_dim, 2 * hidden_size, bias=False),
+ )
+ else:
+ self.adaln_modulation = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(hidden_size, 2 * hidden_size, bias=False),
+ )
+
+ def forward(
+ self,
+ x_B_T_H_W_D: torch.Tensor,
+ emb_B_T_D: torch.Tensor,
+ adaln_lora_B_T_3D: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ if self.use_adaln_lora:
+ assert adaln_lora_B_T_3D is not None
+ shift, scale = (self.adaln_modulation(emb_B_T_D) + adaln_lora_B_T_3D[:, :, : 2 * self.hidden_size]).chunk(
+ 2, dim=-1
+ )
+ else:
+ shift, scale = self.adaln_modulation(emb_B_T_D).chunk(2, dim=-1)
+
+ shift = rearrange(shift, "b t d -> b t 1 1 d")
+ scale = rearrange(scale, "b t d -> b t 1 1 d")
+
+ x_B_T_H_W_D = self.layer_norm(x_B_T_H_W_D) * (1 + scale) + shift
+ return self.linear(x_B_T_H_W_D)
+
+
+class DiTBlock(nn.Module):
+ """Cosmos DiT transformer block with self-attention, cross-attention, and MLP.
+
+ Each component uses AdaLN (Adaptive Layer Normalization) modulation from
+ the timestep embedding.
+ """
+
+ def __init__(
+ self,
+ x_dim: int,
+ context_dim: int,
+ num_heads: int,
+ mlp_ratio: float = 4.0,
+ use_adaln_lora: bool = False,
+ adaln_lora_dim: int = 256,
+ ):
+ super().__init__()
+ self.x_dim = x_dim
+ self.use_adaln_lora = use_adaln_lora
+
+ self.layer_norm_self_attn = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
+ self.self_attn = CosmosAttention(x_dim, None, num_heads, x_dim // num_heads)
+
+ self.layer_norm_cross_attn = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
+ self.cross_attn = CosmosAttention(x_dim, context_dim, num_heads, x_dim // num_heads)
+
+ self.layer_norm_mlp = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
+ self.mlp = GPT2FeedForward(x_dim, int(x_dim * mlp_ratio))
+
+ # AdaLN modulation layers (shift, scale, gate for each of 3 components)
+ if use_adaln_lora:
+ self.adaln_modulation_self_attn = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(x_dim, adaln_lora_dim, bias=False),
+ nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
+ )
+ self.adaln_modulation_cross_attn = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(x_dim, adaln_lora_dim, bias=False),
+ nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
+ )
+ self.adaln_modulation_mlp = nn.Sequential(
+ nn.SiLU(),
+ nn.Linear(x_dim, adaln_lora_dim, bias=False),
+ nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
+ )
+ else:
+ self.adaln_modulation_self_attn = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
+ self.adaln_modulation_cross_attn = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
+ self.adaln_modulation_mlp = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
+
+ def forward(
+ self,
+ x_B_T_H_W_D: torch.Tensor,
+ emb_B_T_D: torch.Tensor,
+ crossattn_emb: torch.Tensor,
+ rope_emb_L_1_1_D: Optional[torch.Tensor] = None,
+ adaln_lora_B_T_3D: Optional[torch.Tensor] = None,
+ extra_per_block_pos_emb: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ residual_dtype = x_B_T_H_W_D.dtype
+ compute_dtype = emb_B_T_D.dtype
+
+ if extra_per_block_pos_emb is not None:
+ x_B_T_H_W_D = x_B_T_H_W_D + extra_per_block_pos_emb
+
+ # Compute AdaLN modulations
+ if self.use_adaln_lora:
+ assert adaln_lora_B_T_3D is not None
+ shift_sa, scale_sa, gate_sa = (self.adaln_modulation_self_attn(emb_B_T_D) + adaln_lora_B_T_3D).chunk(
+ 3, dim=-1
+ )
+ shift_ca, scale_ca, gate_ca = (self.adaln_modulation_cross_attn(emb_B_T_D) + adaln_lora_B_T_3D).chunk(
+ 3, dim=-1
+ )
+ shift_mlp, scale_mlp, gate_mlp = (self.adaln_modulation_mlp(emb_B_T_D) + adaln_lora_B_T_3D).chunk(3, dim=-1)
+ else:
+ shift_sa, scale_sa, gate_sa = self.adaln_modulation_self_attn(emb_B_T_D).chunk(3, dim=-1)
+ shift_ca, scale_ca, gate_ca = self.adaln_modulation_cross_attn(emb_B_T_D).chunk(3, dim=-1)
+ shift_mlp, scale_mlp, gate_mlp = self.adaln_modulation_mlp(emb_B_T_D).chunk(3, dim=-1)
+
+ # Reshape for broadcasting: (B, T, D) -> (B, T, 1, 1, D)
+ shift_sa, scale_sa, gate_sa = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_sa, scale_sa, gate_sa))
+ shift_ca, scale_ca, gate_ca = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_ca, scale_ca, gate_ca))
+ shift_mlp, scale_mlp, gate_mlp = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_mlp, scale_mlp, gate_mlp))
+
+ B, T, H, W, D = x_B_T_H_W_D.shape
+
+ def _adaln(x: torch.Tensor, norm: nn.Module, scale: torch.Tensor, shift: torch.Tensor) -> torch.Tensor:
+ return norm(x) * (1 + scale) + shift
+
+ # Self-attention
+ normed = _adaln(x_B_T_H_W_D, self.layer_norm_self_attn, scale_sa, shift_sa)
+ result = rearrange(
+ self.self_attn(
+ rearrange(normed.to(compute_dtype), "b t h w d -> b (t h w) d"), None, rope_emb=rope_emb_L_1_1_D
+ ),
+ "b (t h w) d -> b t h w d",
+ t=T,
+ h=H,
+ w=W,
+ )
+ x_B_T_H_W_D = x_B_T_H_W_D + gate_sa.to(residual_dtype) * result.to(residual_dtype)
+
+ # Cross-attention
+ normed = _adaln(x_B_T_H_W_D, self.layer_norm_cross_attn, scale_ca, shift_ca)
+ result = rearrange(
+ self.cross_attn(
+ rearrange(normed.to(compute_dtype), "b t h w d -> b (t h w) d"),
+ crossattn_emb,
+ rope_emb=rope_emb_L_1_1_D,
+ ),
+ "b (t h w) d -> b t h w d",
+ t=T,
+ h=H,
+ w=W,
+ )
+ x_B_T_H_W_D = result.to(residual_dtype) * gate_ca.to(residual_dtype) + x_B_T_H_W_D
+
+ # MLP
+ normed = _adaln(x_B_T_H_W_D, self.layer_norm_mlp, scale_mlp, shift_mlp)
+ result = self.mlp(normed.to(compute_dtype))
+ x_B_T_H_W_D = x_B_T_H_W_D + gate_mlp.to(residual_dtype) * result.to(residual_dtype)
+
+ return x_B_T_H_W_D
+
+
+class MiniTrainDIT(nn.Module):
+ """Cosmos Predict2 DiT backbone for video/image generation.
+
+ This is the core transformer architecture that Anima extends. It processes
+ 3D latent tensors (B, C, T, H, W) with patch embedding, positional encoding,
+ and adaptive layer normalization.
+
+ Args:
+ max_img_h: Maximum image height in pixels.
+ max_img_w: Maximum image width in pixels.
+ max_frames: Maximum number of video frames.
+ in_channels: Number of input latent channels.
+ out_channels: Number of output channels.
+ patch_spatial: Spatial patch size.
+ patch_temporal: Temporal patch size.
+ concat_padding_mask: Whether to concatenate a padding mask channel.
+ model_channels: Hidden dimension of the transformer.
+ num_blocks: Number of DiT blocks.
+ num_heads: Number of attention heads.
+ mlp_ratio: MLP expansion ratio.
+ crossattn_emb_channels: Cross-attention context dimension.
+ use_adaln_lora: Whether to use AdaLN-LoRA.
+ adaln_lora_dim: AdaLN-LoRA bottleneck dimension.
+ extra_per_block_abs_pos_emb: Whether to use extra learnable positional embeddings.
+ """
+
+ def __init__(
+ self,
+ max_img_h: int = 240,
+ max_img_w: int = 240,
+ max_frames: int = 1,
+ in_channels: int = 16,
+ out_channels: int = 16,
+ patch_spatial: int = 2,
+ patch_temporal: int = 1,
+ concat_padding_mask: bool = True,
+ model_channels: int = 2048,
+ num_blocks: int = 28,
+ num_heads: int = 16,
+ mlp_ratio: float = 4.0,
+ crossattn_emb_channels: int = 1024,
+ pos_emb_cls: str = "rope3d",
+ pos_emb_learnable: bool = False,
+ pos_emb_interpolation: str = "crop",
+ min_fps: int = 1,
+ max_fps: int = 30,
+ use_adaln_lora: bool = False,
+ adaln_lora_dim: int = 256,
+ rope_h_extrapolation_ratio: float = 1.0,
+ rope_w_extrapolation_ratio: float = 1.0,
+ rope_t_extrapolation_ratio: float = 1.0,
+ extra_per_block_abs_pos_emb: bool = False,
+ extra_h_extrapolation_ratio: float = 1.0,
+ extra_w_extrapolation_ratio: float = 1.0,
+ extra_t_extrapolation_ratio: float = 1.0,
+ rope_enable_fps_modulation: bool = True,
+ image_model: Optional[str] = None,
+ ) -> None:
+ super().__init__()
+ self.max_img_h = max_img_h
+ self.max_img_w = max_img_w
+ self.max_frames = max_frames
+ self.in_channels = in_channels
+ self.out_channels = out_channels
+ self.patch_spatial = patch_spatial
+ self.patch_temporal = patch_temporal
+ self.num_heads = num_heads
+ self.num_blocks = num_blocks
+ self.model_channels = model_channels
+ self.concat_padding_mask = concat_padding_mask
+ self.pos_emb_cls = pos_emb_cls
+ self.extra_per_block_abs_pos_emb = extra_per_block_abs_pos_emb
+
+ # Positional embeddings
+ self.pos_embedder = VideoRopePosition3DEmb(
+ head_dim=model_channels // num_heads,
+ len_h=max_img_h // patch_spatial,
+ len_w=max_img_w // patch_spatial,
+ len_t=max_frames // patch_temporal,
+ max_fps=max_fps,
+ min_fps=min_fps,
+ h_extrapolation_ratio=rope_h_extrapolation_ratio,
+ w_extrapolation_ratio=rope_w_extrapolation_ratio,
+ t_extrapolation_ratio=rope_t_extrapolation_ratio,
+ enable_fps_modulation=rope_enable_fps_modulation,
+ )
+
+ if extra_per_block_abs_pos_emb:
+ self.extra_pos_embedder = LearnablePosEmbAxis(
+ model_channels=model_channels,
+ len_h=max_img_h // patch_spatial,
+ len_w=max_img_w // patch_spatial,
+ len_t=max_frames // patch_temporal,
+ )
+
+ self.use_adaln_lora = use_adaln_lora
+ self.adaln_lora_dim = adaln_lora_dim
+
+ # Timestep embedding
+ self.t_embedder = nn.Sequential(
+ Timesteps(model_channels),
+ TimestepEmbedding(model_channels, model_channels, use_adaln_lora=use_adaln_lora),
+ )
+ self.t_embedding_norm = nn.RMSNorm(model_channels, eps=1e-6)
+
+ # Patch embedding
+ embed_in_channels = in_channels + 1 if concat_padding_mask else in_channels
+ self.x_embedder = PatchEmbed(
+ spatial_patch_size=patch_spatial,
+ temporal_patch_size=patch_temporal,
+ in_channels=embed_in_channels,
+ out_channels=model_channels,
+ )
+
+ # Transformer blocks
+ self.blocks = nn.ModuleList(
+ [
+ DiTBlock(
+ x_dim=model_channels,
+ context_dim=crossattn_emb_channels,
+ num_heads=num_heads,
+ mlp_ratio=mlp_ratio,
+ use_adaln_lora=use_adaln_lora,
+ adaln_lora_dim=adaln_lora_dim,
+ )
+ for _ in range(num_blocks)
+ ]
+ )
+
+ # Final output layer
+ self.final_layer = FinalLayer(
+ hidden_size=model_channels,
+ spatial_patch_size=patch_spatial,
+ temporal_patch_size=patch_temporal,
+ out_channels=out_channels,
+ use_adaln_lora=use_adaln_lora,
+ adaln_lora_dim=adaln_lora_dim,
+ )
+
+ def _pad_to_patch_size(self, x: torch.Tensor) -> torch.Tensor:
+ """Pad input tensor so dimensions are divisible by patch sizes."""
+ _, _, T, H, W = x.shape
+ pad_t = (self.patch_temporal - T % self.patch_temporal) % self.patch_temporal
+ pad_h = (self.patch_spatial - H % self.patch_spatial) % self.patch_spatial
+ pad_w = (self.patch_spatial - W % self.patch_spatial) % self.patch_spatial
+ if pad_t > 0 or pad_h > 0 or pad_w > 0:
+ x = F.pad(x, (0, pad_w, 0, pad_h, 0, pad_t))
+ return x
+
+ def prepare_embedded_sequence(
+ self,
+ x_B_C_T_H_W: torch.Tensor,
+ fps: Optional[torch.Tensor] = None,
+ padding_mask: Optional[torch.Tensor] = None,
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
+ if self.concat_padding_mask:
+ if padding_mask is None:
+ padding_mask = torch.zeros(
+ x_B_C_T_H_W.shape[0],
+ 1,
+ x_B_C_T_H_W.shape[3],
+ x_B_C_T_H_W.shape[4],
+ dtype=x_B_C_T_H_W.dtype,
+ device=x_B_C_T_H_W.device,
+ )
+ x_B_C_T_H_W = torch.cat(
+ [x_B_C_T_H_W, padding_mask.unsqueeze(1).repeat(1, 1, x_B_C_T_H_W.shape[2], 1, 1)], dim=1
+ )
+
+ x_B_T_H_W_D = self.x_embedder(x_B_C_T_H_W)
+
+ extra_pos_emb = None
+ if self.extra_per_block_abs_pos_emb:
+ extra_pos_emb = self.extra_pos_embedder(
+ x_B_T_H_W_D, fps=fps, device=x_B_C_T_H_W.device, dtype=x_B_C_T_H_W.dtype
+ )
+
+ if "rope" in self.pos_emb_cls.lower():
+ return x_B_T_H_W_D, self.pos_embedder(x_B_T_H_W_D, fps=fps, device=x_B_C_T_H_W.device), extra_pos_emb
+
+ return x_B_T_H_W_D, None, extra_pos_emb
+
+ def unpatchify(self, x_B_T_H_W_M: torch.Tensor) -> torch.Tensor:
+ return rearrange(
+ x_B_T_H_W_M,
+ "B T H W (p1 p2 t C) -> B C (T t) (H p1) (W p2)",
+ p1=self.patch_spatial,
+ p2=self.patch_spatial,
+ t=self.patch_temporal,
+ )
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ timesteps: torch.Tensor,
+ context: torch.Tensor,
+ fps: Optional[torch.Tensor] = None,
+ padding_mask: Optional[torch.Tensor] = None,
+ **kwargs,
+ ) -> torch.Tensor:
+ orig_shape = list(x.shape)
+ x = self._pad_to_patch_size(x)
+
+ x_B_T_H_W_D, rope_emb_L_1_1_D, extra_pos_emb = self.prepare_embedded_sequence(
+ x, fps=fps, padding_mask=padding_mask
+ )
+
+ if timesteps.ndim == 1:
+ timesteps = timesteps.unsqueeze(1)
+ t_emb, adaln_lora = self.t_embedder[1](self.t_embedder[0](timesteps).to(x_B_T_H_W_D.dtype))
+ t_emb = self.t_embedding_norm(t_emb)
+
+ block_kwargs = {
+ "rope_emb_L_1_1_D": rope_emb_L_1_1_D.unsqueeze(1).unsqueeze(0) if rope_emb_L_1_1_D is not None else None,
+ "adaln_lora_B_T_3D": adaln_lora,
+ "extra_per_block_pos_emb": extra_pos_emb,
+ }
+
+ # Keep residual stream in fp32 for numerical stability with fp16 compute
+ if x_B_T_H_W_D.dtype == torch.float16:
+ x_B_T_H_W_D = x_B_T_H_W_D.float()
+
+ for block in self.blocks:
+ x_B_T_H_W_D = block(x_B_T_H_W_D, t_emb, context, **block_kwargs)
+
+ x_out = self.final_layer(x_B_T_H_W_D.to(context.dtype), t_emb, adaln_lora_B_T_3D=adaln_lora)
+ x_out = self.unpatchify(x_out)[:, :, : orig_shape[-3], : orig_shape[-2], : orig_shape[-1]]
+ return x_out
+
+
+# ============================================================================
+# LLM Adapter
+# Reference implementation: https://github.com/hdae/diffusers-anima
+# SPDX-License-Identifier: Apache-2.0
+# ============================================================================
+
+
+def _rotate_half(x: torch.Tensor) -> torch.Tensor:
+ """Split the last dimension in half and negate-swap: [-x2, x1]."""
+ half = x.shape[-1] // 2
+ first, second = x[..., :half], x[..., half:]
+ return torch.cat((-second, first), dim=-1)
+
+
+def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
+ """Apply rotary position embeddings to tensor x given precomputed cos/sin."""
+ return (x * cos.unsqueeze(1)) + (_rotate_half(x) * sin.unsqueeze(1))
+
+
+class LLMAdapterRotaryEmbedding(nn.Module):
+ """Rotary position embedding for the LLM Adapter's attention layers."""
+
+ def __init__(self, head_dim: int, theta: float = 10000.0):
+ super().__init__()
+ half_dim = head_dim // 2
+ index = torch.arange(half_dim, dtype=torch.float32)
+ exponent = (2.0 / float(head_dim)) * index
+ inv_freq = torch.reciprocal(torch.pow(torch.tensor(theta, dtype=torch.float32), exponent))
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
+
+ def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+ pos = position_ids.to(device=x.device, dtype=torch.float32)
+ inv = self.inv_freq.to(device=x.device, dtype=torch.float32)
+ freqs = torch.einsum("bl,d->bld", pos, inv)
+ emb = freqs.repeat(1, 1, 2)
+ return emb.cos().to(dtype=x.dtype), emb.sin().to(dtype=x.dtype)
+
+
+class LLMAdapterAttention(nn.Module):
+ """Attention for the LLM Adapter with QK normalization and rotary position embeddings."""
+
+ def __init__(self, query_dim: int, context_dim: int, n_heads: int, head_dim: int):
+ super().__init__()
+ inner_dim = head_dim * n_heads
+ self.n_heads = n_heads
+ self.head_dim = head_dim
+
+ self.q_proj = nn.Linear(query_dim, inner_dim, bias=False)
+ self.q_norm = nn.RMSNorm(head_dim, eps=1e-6)
+ self.k_proj = nn.Linear(context_dim, inner_dim, bias=False)
+ self.k_norm = nn.RMSNorm(head_dim, eps=1e-6)
+ self.v_proj = nn.Linear(context_dim, inner_dim, bias=False)
+ self.o_proj = nn.Linear(inner_dim, query_dim, bias=False)
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ *,
+ context: Optional[torch.Tensor] = None,
+ attn_mask: Optional[torch.Tensor] = None,
+ pos_q: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
+ pos_k: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
+ ) -> torch.Tensor:
+ context = x if context is None else context
+
+ q = self.q_proj(x).view(x.shape[0], x.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
+ k = self.k_proj(context).view(context.shape[0], context.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
+ v = self.v_proj(context).view(context.shape[0], context.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
+
+ q = self.q_norm(q)
+ k = self.k_norm(k)
+
+ if pos_q is not None and pos_k is not None:
+ q = _apply_rope(q, *pos_q)
+ k = _apply_rope(k, *pos_k)
+
+ y = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
+ y = y.transpose(1, 2).reshape(x.shape[0], x.shape[1], -1).contiguous()
+ return self.o_proj(y)
+
+
+class LLMAdapterTransformerBlock(nn.Module):
+ """Single transformer block in the LLM Adapter.
+
+ Each block contains self-attention, cross-attention, and MLP with
+ RMSNorm pre-normalization.
+ """
+
+ def __init__(
+ self,
+ source_dim: int,
+ model_dim: int,
+ num_heads: int = 16,
+ ):
+ super().__init__()
+ head_dim = model_dim // num_heads
+
+ self.norm_self_attn = nn.RMSNorm(model_dim, eps=1e-6)
+ self.self_attn = LLMAdapterAttention(model_dim, model_dim, num_heads, head_dim)
+
+ self.norm_cross_attn = nn.RMSNorm(model_dim, eps=1e-6)
+ self.cross_attn = LLMAdapterAttention(model_dim, source_dim, num_heads, head_dim)
+
+ self.norm_mlp = nn.RMSNorm(model_dim, eps=1e-6)
+ self.mlp = nn.Sequential(
+ nn.Linear(model_dim, model_dim * 4),
+ nn.GELU(),
+ nn.Linear(model_dim * 4, model_dim),
+ )
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ *,
+ context: torch.Tensor,
+ target_mask: Optional[torch.Tensor] = None,
+ source_mask: Optional[torch.Tensor] = None,
+ pos_target: Tuple[torch.Tensor, torch.Tensor],
+ pos_source: Tuple[torch.Tensor, torch.Tensor],
+ ) -> torch.Tensor:
+ x = x + self.self_attn(
+ self.norm_self_attn(x),
+ attn_mask=target_mask,
+ pos_q=pos_target,
+ pos_k=pos_target,
+ )
+ x = x + self.cross_attn(
+ self.norm_cross_attn(x),
+ context=context,
+ attn_mask=source_mask,
+ pos_q=pos_target,
+ pos_k=pos_source,
+ )
+ x = x + self.mlp(self.norm_mlp(x))
+ return x
+
+
+class LLMAdapter(nn.Module):
+ """LLM Adapter: bridges Qwen3 hidden states and T5-XXL token embeddings.
+
+ Takes Qwen3 hidden states and T5-XXL token IDs, produces conditioning
+ embeddings for the Cosmos DiT via cross-attention through 6 transformer layers.
+
+ Args:
+ vocab_size: Size of the T5 token vocabulary.
+ dim: Model dimension (used for embeddings, projections, and all layers).
+ num_layers: Number of transformer layers.
+ num_heads: Number of attention heads.
+ """
+
+ def __init__(
+ self,
+ vocab_size: int = 32128,
+ dim: int = 1024,
+ num_layers: int = 6,
+ num_heads: int = 16,
+ ):
+ super().__init__()
+ self.embed = nn.Embedding(vocab_size, dim)
+ self.blocks = nn.ModuleList(
+ [LLMAdapterTransformerBlock(source_dim=dim, model_dim=dim, num_heads=num_heads) for _ in range(num_layers)]
+ )
+ self.out_proj = nn.Linear(dim, dim)
+ self.norm = nn.RMSNorm(dim, eps=1e-6)
+ self.rotary_emb = LLMAdapterRotaryEmbedding(dim // num_heads)
+
+ def forward(
+ self,
+ source_hidden_states: torch.Tensor,
+ target_input_ids: torch.Tensor,
+ target_attention_mask: Optional[torch.Tensor] = None,
+ source_attention_mask: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ # Expand attention masks for multi-head attention
+ if target_attention_mask is not None:
+ target_attention_mask = target_attention_mask.to(torch.bool)
+ if target_attention_mask.ndim == 2:
+ target_attention_mask = target_attention_mask[:, None, None, :]
+
+ if source_attention_mask is not None:
+ source_attention_mask = source_attention_mask.to(torch.bool)
+ if source_attention_mask.ndim == 2:
+ source_attention_mask = source_attention_mask[:, None, None, :]
+
+ context = source_hidden_states
+ x = self.embed(target_input_ids).to(dtype=context.dtype)
+
+ # Build position IDs and compute rotary embeddings
+ target_pos_ids = torch.arange(x.shape[1], device=x.device, dtype=torch.long).unsqueeze(0)
+ source_pos_ids = torch.arange(context.shape[1], device=x.device, dtype=torch.long).unsqueeze(0)
+ pos_target = self.rotary_emb(x, target_pos_ids)
+ pos_source = self.rotary_emb(x, source_pos_ids)
+
+ for block in self.blocks:
+ x = block(
+ x,
+ context=context,
+ target_mask=target_attention_mask,
+ source_mask=source_attention_mask,
+ pos_target=pos_target,
+ pos_source=pos_source,
+ )
+ return self.norm(self.out_proj(x))
+
+
+# ============================================================================
+# Anima: MiniTrainDIT + LLMAdapter
+# Reference implementation: https://github.com/hdae/diffusers-anima
+# SPDX-License-Identifier: Apache-2.0
+# ============================================================================
+
+
+class AnimaTransformer(MiniTrainDIT):
+ """Anima transformer: Cosmos Predict2 DiT with integrated LLM Adapter.
+
+ Extends MiniTrainDIT by adding the LLMAdapter component that preprocesses
+ text embeddings before they are fed to the DiT cross-attention layers.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.llm_adapter = LLMAdapter()
+
+ def preprocess_text_embeds(
+ self,
+ text_embeds: torch.Tensor,
+ text_ids: Optional[torch.Tensor],
+ t5xxl_weights: Optional[torch.Tensor] = None,
+ ) -> torch.Tensor:
+ """Run the LLM Adapter to produce conditioning for the DiT.
+
+ Args:
+ text_embeds: Qwen3 hidden states. Shape: (batch, seq_len, 1024).
+ text_ids: T5-XXL token IDs. Shape: (batch, seq_len). If None, returns text_embeds directly.
+ t5xxl_weights: Optional per-token weights. Shape: (batch, seq_len, 1).
+
+ Returns:
+ Conditioning tensor. Shape: (batch, 512, 1024), zero-padded if needed.
+ """
+ if text_ids is None:
+ return text_embeds
+ out = self.llm_adapter(text_embeds, text_ids)
+ if t5xxl_weights is not None:
+ out = out * t5xxl_weights
+ if out.shape[1] < 512:
+ out = F.pad(out, (0, 0, 0, 512 - out.shape[1]))
+ return out
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ timesteps: torch.Tensor,
+ context: torch.Tensor,
+ t5xxl_ids: Optional[torch.Tensor] = None,
+ t5xxl_weights: Optional[torch.Tensor] = None,
+ **kwargs,
+ ) -> torch.Tensor:
+ """Forward pass with LLM Adapter preprocessing.
+
+ Args:
+ x: Input latent tensor. Shape: (B, C, T, H, W).
+ timesteps: Timestep values. Shape: (B,) or (B, T).
+ context: Qwen3 hidden states. Shape: (B, seq_len, 1024).
+ t5xxl_ids: T5-XXL token IDs. Shape: (B, seq_len).
+ t5xxl_weights: Per-token weights. Shape: (B, seq_len, 1).
+
+ Returns:
+ Denoised output. Shape: (B, C, T, H, W).
+ """
+ if t5xxl_ids is not None:
+ context = self.preprocess_text_embeds(context, t5xxl_ids, t5xxl_weights=t5xxl_weights)
+ return super().forward(x, timesteps, context, **kwargs)
diff --git a/invokeai/backend/anima/anima_transformer_patch.py b/invokeai/backend/anima/anima_transformer_patch.py
new file mode 100644
index 00000000000..4eff79830e9
--- /dev/null
+++ b/invokeai/backend/anima/anima_transformer_patch.py
@@ -0,0 +1,106 @@
+"""Utilities for patching the AnimaTransformer to support regional cross-attention masks."""
+
+from contextlib import contextmanager
+from typing import Optional
+
+import torch
+import torch.nn.functional as F
+from einops import rearrange
+
+from invokeai.backend.anima.regional_prompting import AnimaRegionalPromptingExtension
+
+
+def _patched_cross_attn_forward(
+ original_forward,
+ attn_mask: torch.Tensor,
+):
+ """Create a patched forward for CosmosAttention that injects a cross-attention mask.
+
+ Args:
+ original_forward: The original CosmosAttention.forward method (bound to self).
+ attn_mask: Cross-attention mask of shape (img_seq_len, context_seq_len).
+ """
+
+ def forward(x, context=None, rope_emb=None):
+ # If the context sequence length doesn't match the mask (e.g. negative conditioning
+ # has a different number of tokens than positive regional conditioning), skip masking
+ # and use the original unmasked forward.
+ actual_context = x if context is None else context
+ if actual_context.shape[-2] != attn_mask.shape[1]:
+ return original_forward(x, context, rope_emb=rope_emb)
+
+ self = original_forward.__self__
+
+ q = self.q_proj(x)
+ context = x if context is None else context
+ k = self.k_proj(context)
+ v = self.v_proj(context)
+ q, k, v = (rearrange(t, "b ... (h d) -> b ... h d", h=self.n_heads, d=self.head_dim) for t in (q, k, v))
+
+ q = self.q_norm(q)
+ k = self.k_norm(k)
+ v = self.v_norm(v)
+
+ if self.is_selfattn and rope_emb is not None:
+ from invokeai.backend.anima.anima_transformer import apply_rotary_pos_emb_cosmos
+
+ q = apply_rotary_pos_emb_cosmos(q, rope_emb)
+ k = apply_rotary_pos_emb_cosmos(k, rope_emb)
+
+ in_q_shape = q.shape
+ in_k_shape = k.shape
+ q = rearrange(q, "b ... h d -> b h ... d").reshape(in_q_shape[0], in_q_shape[-2], -1, in_q_shape[-1])
+ k = rearrange(k, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
+ v = rearrange(v, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
+
+ # Convert boolean mask to float additive mask for SDPA
+ # True (attend) -> 0.0, False (block) -> -inf
+ # Shape: (img_seq_len, context_seq_len) -> (1, 1, img_seq_len, context_seq_len)
+ float_mask = torch.zeros_like(attn_mask, dtype=q.dtype)
+ float_mask[~attn_mask] = float("-inf")
+ expanded_mask = float_mask.unsqueeze(0).unsqueeze(0)
+
+ result = F.scaled_dot_product_attention(q, k, v, attn_mask=expanded_mask)
+ result = rearrange(result, "b h s d -> b s (h d)")
+ return self.output_dropout(self.output_proj(result))
+
+ return forward
+
+
+@contextmanager
+def patch_anima_for_regional_prompting(
+ transformer,
+ regional_extension: Optional[AnimaRegionalPromptingExtension],
+):
+ """Context manager to temporarily patch the Anima transformer for regional prompting.
+
+ Patches the cross-attention in each DiT block to use a regional attention mask.
+ Uses alternating pattern: masked on even blocks, unmasked on odd blocks for
+ global coherence.
+
+ Args:
+ transformer: The AnimaTransformer instance.
+ regional_extension: The regional prompting extension. If None or no mask, no patching.
+
+ Yields:
+ The (possibly patched) transformer.
+ """
+ if regional_extension is None or regional_extension.cross_attn_mask is None:
+ yield transformer
+ return
+
+ # Store original forwards
+ original_forwards = []
+ for block_idx, block in enumerate(transformer.blocks):
+ original_forwards.append(block.cross_attn.forward)
+
+ mask = regional_extension.get_cross_attn_mask(block_idx)
+ if mask is not None:
+ block.cross_attn.forward = _patched_cross_attn_forward(block.cross_attn.forward, mask)
+
+ try:
+ yield transformer
+ finally:
+ # Restore original forwards
+ for block_idx, block in enumerate(transformer.blocks):
+ block.cross_attn.forward = original_forwards[block_idx]
diff --git a/invokeai/backend/anima/conditioning_data.py b/invokeai/backend/anima/conditioning_data.py
new file mode 100644
index 00000000000..b96c807835d
--- /dev/null
+++ b/invokeai/backend/anima/conditioning_data.py
@@ -0,0 +1,64 @@
+"""Anima text conditioning data structures.
+
+Anima uses a dual-conditioning scheme:
+- Qwen3 0.6B hidden states (continuous embeddings)
+- T5-XXL token IDs (discrete IDs, embedded by the LLM Adapter inside the transformer)
+
+Both are produced by the text encoder invocation and stored together.
+
+For regional prompting, multiple conditionings (each with an optional spatial mask)
+are concatenated and processed together. The LLM Adapter runs on each region's
+conditioning separately, producing per-region context vectors that are concatenated
+for the DiT's cross-attention layers. An attention mask restricts which image tokens
+attend to which regional context tokens.
+"""
+
+from dataclasses import dataclass
+
+import torch
+
+from invokeai.backend.stable_diffusion.diffusion.conditioning_data import Range
+
+
+@dataclass
+class AnimaTextConditioning:
+ """Anima text conditioning with Qwen3 hidden states, T5-XXL token IDs, and optional mask.
+
+ Attributes:
+ qwen3_embeds: Text embeddings from Qwen3 0.6B encoder.
+ Shape: (seq_len, hidden_size) where hidden_size=1024.
+ t5xxl_ids: T5-XXL token IDs for the same prompt.
+ Shape: (seq_len,).
+ t5xxl_weights: Per-token weights for prompt weighting.
+ Shape: (seq_len,). Defaults to all ones if not provided.
+ mask: Optional binary mask for regional prompting. If None, the prompt is global.
+ Shape: (1, 1, img_seq_len) where img_seq_len = (H // patch_size) * (W // patch_size).
+ """
+
+ qwen3_embeds: torch.Tensor
+ t5xxl_ids: torch.Tensor
+ t5xxl_weights: torch.Tensor | None = None
+ mask: torch.Tensor | None = None
+
+
+@dataclass
+class AnimaRegionalTextConditioning:
+ """Container for multiple regional text conditionings processed by the LLM Adapter.
+
+ After the LLM Adapter processes each region's conditioning, the outputs are concatenated.
+ The DiT cross-attention then uses an attention mask to restrict which image tokens
+ attend to which region's context tokens.
+
+ Attributes:
+ context_embeds: Concatenated LLM Adapter outputs from all regional prompts.
+ Shape: (total_context_len, 1024).
+ image_masks: List of binary masks for each regional prompt.
+ If None, the prompt is global (applies to entire image).
+ Shape: (1, 1, img_seq_len).
+ context_ranges: List of ranges indicating which portion of context_embeds
+ corresponds to each regional prompt.
+ """
+
+ context_embeds: torch.Tensor
+ image_masks: list[torch.Tensor | None]
+ context_ranges: list[Range]
diff --git a/invokeai/backend/anima/regional_prompting.py b/invokeai/backend/anima/regional_prompting.py
new file mode 100644
index 00000000000..c0af366332f
--- /dev/null
+++ b/invokeai/backend/anima/regional_prompting.py
@@ -0,0 +1,173 @@
+"""Regional prompting extension for Anima.
+
+Anima's architecture uses separate cross-attention in each DiT block: image tokens
+(in 5D spatial layout) cross-attend to context tokens (LLM Adapter output). This is
+different from Z-Image's unified [img, txt] sequence with self-attention.
+
+For regional prompting, we:
+1. Run the LLM Adapter separately for each regional prompt
+2. Concatenate the resulting context vectors
+3. Build a cross-attention mask that restricts each image region to attend only to
+ its corresponding context tokens
+4. Patch the DiT's cross-attention to use this mask
+
+The mask alternation strategy (masked on even blocks, full on odd blocks) helps
+maintain global coherence across regions.
+"""
+
+from typing import Optional
+
+import torch
+import torchvision
+
+from invokeai.backend.anima.conditioning_data import AnimaRegionalTextConditioning
+from invokeai.backend.util.devices import TorchDevice
+from invokeai.backend.util.mask import to_standard_float_mask
+
+
+class AnimaRegionalPromptingExtension:
+ """Manages regional prompting for Anima's cross-attention.
+
+ Unlike Z-Image which uses a unified [img, txt] sequence, Anima has separate
+ cross-attention where image tokens (query) attend to context tokens (key/value).
+ The cross-attention mask shape is (img_seq_len, context_seq_len).
+ """
+
+ def __init__(
+ self,
+ regional_text_conditioning: AnimaRegionalTextConditioning,
+ cross_attn_mask: torch.Tensor | None = None,
+ ):
+ self.regional_text_conditioning = regional_text_conditioning
+ self.cross_attn_mask = cross_attn_mask
+
+ def get_cross_attn_mask(self, block_index: int) -> torch.Tensor | None:
+ """Get the cross-attention mask for a given block index.
+
+ Uses alternating pattern: apply mask on even blocks, no mask on odd blocks.
+ This helps balance regional control with global coherence.
+ """
+ if block_index % 2 == 0:
+ return self.cross_attn_mask
+ return None
+
+ @classmethod
+ def from_regional_conditioning(
+ cls,
+ regional_text_conditioning: AnimaRegionalTextConditioning,
+ img_seq_len: int,
+ ) -> "AnimaRegionalPromptingExtension":
+ """Create extension from pre-processed regional conditioning.
+
+ Args:
+ regional_text_conditioning: Regional conditioning with concatenated context and masks.
+ img_seq_len: Number of image tokens (H_patches * W_patches).
+ """
+ cross_attn_mask = cls._prepare_cross_attn_mask(regional_text_conditioning, img_seq_len)
+ return cls(
+ regional_text_conditioning=regional_text_conditioning,
+ cross_attn_mask=cross_attn_mask,
+ )
+
+ @classmethod
+ def _prepare_cross_attn_mask(
+ cls,
+ regional_text_conditioning: AnimaRegionalTextConditioning,
+ img_seq_len: int,
+ ) -> torch.Tensor | None:
+ """Prepare a cross-attention mask for regional prompting.
+
+ The mask shape is (img_seq_len, context_seq_len) where:
+ - Each image token can attend to context tokens from its assigned region
+ - Global prompts (mask=None) attend to background regions
+
+ Args:
+ regional_text_conditioning: The regional text conditioning data.
+ img_seq_len: Number of image tokens.
+
+ Returns:
+ Cross-attention mask of shape (img_seq_len, context_seq_len), or None
+ if no regional masks are present.
+ """
+ has_regional_masks = any(mask is not None for mask in regional_text_conditioning.image_masks)
+ if not has_regional_masks:
+ return None
+
+ # Identify background region (area not covered by any mask)
+ background_region_mask: torch.Tensor | None = None
+ for image_mask in regional_text_conditioning.image_masks:
+ if image_mask is not None:
+ mask_flat = image_mask.view(-1)
+ if background_region_mask is None:
+ background_region_mask = torch.ones_like(mask_flat)
+ background_region_mask = background_region_mask * (1 - mask_flat)
+
+ device = TorchDevice.choose_torch_device()
+ context_seq_len = regional_text_conditioning.context_embeds.shape[0]
+
+ # Cross-attention mask: (img_seq_len, context_seq_len)
+ # img tokens are queries, context tokens are keys/values
+ cross_attn_mask = torch.zeros((img_seq_len, context_seq_len), device=device, dtype=torch.float16)
+
+ for image_mask, context_range in zip(
+ regional_text_conditioning.image_masks,
+ regional_text_conditioning.context_ranges,
+ strict=True,
+ ):
+ ctx_start = context_range.start
+ ctx_end = context_range.end
+
+ if image_mask is not None:
+ # Regional prompt: only masked image tokens attend to this region's context
+ mask_flat = image_mask.view(img_seq_len)
+ cross_attn_mask[:, ctx_start:ctx_end] = mask_flat.view(img_seq_len, 1)
+ else:
+ # Global prompt: background image tokens attend to this context
+ if background_region_mask is not None:
+ cross_attn_mask[:, ctx_start:ctx_end] = background_region_mask.view(img_seq_len, 1)
+ else:
+ cross_attn_mask[:, ctx_start:ctx_end] = 1.0
+
+ # Convert to boolean
+ cross_attn_mask = cross_attn_mask > 0.5
+ return cross_attn_mask
+
+ @staticmethod
+ def preprocess_regional_prompt_mask(
+ mask: Optional[torch.Tensor],
+ target_height: int,
+ target_width: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> torch.Tensor:
+ """Preprocess a regional prompt mask to match the target image token grid.
+
+ Args:
+ mask: Input mask tensor. If None, returns a mask of all ones.
+ target_height: Height of the image token grid (H // patch_size).
+ target_width: Width of the image token grid (W // patch_size).
+ dtype: Target dtype for the mask.
+ device: Target device for the mask.
+
+ Returns:
+ Processed mask of shape (1, 1, target_height * target_width).
+ """
+ img_seq_len = target_height * target_width
+
+ if mask is None:
+ return torch.ones((1, 1, img_seq_len), dtype=dtype, device=device)
+
+ mask = to_standard_float_mask(mask, out_dtype=dtype)
+
+ tf = torchvision.transforms.Resize(
+ (target_height, target_width),
+ interpolation=torchvision.transforms.InterpolationMode.NEAREST,
+ )
+
+ if mask.ndim == 2:
+ mask = mask.unsqueeze(0)
+ if mask.ndim == 3:
+ mask = mask.unsqueeze(0)
+
+ resized_mask = tf(mask)
+ return resized_mask.flatten(start_dim=2).to(device=device)
diff --git a/invokeai/backend/anima/scheduler_driver.py b/invokeai/backend/anima/scheduler_driver.py
new file mode 100644
index 00000000000..854d133011b
--- /dev/null
+++ b/invokeai/backend/anima/scheduler_driver.py
@@ -0,0 +1,150 @@
+"""Anima scheduler driver.
+
+Encapsulates the per-scheduler API quirks that ``anima_denoise._run_diffusion``
+would otherwise have to know about:
+
+* Schedulers that accept ``set_timesteps(sigmas=...)`` get the pre-shifted
+ Anima schedule passed directly.
+* Schedulers that don't accept ``sigmas=`` use ``set_begin_index()`` over their
+ own internal flow-shifted schedule. For Heun, the doubled-array index
+ translation (logical step ``k`` → doubled index ``2k``) is handled here.
+* SDE-style schedulers receive a seeded ``torch.Generator`` on every step.
+
+The denoise loop iterates :meth:`AnimaSchedulerDriver.iterations` and calls
+:meth:`AnimaSchedulerDriver.step` per iteration; the driver yields the
+``sigma_prev`` and ``completes_user_step`` flags the caller needs for inpaint
+mixing and progress reporting.
+"""
+
+from __future__ import annotations
+
+import inspect
+from dataclasses import dataclass
+from typing import Iterator
+
+import torch
+from diffusers import FlowMatchHeunDiscreteScheduler
+from diffusers.schedulers.scheduling_utils import SchedulerMixin
+
+from invokeai.backend.flux.schedulers import ANIMA_SCHEDULER_MAP
+
+
+@dataclass(frozen=True)
+class AnimaSchedulerIteration:
+ """Per-iteration metadata yielded by :meth:`AnimaSchedulerDriver.iterations`.
+
+ ``sigma_prev`` is the noise level the latents will be at after this iteration's
+ :meth:`AnimaSchedulerDriver.step` call. ``completes_user_step`` is True when
+ this iteration finishes a user-visible step — for Heun, the second-order
+ half of each pair plus the unpaired terminal first-order step; for every
+ other scheduler, always True.
+ """
+
+ sched_timestep: torch.Tensor
+ sigma_curr: float
+ sigma_prev: float
+ completes_user_step: bool
+ order: int
+
+
+class AnimaSchedulerDriver:
+ """Drives a diffusers scheduler over Anima's pre-shifted sigma schedule."""
+
+ def __init__(
+ self,
+ scheduler_name: str,
+ sigmas: list[float],
+ steps: int,
+ denoising_start: float,
+ denoising_end: float,
+ device: torch.device,
+ seed: int,
+ ):
+ scheduler_class, scheduler_kwargs = ANIMA_SCHEDULER_MAP[scheduler_name]
+ self.scheduler: SchedulerMixin = scheduler_class(num_train_timesteps=1000, **scheduler_kwargs)
+ # Heun toggles state_in_first_order during step(); detect by class so we
+ # can read it before set_timesteps has run.
+ self.is_heun: bool = isinstance(self.scheduler, FlowMatchHeunDiscreteScheduler)
+ self._begin_index: int = 0
+ self._step_generator = torch.Generator(device=device).manual_seed(seed)
+
+ is_lcm = scheduler_name == "lcm"
+ accepts_sigmas = "sigmas" in inspect.signature(self.scheduler.set_timesteps).parameters
+ clipped = denoising_start > 0 or denoising_end < 1
+
+ if not is_lcm and accepts_sigmas:
+ self.scheduler.set_timesteps(sigmas=sigmas, device=device)
+ self._num_iterations = len(self.scheduler.timesteps)
+ elif not is_lcm and clipped and hasattr(self.scheduler, "set_begin_index"):
+ k_start = int(denoising_start * steps)
+ k_end = int(denoising_end * steps)
+ self.scheduler.set_timesteps(num_inference_steps=steps, device=device)
+ if self.is_heun:
+ # Heun's timesteps array is 2N-1 entries; logical step k maps to
+ # doubled index 2k. min() clamps denoising_end=1.0 to the
+ # unpaired terminal first-order step.
+ self._begin_index = 2 * k_start
+ self._num_iterations = min(
+ 2 * (k_end - k_start),
+ len(self.scheduler.timesteps) - self._begin_index,
+ )
+ else:
+ self._begin_index = k_start
+ self._num_iterations = k_end - self._begin_index
+ self.scheduler.set_begin_index(self._begin_index)
+ else:
+ self.scheduler.set_timesteps(num_inference_steps=len(sigmas) - 1, device=device)
+ self._num_iterations = len(self.scheduler.timesteps)
+
+ @property
+ def num_iterations(self) -> int:
+ """Total :meth:`step` calls. For Heun this is roughly 2× the user-visible step count."""
+ return self._num_iterations
+
+ @property
+ def begin_index(self) -> int:
+ return self._begin_index
+
+ def iterations(self) -> Iterator[AnimaSchedulerIteration]:
+ for i in range(self._num_iterations):
+ sched_idx = i + self._begin_index
+ sched_timestep = self.scheduler.timesteps[sched_idx]
+ sigma_curr = sched_timestep.item() / self.scheduler.config.num_train_timesteps
+
+ # Read state_in_first_order before step (Heun toggles it inside step()).
+ in_first_order = self.scheduler.state_in_first_order if self.is_heun else True
+
+ next_idx = sched_idx + 1
+ sigma_prev = self.scheduler.sigmas[next_idx].item() if next_idx < len(self.scheduler.sigmas) else 0.0
+
+ # For Heun, a user step completes on the second-order half of each
+ # pair AND on the unpaired terminal first-order step (sigma_prev==0).
+ is_terminal = sigma_prev == 0.0
+ completes_user_step = (not self.is_heun) or (not in_first_order) or is_terminal
+ order = 2 if self.is_heun else 1
+
+ yield AnimaSchedulerIteration(
+ sched_timestep=sched_timestep,
+ sigma_curr=sigma_curr,
+ sigma_prev=sigma_prev,
+ completes_user_step=completes_user_step,
+ order=order,
+ )
+
+ def step(
+ self,
+ model_output: torch.Tensor,
+ timestep: torch.Tensor,
+ sample: torch.Tensor,
+ ) -> torch.Tensor:
+ step_output = self.scheduler.step(
+ model_output=model_output,
+ timestep=timestep,
+ sample=sample,
+ generator=self._step_generator,
+ )
+ return step_output.prev_sample
+
+ @property
+ def step_generator(self) -> torch.Generator:
+ return self._step_generator
diff --git a/invokeai/backend/anima/t5_tokenizer.py b/invokeai/backend/anima/t5_tokenizer.py
new file mode 100644
index 00000000000..234a574c3e0
--- /dev/null
+++ b/invokeai/backend/anima/t5_tokenizer.py
@@ -0,0 +1,25 @@
+"""Bundled T5-XXL tokenizer for Anima.
+
+Anima tokenizes the prompt with the T5-XXL tokenizer to produce token IDs that
+index the LLM Adapter's learned embedding table. Only the tokenizer is needed —
+never the 9GB T5-XXL weights — so the tokenizer is vendored in the package as a
+self-contained fast tokenizer (tokenizer.json), avoiding both the large download
+and the sentencepiece runtime path.
+"""
+
+from functools import lru_cache
+from pathlib import Path
+
+from transformers import T5TokenizerFast
+
+# Size of the LLM Adapter's token embedding table (T5 v1.1 vocab incl. 100 sentinel
+# extra_id tokens). Token IDs must stay within this range.
+ANIMA_T5_VOCAB_SIZE = 32128
+
+_TOKENIZER_DIR = Path(__file__).parent / "tokenizer"
+
+
+@lru_cache(maxsize=1)
+def load_bundled_t5_tokenizer() -> T5TokenizerFast:
+ """Load the vendored T5-XXL fast tokenizer. Result is cached for the process."""
+ return T5TokenizerFast.from_pretrained(_TOKENIZER_DIR)
diff --git a/invokeai/backend/anima/tokenizer/special_tokens_map.json b/invokeai/backend/anima/tokenizer/special_tokens_map.json
new file mode 100644
index 00000000000..17ade346a10
--- /dev/null
+++ b/invokeai/backend/anima/tokenizer/special_tokens_map.json
@@ -0,0 +1,125 @@
+{
+ "additional_special_tokens": [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ],
+ "eos_token": {
+ "content": "",
+ "lstrip": false,
+ "normalized": false,
+ "rstrip": false,
+ "single_word": false
+ },
+ "pad_token": {
+ "content": "",
+ "lstrip": false,
+ "normalized": false,
+ "rstrip": false,
+ "single_word": false
+ },
+ "unk_token": {
+ "content": "",
+ "lstrip": false,
+ "normalized": false,
+ "rstrip": false,
+ "single_word": false
+ }
+}
diff --git a/invokeai/backend/anima/tokenizer/tokenizer.json b/invokeai/backend/anima/tokenizer/tokenizer.json
new file mode 100644
index 00000000000..21ed409afa3
--- /dev/null
+++ b/invokeai/backend/anima/tokenizer/tokenizer.json
@@ -0,0 +1,129428 @@
+{
+ "version": "1.0",
+ "truncation": null,
+ "padding": null,
+ "added_tokens": [
+ {
+ "id": 0,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 1,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 2,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32000,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32001,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32002,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32003,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32004,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32005,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32006,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32007,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32008,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32009,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32010,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32011,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32012,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32013,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32014,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32015,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32016,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32017,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32018,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32019,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32020,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32021,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32022,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32023,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32024,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32025,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32026,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32027,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32028,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32029,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32030,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32031,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32032,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32033,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32034,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32035,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32036,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32037,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32038,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32039,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32040,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32041,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32042,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32043,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32044,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32045,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32046,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32047,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32048,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32049,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32050,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32051,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32052,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32053,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32054,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32055,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32056,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32057,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32058,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32059,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32060,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32061,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32062,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32063,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32064,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32065,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32066,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32067,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32068,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32069,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32070,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32071,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32072,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32073,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32074,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32075,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32076,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32077,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32078,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32079,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32080,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32081,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32082,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32083,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32084,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32085,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32086,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32087,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32088,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32089,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32090,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32091,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32092,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32093,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32094,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32095,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32096,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32097,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32098,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ },
+ {
+ "id": 32099,
+ "content": "",
+ "single_word": false,
+ "lstrip": false,
+ "rstrip": false,
+ "normalized": false,
+ "special": true
+ }
+ ],
+ "normalizer": {
+ "type": "Sequence",
+ "normalizers": [
+ {
+ "type": "Precompiled",
+ "precompiled_charsmap": "ALQCAACEAAAAAACAAQAAgMz8AgC4BQAAhyIAgMzkAgC4PQAAeyIAgMzsAgC4BQAAiyIAgMw8AADNvAAAmwkAgJ4JAIChCQCAgx0AAIAZAACBGQAAPR0AgDUdAIBNHQCARR0AgIAxAACBMQAApAkAgIkxAAA9WAMAPEgDAEAKAIA+aAMAAYUAAIQBAQADjQAAAokAAAWVAAAEkQAAB50AAAaZAAAJqQAACKEAAAutAAAKpQAADbkAAAy9AAAPvQAADrkAABHFAAAQwQAAE80AABLJAAAV1QAAFNEAABfdAAAW2QAAGeUAABjhAAAb7QAAGukAAB31AAAc8QAAH/0AAB75AABhOAkAZR0AgGNADgBi8AgAZSgPAGSADgBn2A8AZvAPAGlwDABoMAwAa/AMAGrYDABtSA0AbBwNAG8QEgBubA0ARgoAgHAMEwBzqBMAcuwTAHUoEAB0TBAAd9ARAHYUEAB50BYAePQQAF0dAIB69BYAdR0AgG0dAIB/fQEAhgwAgEGAAgDeCwCAQxgAAELAAABFSAAARGAAAEeQBgBGhAEASSgGAEhsAQBLOAcASvAHAE1wBwBMRAcAT/AEAE7MBACnCQCAUCwFAFOgCgBSEAUAVQAKAFRQCgBX0AgAVhALAFlICABYuAgAhBEAAFo8CACA9QAAgZ0AANgLAIAtHQCAg2kCAIJFAgCBNQIAgDUCAIdtAwCGVQMAgTkAAIRlAgAXDACAigEEAInVAwCI7QMAjwkAAKgLAIApDACAjAkAAC8MAICJMQMAkQkAAMzYAABVHQCAfR0AgL0aAIBMCgCAgGUDAIENAwCGPQAAgx0DAMwQAgDNhAEAgikAAMx0AwCjgQYAxRoAgICxAgCBsQIAzRoAgIEpAAClwQAA1RoAgMzoAwDNYAIAUgoAgKjxAABYCgCAXgoAgGQKAIDdGgCAgWkAAMzcBACCEQEA5RoAgGoKAIDtGgCA/RoAgAUbAID1GgCAswkAgMygBADN3AQAzAgBALYJAIClHQCAhhEBAOEAKwDgfCcA44hIAuIMOAKdHQCAh5EBALUdAICtHQCAgNkBAIE1AADMxAIA6kRkApUdAIANGwCA72hkAoERBwCC8QEA8NCLAolVAACB5QEAFRsAgIfhAQCAbQAAgQ0AAIN5AAB2CgCAgXkAAICVAQDMOAEAzRQBAIzBAQB8CgCAvAkAgKMVAQDDlBcAwpwUAMWEFwDEUBcAx+wXAMaAEgCNHQCAiAoAgMvQFgDK4BYAzRQWADUMAIDPvCAAzpwZANHMJADQ2CUA0+gkALFRAQA7DACAp90HAL0dAIDWvCQA2cgnANjUIgDb+CcALRsAgIftBwCCCgCAzPgEAB0bAIAlHQCAh8kGALAJAICR3QcAuQkAgCUbAIBwCgCANRsAgIUdAICMDACAjPkGAAsMAICA1QYAgcEGAMzEAgDNBAUAglEAAIN1BwCArQYAgbkGAIY1BwCHKQcAhEEAAI4KAICn7QAAPRsAgIjpBwCJzQcAlAoAgI/BBwCM3QcAmgoAgOoLAICnXQYAsJ0AAKAKAICmCgCAo0EGAEUbAIBVGwCAfQwAgE0bAIBdGwCArXEGAGUbAIC/CQCAzPgDAM0sAwDCCQCAo+UAAMUJAICMTQAAsgoAgKfxAAC4CgCAsT0GAIedAACGlQAAqB0HAISJAAC+CgCAgqkAAIHVAACtAQcAygoAgJE9AACCmQEAyAkAgM0MBQDMCAUAgT0AAIeFAQCIvQEAdRsAgMUdAICuCwCAjJEBAEEMAIBHDACAzR0AgID1AQCBhQEAgoEBAIOdAQCEiQEAxAoAgIapAQCHXQAAiG0AAIlNAABtGwCAzBACAIxdAACCDQAA0AoAgI9JAACw6QAAfRsAgPALAICjKQEAgCUBAIFVAQCFGwCApzUBAMykAQDNEAIA1goAgI0bAICBNQAA3AoAgK4JAQDoCgCAzOgBAM0oAgCVGwCAo/EAAIQFAACdGwCA4goAgK0bAICotQAApRsAgIFdAAC1GwCAzPwBAM3AAQC9GwCAxRsAgIGFAwARDACAgeUDAO4KAICH6QMAywkAgIylAwDNGwCA+goAgKoJAIDVGwCAgZkDAIHdAwCMvQMAzSQBAMwgAQDMEAIAzTACAIH5AACHUQAAgFUAAIFZAAD0CgCAg0kAAIxBAADlGwCA3RsAgM4JAICBfQAAgHEAAMwgAwDNsAMAo30DANEJAICjEQMA7R0AgIEtAQCx/QAApzEDAK1BAwDlHQCAo20DAP0dAID1HQCA7RsAgKdtAwCANQAAgR0AALFtAwCILQAAmAwAgKeVAACBcQAAgFkAAINxAACj9QAAgVEAAK2BAAD1GwCAsQkDAIldAACEPQAAzDgBAISdAQCBGQAAgAkAAIRlAAD9GwCAzNAHAMzwBwAFHACAkYkAAMxMBgDNBAYAzHAGAM10BgDMQAcAmy0PAMyoBwDNrAcAhg0AAIdVDwCEQQ8ACQsAgIIBDACDVQ8AgDUBAIHZAQCkDACAj+kAAIztAACSDACA3R0AgIv1AACIbQ8AiQ0AAA8LAIC0CwCAgiUAAE0MAICBQQAAUwwAgBUeAIANHgCAJR4AgB0eAIAtHgCABR4AgIApAACBKQAA/AsAgA0cAICEeQAAFRwAgIFNAQCAoQEAGAsAgKP9DwDMOAIAzUgDAB0cAICBWQAAzXwCAMykDQAkCwCAWQwAgKjJDwCHOQAA1wkAgImhDwADCwCAkREAAJ4MAIDaCQCAmQsAgF8MAICAuQ8AgbkPANUdAICDjQ8A9gsAgCUcAICEBQAALRwAgB4LAIA1HACAKgsAgIGdDwCHIQAAh7UPAMyoAgDN6AIAzLQMAM3cDACmzQAAp8UAAE0cAICPgQ8AjIkPAKPlAAAwCwCAPRwAgDwLAICxyQAAhwUAAFUcAIBFHACAhz0AAF0cAIBxDACANgsAgKMFDwCB+QAAzKgDAGUcAIBICwCAjEkAAKPxAABtHACAdwwAgEILAICnlQAAfRwAgHUcAIDMrAMAzcgAAN0JAICHaQAA4AkAgIG9AACCeQAA4wkAgIe5AQBOCwCAkaUAAIEdAACdHACAVAsAgIgFAAClHACAm5EAAFoLAIDmCQCAjJEBANILAIDGCwCAwAsAgMwLAICDRQAAgrkBAIG5AQCApQEAPR4AgIZxAABgCwCAhEkAAIsVAACKPQAAiTkAAIhFAACP+QAAZgsAgLoLAICMBQAAp1EBAKZJAQBlDACAsHkAAKNZAQCMqQAAgKkAAIGpAACBlQAAgJUAAK1xAQBrDACAogsAgISNAABNHgCARR4AgKMhAABdHgCAVR4AgGUeAICBbQAAgG0AALEFAQCkOQAANR4AgIUcAIBsCwCAqAUAAJUcAICNHACArQkAAMywAQCBvQMAgL0DAIPNAwCtHACAtRwAgL0cAIDMvAEAzYQBAInpAwDMHAEAgdkCAIDFAgDNOAEAzDwBAMxoAgDNRAIAg00AAMUcAICH2QAAhy0AAIBFAACBEQAAggUAAHILAIDVHACAzRwAgN0cAIDMOAIAiBUAAIjhAACAbQAAgTkAAMyEAgDNUAEAo0UDAIQ5AQDlHACA7RwAgMzcAwDNSAIAbR4AgOkJAIB4CwCAhR4AgKoMAICBbQAA9RwAgH4LAICj0QAAfR4AgHUeAIDMiAQAgXUAAIB1AACBCwCAo7UAAMwABADNVAIA/RwAgIcLAICETQEAjQsAgAUdAIANHQCAzNAOAMwsAQDMAAUAzVwFAOwJAIDvCQCAzJgOAIHBAADMzA8AzDwOAMwIAQDNnA4AzNQPAM14DwDMPA4AzTgOAIHlAQCA5QEAg+UBAILlAQDUCQCAhOUBAIfhAQBBHQCAiaUBAIjZAQCByQcAOR0AgFEdAIBJHQCAzDQBAPUJAICA3QAAgekAAEMKAICD/QAAgM0AAIH5AACBEQcAaR0AgGEdAICJ0QAAzCgBAHkdAIBxHQCA4QsAgMw0AQDbCwCAgF0AAIFlAACjAQEAg2EAAIFxAACASQAAMR0AgBoMAICrCwCAiVUAACwMAIAyDACAWR0AgIEdAIDBGgCATwoAgIIdAACDeQcAgBkHAIEZBwCGIQAAhykAAISRBwDyCQCAimkAALHZBgCIaQAAifUHAEkKAICP3QcAjNkHAIkMAID4CQCAKR0AgPsJAICRoQcAgEEHAIFBBwCHBQAAyRoAgIKRBwDRGgCA2RoAgKOVBgCGhQcAp+0AAMyQAgDN4AUAsekAAKPBAABVCgCAWwoAgGEKAIBnCgCA/gkAgKVlBwDhGgCAzLgDAKhVBwDpGgCAbQoAgPEaAIABGwCACRsAgPkaAIABCgCAo60AAAQKAICMJQYABwoAgIxNAACpHQCAgm0AAIE9BgCCAQYAgWUAAKEdAICHZQAAuR0AgIcRBgCHrQEAsR0AgMxQAgDNxAIAgeEBAIDJAQCD4QEAkYkAAID9AQCB1QEAmR0AgIydAQCJNQAAcwoAgIB1AACBXQAAhi0AAIc1AACEfQAAERsAgIKFAQCDfQAAgJ0BAIGRAQAZGwCAj+kAAIzhAAB5CgCAfwoAgAoKAICIDQAAifkAAKc5AQCRHQCAiwoAgDgMAICjJQEAPgwAgLBZAACJHQCAggUAAMEdAICtFQEAjwwAgDEbAICGBQAAhQoAgCEbAIApGwCAp2kAAIANAQCBAQEAhzEAAKNJAACxGQEAzBACADkbAIAODACAkQoAgK1RAADM1AEAzfgBAKhBAABBGwCAzTgBAMw8AQCB7QMAlwoAgJ0KAICMDQAA7QsAgKMKAICBxQMAzGgCAKkKAICCxQMASRsAgITJAwCHKQAAhjEAAFkbAICCbQAAgAwAgFEbAICHYQAAYRsAgGkbAIAVHQCAzKgDAM2sAgCB+QAAiC0AAA0KAIAQCgCAEwoAgIw1AAC1CgCAuwoAgLHVAADBCgCAeRsAgMkdAICxCwCAzDABAEQMAIBKDACA0R0AgMwEAQDHCgCAcRsAgKelAADTCgCAo40AAMwUAgCAuQAAgbkAAKeFAAAIDACAgmUAAIEbAICMNQAA8wsAgMzsHADN/AMAiRsAgK6tAADZCgCAkRsAgMzABgDN0AYAsL0BAMyQBwDfCgCAgckBAMwYHQDNIAIAhBEAAOsKAIDNuAYAzKwGAKEbAIDlCgCAgSkAALEbAICpGwCAo+0BAMxAHQDNEAIAuRsAgMEbAICBCQAAyRsAgMxAHQDN0AIAqNkBABQMAIDMkAcAzBwBAMxgBgDNZAYA8QoAgBwKAIDRGwCAkSkBAP0KAICBzR8A2RsAgPcKAIDpGwCA4RsAgMzEBgDNwAYAgTEAAIDZAAAfCgCAIgoAgIK5AQCDRQEAgLkBAIG5AQCGXQEA8R0AgIRdAQDpHQCAzcAAAMzwAACIARwAiXkBAAEeAICPVQEAjGEBAPkdAICB3R4AgRUfAJkbAICBXR8AjIEfAIdBHwDMGAMAzWgDAIBNHwCBpR8AJQoAgIOpHwCMFR8AjNEeACgKAICHtR8AgJUfAIGZHwCBEQAAg70fAICFHwCBiR8A8RsAgIQ9AACbDACAiZkfAPkbAICIBQAABgsAgAEcAICADQAAgf0AAAkcAICj2R8Ao3keAKOFAAAMCwCArTUfAKdhHgCnqR8AoQwAgIQNAACnDACAozUfACsKAICtiR8AhHEAAKchHwCxPR4AsYUfAJUMAIDhHQCAEgsAgLcLAIDMtBwAzbAcAFAMAICxQR8AVgwAgJwLAIAZHgCAER4AgCkeAIAhHgCAgLkeAIG5HgCCIQEAgzUBAIRhAQAxHgCAhokBAIe9AQCIkQEAiekBANkdAICL/QEAjOUBAIINAAAJHgCAj90BAIO5AQCRrQEAgb0BAIC9AQCAoQEAgaEBAPkLAID/CwCAhD0AABEcAICJlQEAm4EBAIHNHgCAzR4AzPwCAM3wAgCB5QAAGRwAgIHtAACjpQAAzJABAM1cAgCHHQAAGwsAgKj5AAAhHACAJwsAgFwMAIBiDACAKRwAgIQFAAAxHACAo9UAACELAIA5HACAgVEAAMz0AQDN0AEALQsAgIc9AABRHACAMwsAgEEcAIA/CwCAhwUAAFkcAIBJHACAh/EDAIHZAwCBmQMAgZEAAGEcAIB0DACAjPkDAMwkAQCHuQMAgfkDADkLAIDMZAIAgskDAIyZAwBpHACAh9EDAI+RAwCB3QYAkfUDAMwABADN7AMAh2UAABkdAIBLCwCAcRwAgHoMAIBFCwCAzBgBAIg5AACBHACAeRwAgMxcAwCMJQAALgoAgMwsAQCx/QAAozkDADEKAIA0CgCAoRwAgKdZAwDMdAMAiAkAAKNRAwCpHACAXQsAgINtDQCnnQAApq0AAKOdAACxDQMAzCgBANULAICntQAAprUAAMkLAIDMMAEAgdUHAMMLAIDMKAEAzwsAgEEeAIBjCwCArYkAAGkLAICAzQEAgd0BAMxEAQDNnB4AhPUBAL0LAIDMWAEAzUwBAIDtAQCB/QEAg7UAAGgMAICM3QEAbgwAgMwIHgCM8QYAzDgBAM08AQBRHgCAiREAAIEFBgBJHgCAYR4AgFkeAIBpHgCAgz0AAIAhAACBOQAAgDkAAIEhAAA5HgCAiRwAgMwoAQCB2QYAbwsAgIH9BgDMJAEAmRwAgJEcAICxHACAgCEBAIE1AQCjBQAAuRwAgMEcAIDJHACAzIwFAM1AAgC3HAMAdQsAgIfNBwDZHACA0RwAgB0dAIDNiAAAzJAAAIzdBQCjhQAAFgoAgMzgAgDhHACAiNUHAIFNAACATQAAUQsAgOkcAIBXCwCAkTkHADcKAICIxQcApQsAgIrJBwDxHACAmz0AAIflBwBxHgCAgYUHAICFBwA6CgCAgvkHAILVBgCDRQAAgMkGAIHdBgCG4QYAewsAgIRRAACJHgCAipUGAIuZBgCIeQAAiZ0GAK0MAICPWQcAjG0HAPkcAIDMgAMAzSQCALARBwA9CgCAgR4AgCEdAIB5HgCAhAsAgICNAACBnQAAzOwDAM3oBAABHQCAigsAgKNJBwCQCwCACR0AgKO9BwARHQCAGwAAgOcHAIALAACApKUHAOsEAICKBQCAAwAAgKhhBwDZDQCAZQAAgMgDAIAbCQCArWkHAIAtAQCBPQEAgl0BAINRAQCEYQEAuAQAgKwEAICHYQEAiK0BAIm1AQCKvQEAjykVALwFAIAdDACAzHgCAM3YBQCB3QEAgXEAAOQLAICC/QEAhBkAACMMAICH7QEAIAwAgMw0BADNMAQA5wsAgJ9pFQAmDACAjMkBAM34BADM8AIAsUkBACEHAICB1QAAoxUBAKCZFQBzCACARgcAgIT1AADMKAQAzSwEAMMIAICveQEAqH0BADENAICqaQEAUgkAgLQlAQC1KQEAowkBAAIMAIDqBgCA7gYAgLIFAQCzPQEAvPUAAL39AAC+2QAAOAgAgLgBAQC5AQEAugEBADwHAIBDBwCAhgwAALOdAwCyiQMAswgAgIC9AwBpBwCAbAcAgBIJAIDkBgCA5wYAgDUIAICJhQMAzOQHAL+hAwAFDACA1wwAgIxlAADN5AwAzCQMAIlBAACIVQAAi0UAAIpFAACFtQMAhLUDAIeVAwCGgQMAAQ0AgAQNAIAHDQCAmCwAABMAAICmyAAAzYwGAMyoBgCFaQAAFwAAgDEAAIBpAACAzPADAAcAAIA1AACA0QwAgLGVAAAlDQCAs5UAALKVAAA1DQCAOA0AgEANAIA7DQCALg0AgHUAAICmBgCAJQAAgJgJAIAdIQCAv1UDAEMNAIAZIQCAFSEAgGEgAIC4bAAAlGUNAJIAAgCcrQEAnaUBAJqJAQCbiQEAmJkBAJmJAQDMIAYAzQQGAMxABgDNXAYAzDwHAM04BwDMvAcAhXUAAIABDwCBDQ8AaSAAgLqZAQCFBQAAcSAAgFkgAIC+hQEAgSkPAIAlDwBlIACAgiEPAIUpAAC0pQEAhREAAG0gAICziQ8AsoUPALHJAQCwAQwAt4EPALbtAQC17QEAtO0BAIFlAQCAZQEAg2EBALi1DwDMPAsAhHkBAIDhDwCB3Q8AdSAAgF0gAIDMyAQAzbgEAIWtAACFFQAAISEAgDkhAIDM6BkAzbQZAKRdAQBGDQCAok0CAKPxDwCgVQEAod0PAH8IAIBuCQCAOwkAgO0eAIBsCQCA9R4AgHcJAIDxHgCAsQgAgJMNAACtHgCA+R4AgITVDACF6Q4AlGkAAIfdDgC1HgCAmbQCAL0eAIDFHgCAsR4AgD0hAIC5HgCAn3QBAMEeAICRGA0AgI0OAIGBDgCGhQ4AlYwDAISJDgCXRAIAghEAAKm4AACA0QAAge0AAMkeAIBJDQCA5R4AgIVZDwCDiQAAoTQNAIFFDgCASQ4A6R4AgKU0AQCFYQ8AzPAUAB0fAIC5xAUAzMgDAM3cAwCA3QAAgcEAACUfAIC/kAUAhREAALHsBwCA9QAAgcEAAKEgAIC1jAYALR8AgLdABgCA3Q4AgekOAMwoAgDNtAIAgM0OAIH5DgCFKQAAg4UBAIB1AQCBsQEAgPEBAIHVAQCpIACANR8AgIUFAACxIACAgJkBAIG9AQCCfQAAk9UBAJThAQCFDQAAmSAAgCEfAICACQAAgRkAACkfAICTrQEAlC0AAKUgAICFDQAAMR8AgIUFAACtIACAOR8AgIUpAACCGQAAhTUAAIDxAACB4QAAtSAAgJ0gAIBBIQCAhQUAAGEhAICDdQEAgO0BAIEpAQDM8AEAzbABAEwNAIBdIQCAWSEAgKMNAIBdHwCAZR8AgIA9AACBDQAAbR8AgHUfAICALQAAgR0AAIIVAABhHwCAzSwBAGkfAIBxHwCAeR8AgIjFAwClIQCAzJACAM28AgCE7QMATw0AgIb5AwCdHwCAgIEDAIH9AwCAPQAAgTUAAIFJAACAQQAAzdwBAIJBAAClHwCAoR8AgKkfAIDNMAEAlJ0DAI0hAIDN8AEAzAwBAIG5AwCAxQMAg6EDAJOlAwCArQAAgdUAAICdAACBqQAAiSEAgFINAICBwQAAgMkAAIC1AACBgQAAhSEAgINpBADMcAMAzbQDAIEhAIDNPAEApg0AgJMBBADNjAIAzPQCAIANAACBNQAAlNkGANEfAIDVHwCA2R8AgMwIAQDNHAEAgREAAIApAACpIQCAghkAAICRAQCBkQEAzWgFAMyUAgDMEAkAzSgWAMxYDgDNeA4AzBQNAM3YCgDMKAwAzYwNAMzgFwDM4AoAzDgLAM30CACFEQAAVQ0AgIBRBwCBUQcA4SAAgM2QDgCFBQAA6SAAgMzYDgDN7AEA8SAAgM0ADgCFGQAAzfAPAM08DgDNVA4AzGgBAM1sAQDZIACAYQgAgJSZBwDMwDsAgGEBAIHZAACFKQAAzWQOAMx4AQDNfAEAga0HAICtBwCFZQAAgp0HAIBRAQCBUQEAlOEHAM3AAACEeQEAk8UHAIZhAQDlIACAiCEBAIUNAADtIACAzRgBAMzYAADNtAAAgN0HAIHNBwCZHwCAhQkAAM0fAID1IACA/R8AgN0gAIAFIACADSAAgBUgAIAJIACAASAAgK0hAIARIACAGSAAgMy4AgDNHAMAgGUAAIF1AACCfQAAHSAAgIUJAACFQQAAASEAgKkNAICAmQYAgSEHAIUZAACDfQAACSEAgIVZAAD9IACA+SAAgIDNAACB2QAAjR4AgIURAACE6QAAlR4AgIblAABBIACAgDUAAIENAACdHgCAhR0AAEkgAIClHgCAhQUAAFEgAICAVQAAgW0AAIJ9AACTRQAAlA0AAIUNAAA5IACAkR4AgIAJAACBEQAAmR4AgIUdAABFIACAoR4AgIUFAABNIACAgOkBAIHxAQCCBQAAqR4AgIUJAACFCQAAVSAAgD0gAICAbQEAgXkBAIIZAACDpQEADSEAgIV1AACFBQAAESEAgAUhAIAhIACAzMgCAM3cAgCsDQCAzR4AgIA5AACBOQAA1R4AgN0eAIDRHgCA2R4AgIAdAACBDQAA4R4AgCUgAICAxQAAgdUAAM3AAADMJAIAgNUAAIHFAACFOQAAg8kAACUhAICvDQCAgNUAAIEJAACFBQAALSEAgP0eAICBIACAgAkAAIERAAAFHwCAk5kAAJS5AAANHwCAhWUAAIU9AACJIACAk10AABUfAICFEQAAzXAFAMx0BQCUATwAkSAAgHkgAIDNKAEAhSAAgI0gAICFGQAAlSAAgH0gAIA1IQCAKSEAgCkgAICFJQAAhTkAAMz4AgDNxAMAzTwBALINAICBlQMAgI0DAM3EAQCCpQMAhVEAAIVJAADMKAEAzSwBAM04AQDMPAEAgGk+AIFpPgBJIQCARSEAgM04PADMVDwAgdE8AJOdPgDMSAEAzcgCAM00AQBNIQCAlLk+AFgNAICAoT4AgaE+AIKhPgCIjTwAVSEAgIWtAACALQAAgSEAAIXVPwCVHwCAgO0AAIHxAACGpQAARR8AgISpAADNJAEAzSgBAE0fAICI+T4AhfE/AFUfAIBJHwCAhcU/AM0wAQDNEAEAzfQGAIDdAQCB6QEAzbwGAM1wBgDM4AYAzVwBAMxoBgDNkAYAzWQGAM14BgDMrAcAzagHAMzoBwDNyAcAgk0/AIP9AgCANQIAgekCAFEfAIBZHwCAgAU9AIV9AQBRIQCALSAAgM0UAQApDgCAge0BAIDhAQDNPAEAgs0BAM0sAQCCdQEAgW0BAIBZAQCAZQEAgcUAAIUfAIDNJAEAzTgBAILxAACB+QAAgFkBAIApAACBcQAAzBgBAM18AQDNLAEAjR8AgIEdAACAHQAAiR8AgJEfAIBxIQCAzSQBAMzkPQDNXA8AzegAAMwMAQCA1QEAgckBAIKZAACD5T8ACR8AgBEfAIAZHwCAMSEAgCMOAIB1IQCAPR8AgDEgAIBBHwCALA4AgIBNPwCBQT8AfR8AgGkhAICBHwCAZSEAgIAlPwCBKT8Ak5E/AIN9AAAmDgCAlEEAAMzYAgDNrAIAbSEAgJNVAACACQAAgR0AALUNAIB9IQCAlEEAAK0fAICAnQAAgaEAAIAdAACBEQAAhKUAALUfAICGpQAAvR8AgIjxAACC0QAAgdkAAIDNAACAJQAAgSkAAIIFAADFHwCAsR8AgLkfAIDBHwCAk7EAAJQRAADJHwCAgB0AAIEVAACAJQAAgS0AAII9AAB5IQCAgO0AAIHRAACCFQAAg4EAAIHQPQA1IACAzCACAM3cAQCFeAIAkSEAgC8OAICZIQCAiRgDAN0fAICALQAAgTUAAIAJAACBbQAA5R8AgMEgAICRsQAAkKkAAJPdOwCSAQQAlaUAAJSVOwDtHwCAlqEAAIUJAACTQQAAySAAgPUfAICFBQAA0SAAgJT1AAC5IACAgLkAAIHdAACC5QAA4R8AgOkfAICF6QAAgAkAAIE1AACFBQAAxSAAgPEfAICFHQAAzSAAgPkfAICFBQAA1SAAgLHBBQCwxQMAvSAAgLLFAwC12QUAtM0DAJ0hAICFOQAAuf0DAKEhAICVIQCAuw0AgM0NAIAXDgCAAR8AgAUOAIDTDQCAzIgCAAsOAIDN4D4AzZABAMwkAQBwDQCAjg0AgEEOAIB9DgCAgLEAAM3UPgDN5D4Agw4AgMy8PgDNuD4AgNEDAIHtAwCC/QMAhmkAAD4OAICFnQMAzTwBADgOAIDM6AIAzTw/AIjlAADNGAEAiQ4AgIhBAAA7DgCAdw4AgM0sAQCVDgCAgNUAAJsOAICG4QAAhukAAEcOAIDNJAEAoQ4AgM0QAQCI0QAAiCkAAMz4AgBNDgCAzfgCAMwkAQCnDgCAhS0DAMygPgDNbD4AgNUDAIHNAwCCAQMAg/kDAMxkAwDNzAIARA4AgM0kAQDMDAIAzQgCAIERAADMnAMAzLA+AM20PgDMxD4AzcA+AMyAPgDNuD4ArQ4AgMyEAgDMmD8AzVA+AMwgPgDNoD4AzQw/AM0wPwDNeD8AzQQ/AIhZAAC/DgCAzfgBAMzEAQBKDgCAxQ4AgMsOAIDMFAIAzAgBAM3IAQCIBQAA0Q4AgNcOAIDMKAIAuQ4AgIgNAACG0QAAgB0BAITNAACI9QAAzDwCAIQ1AQDMRAIAhikBAIAOAICIZQEAhg4AgKdEBQBiDgCAi+0AAIjtAACBDQAAiCUAAIZlAADMcAIAzXQCAMwwAgDN2AUAXA4AgIwOAICAOQAAXw4AgMzgBQB6DgCAzCgBAM0UAQCGJQAAiFUAAAgOAICGhDAAxA0AgIDVBwCG/QcAmA4AgMwkAgCIPQAAng4AgGsOAICIPQAApA4AgMxIAgDNeAIAUA4AgKoOAICXwAUAlnAFAJUYBQCAaQAAk1gFAIE5AACIZQAAkPg8AIZZAACeqAUAhEUAAGgOAIDM1AIAmrQFAIBdAACYrAUAp+wEAIgRAADM2AIAzdwCAKO8BACwDgCAzGACAMIOAIBuDgCAyA4AgK0IBADODgCAq/QEAMwsAgCIBQAA1A4AgLfoAwC2HAQAtSgEAMwAAgCzKAQAi3kAAIh9AACwdAQAhkEAAL6kAwCEdQAAiB0AANoOAIC6TAMAzNwDALj8AwCDqAIAiA0AALwOAICIFQAAh5QCAMw4AgBlDgCAzAQCAIvcAgCPDQAAcQ4AgI8ZAADMIAIAdA4AgI3wAgCIdQAAmCADAJksAwCPDgCAlA0AgMxMAgCWcAMAzCQCAIg9AACSDgCAzCwCAIgFAACzDgCAzCQCAIgNAAC2DgCAh/UAAKjUAwCpxAMA3Q4AgNlgAgDSDwCA1Q8AgNsPAICUNQAAkzEAANloAgDYDwCA2UwCAJQFAADeDwCAlSEAAJQpAABQEACAdBYAgEMXAIDSFgCA2WACADcXAIC12AMAtPADAJQ1AADZWAIAWhcAgJQFAADZVAIAlA0AADEXAIDgdAEAisgAALwVAACIyAAA4IACAIcXAICBoAAApOwCAKTIAgCoXAAAvA0AAJkXAIDghAIAvAUAAJ0XAICk+AIA4PQCALDMAwCV0AAAXRcAgLPgAwCmyAIAp2ACAJLYAABkFwCAvsEAAGsXAICXwQAAchcAgHkXAICAFwCAzXg/AMy8PwC+gA0AixcAgLx4DAC9gA0AuvQMALtUDAC49AwAkhcAgLYXAIC3uAwAuhcAgLWMDACyoAMAs6AMAKEXAICxQAMArnACAK9kAwC4BQMArUgDAKgXAICvFwCAqEQDAKnYAwDaFwCAp9gDAKRoAgCliAMAtjUDALc9AwCSyAIAtT0DAJldAQCYTQEAm2UBAJppAQCdZQEAnGUBAJ+FAQCemQEAh5wCAL6tAACWpQAAl70AAMw0BQDNjDcAzLg4AM2sOACflQEAth0AAJ2ZAQCc9QEAs7EBAK54AgDhFwCAvhcAgJk9AADFFwCAmxkAAJoJAADMFwCA0xcAgOBIAgCeCQAArFwCAK30AgD6FwCA9hcAgP4XAIDoFwCAh2ADAO8XAICvVAIAvhEAAJcFAAACGACA4KwCAAYYAICG+AMAh+wDAOC0AgAOGACAr0gCAK6QAgDgPAIAvg0AAAoYAICXGQAA4NgCAIaEAwCWEQAAvwAMAJ1tAACcYQAAEhgAgLFMAgCzUAIAlQ0AABYYAICGnAMA4MgCALMEAgCCBQAAIhgAgLNQAgCVDQAAJhgAgBoYAIAeGACA4LQCAIaMAwCH3AMAvg0AAJVpAACWeQAAKhgAgLToAgC1UAIAlwUAADIYAIDg1AIAtPQCAL4ZAADgoAIALhgAgODUAgCZjAMAt9QCAIoFAAA2GACAOhgAgIoVAAC3NAIAjx0AAD4YAIBCGACAswUAAEYYAICzBQAAWxgAgJwJAACdCQAATRgAgFQYAICMBQAAYhgAgG0YAIB0GACAexgAgJ9JAACCGACAiRgAgGYYAICQGACAlxgAgNkYAIDPGACA6hgAgOAYAICeGACAg8kBAIH5AQCsGACAsxgAgLoYAIDBGACAyBgAgKUYAICAtAIApYgDAOEIAgCuHQAA8RgAgLwJAACN9QEA9RgAgOEAAgCSlQEA45QQAJNFAACXiQEAhRQAAId4AQCGAAQARjoAgEo6AIBOOgCAUjoAgFY6AICdeQAA74xoAJyhAQBaOgCAXjoAgKKZAABiOgCAZjoAgGo6AIBuOgCAp4kAAHI6AIB2OgCAqUkBAHo6AICsqQAAfjoAgII6AICGOgCAsyUBAIo6AICOOgCAkjoAgLchAQC2OQEAtTEBAJY6AICaOgCAufkAALkRAQC4GQEAnjoAgKI6AICmOgCAqjoAgICwAQCEiAIArjoAgIPIAQCEVAMAhFwEALI6AICEXAUAgN0DAIEtAACCMQAAvjwCALo6AIC+OgCAh4gDAIacBACzLQMAwjoAgMY6AIC+AAQAvhwFALbRAwC12QMAyjoAgLv5AwC68QMAmljTAYTgBwC/xQMAvtkDAL3dAwC83QMAvgAYAKUFAwCmDQMAzjoAgIQcGADSOgCA1joAgKPxAwCsAQMArQEDAK4FAwCvGQMArKQbAq3cGgKqLQMAqyUDAL5MGQC+SBoA2joAgL6AGwC04BoCtdQdArYwHgLvCAIA3joAgOGgAQC6OBoC4/gCALoAAAC9ZBwCvvQcAr8AEAKRBNMBkOT2AeBEAQCSCD4C4joAgOY6AIDqOgCA7joAgL6sHADyOgCA9joAgPo6AID+OgCAAjsAgAY7AIAKOwCAgbBtAICAAQCDHFIAgth3AIUgmgCEkL4AhwjPAIaM5gCJbDcBiOAsAYsYfgGK2BMBjeClAYzwWgGP/OsBjliPAbDVFwCxAWgAso1rALOdawC0SWsAtZVvAA47AIDgcAEAEjsAgBY7AIAaOwCAHjsAgIAZAACBGQAAggUAACI7AIAqOwCAoaUCAKJJBwCjQQcApEEGAKXVGwCm3RsAp8EaAKgBHACp4R8AqkkfAKsBEACs9RMAra0TAK4BFACv+RcAqDEGAKkxBgCqTQYAq0UGAKxNBgCtmQYAro0GAK+FBgCGgAMAhxgDAC47AIAyOwCANjsAgDo7AIA+OwCAQjsAgLhtBwC5dQcAun0HALt1BwC8bQcAvc0HAL75BwC/+QcAsKkGALGFBgCyeQcAs3kHALRpBwC1aQcAtl0HALdVBwC2OgCAs8EGAEY7AIAmOwCAth0GAEo7AIBOOwCAtcEGALppBgC7RQYAUjsAgFY7AIC+qQcAv6kHALypBwC9qQcAo4UGAFo7AIBeOwCAYjsAgGY7AICmWQYApYUGAGo7AICrAQYAqi0GAG47AIByOwCAr+0HAK7tBwCt7QcArO0HAKjBBgCpLQEAqiUBAKs9AQCsJQEArS0BAK4lAQCvlQEAdjsAgHo7AIB+OwCAgjsAgIY7AICCvQAAgb0AAIC9AAC4nQEAua0BALqlAQC7bQAAvHUAAL19AAC+dQAAv20AALD1AQCx/QEAssEBALPBAQC0tQEAtb0BALa1AQC3rQEAijsAgI47AICSOwCAs6EBAJY7AIC1oQEAtqEBAJo7AICGgAEAh8QBALo9AQC7NQEAvBkBAL0ZAQC+fQEAv3UBAKPtAQCeOwCAojsAgKY7AICqOwCApu0BAKXtAQCuOwCAq3kBAKpxAQCyOwCAtjsAgK85AQCuMQEArVUBAKxVAQC6OwCAvjsAgMI7AIDGOwCAyjsAgOGsAQDOOwCA42AGANI7AIDWOwCA2jsAgO9UBgDeOwCA4jsAgL60GgDmOwCA6jsAgO47AICGaBwAh4wDAPI7AID2OwCA+jsAgP47AICAOQAAgTkAAIIFAAACPACACjwAgA48AIASPACAFjwAgKgdAwCpQQMAqkEDAKtBAwCsQQMArUkDAK5xAwCvcQMAhCAdABo8AIAePACAIjwAgCY8AIAqPACALjwAgDI8AIC46QAAufUAALr9AAC78QAAvJEAAL2RAAC+iQAAv4kAALDhAACx4QAAsuEAALPhAAC04QAAte0AALbZAAC32QAA4wwHAOEgBwDhMAEA4wgHADY8AIA6PACAPjwAgEI8AIBGPACASjwAgE48AIBSPACA75gHAFY8AIBaPACA74gHALOJAgBePACAYjwAgL6AGgBmPACAtokCALWJAgBqPACAu2UBALplAQBuPACAcjwAgL9pAQC+ZQEAvXUBALx1AQC3PQYAtj0GALU9BgC0IQYAszUGALI1BgCxAQYAsAkGAL9ZBgC+UQYAvVkGALxNBgC7bQYAunkGALlxBgC4eQYAgJ0AAIGtAACCpQAAejwAgH48AICCPACAhjwAgIo8AICvcQYArmkGAK1tBgCsbQYAq4EGAKqZBgCpkQYAqJkGAAY8AIB2PACAjjwAgKPFHQCSPACApcUdAKbFHQCWPACAhgADAIdkAwCqKR4AqykeAKw5HgCtOR4ArikeAK8lHgCzOR4AmjwAgJ48AICiPACApjwAgLb9HgC1/R4AqjwAgLvZHgC60R4ArjwAgLI8AIC/aR8AvmEfAL1pHwC8wR4AqPEeAKnxHgCq8R4Aq/EeAKw1HgCtPR4ArjUeAK8tHgC2PACAujwAgL48AIDCPACAxjwAgMo8AIDOPACA0jwAgLjlHwC57R8AuuUfALv5HwC86R8AvZEfAL6RHwC/jR8AsFUeALFdHgCyVR4As/0fALTlHwC17R8AtuUfALfdHwCjeR8A1jwAgNo8AIDePACA4jwAgKa9HwClvR8A5jwAgKuZHwCqkR8AhogAAIdMAQCvKR4AriEeAK0pHgCsgR8AgEkAAIFJAACCWQAAs5keAOo8AIC1iR4AtlEBAO48AIDyPACA9jwAgLotAQC7JQEAvD0BAL0lAQC+JQEAvxUBAKhNHgCpVR4Aql0eAKtVHgCsTR4ArZ0BAK6JAQCvgQEAhKwBAPo8AID+PACAAj0AgAY9AIAKPQCADj0AgBI9AIC4ZQEAuW0BALplAQC7fQEAvGUBAL1tAQC+ZQEAv9kAALClAQCxrQEAsqUBALO9AQC0rQEAtZ0BALaVAQC3XQEAo9UdABY9AIAaPQCAHj0AgCI9AICmHQIApcUdACY9AICraQIAqmECACo9AIAuPQCAr1kCAK5pAgCtaQIArHECADI9AIA2PQCAOj0AgD49AIBCPQCARj0AgEo9AIBOPQCAgDkAAIE5AACCBQAAUj0AgFo9AIBePQCAh0ADAIZcBACETAQAYj0AgGY9AICEBAUA4yABAGo9AIDhqAEAbj0AgO+UGgByPQCAdj0AgHo9AIB+PQCAgj0AgIY9AICKPQCAs6EDAI49AICSPQCAlj0AgJo9AIC2fQMAtX0DAJ49AIC7WQMAulEDAKI9AICmPQCAv/0AAL79AAC9/QAAvEEDAKhRAgCpWQIAqmkCAKtpAgCstQIArb0CAK61AgCvrQIAhKgHAKo9AICuPQCAsj0AgIKpAAC2PQCAgKkAAIGpAAC4aQEAuWkBALoJAQC7CQEAvBkBAL0ZAQC+CQEAvwkBALDVAgCx3QIAstUCALNpAQC0eQEAtXkBALZpAQC3YQEA4bgBAOHUHwDjOB8A4wwbALo9AIC+PQCAwj0AgMo9AIDOPQCA0j0AgNY9AIDaPQCAvjwJAN49AIDvhBsA74QbAKOhAgDiPQCAhugEAIe8BQDmPQCApn0CAKV9AgDqPQCAq1kCAKpRAgDuPQCA8j0AgK/9AQCu/QEArf0BAKxBAgCzhQYAxj0AgPY9AID6PQCA/j0AgLaJBgC1jQYAAj4AgLuRBgC6iQYABj4AgAo+AIC/9QYAvokGAL2BBgC8iQYADj4AgBI+AIAWPgCAGj4AgB4+AIAiPgCAJj4AgO+EHQAqPgCA4QAEAC4+AIDj/AQAgBEAAIEdAACCBQAAMj4AgKjxBgCp8QYAqg0GAKsFBgCsBQYArQkGAK49BgCvNQYANj4AgDo+AICGiAAAhxADAD4+AIBCPgCARj4AgEo+AIC4EQYAuRkGALohBgC7IQYAvPUHAL39BwC+9QcAv+kHALBNBgCxVQYAsl0GALNVBgC0TQYAtTEGALYxBgC3MQYAo4UHAE4+AIBSPgCAVj4AgFo+AICmiQcApY0HAF4+AICrkQcAqokHAGI+AIBmPgCAr/UHAK6JBwCtgQcArIkHAGo+AICz4QYAbj4AgHI+AIC25QYAdj4AgHo+AIC18QYAur0GALuNBgB+PgCAgj4AgL59AQC/ZQEAvJUGAL11AQCoHQYAqSUGAKotBgCrJQYArD0GAK0hBgCuXQYAr00GAIY+AICKPgCAjj4AgJI+AICWPgCAgrkDAIGxAwCAuQMAuO0BALmFAQC6jQEAu4UBALydAQC9hQEAvo0BAL+FAQCwPQYAsQ0GALIFBgCz5QEAtP0BALXlAQC25QEAt9UBAKOlBQCaPgCAnj4AgKI+AICqPgCApqEFAKW1BQCuPgCAq8kFAKr5BQCGCAwAhxwDAK8hAgCuOQIArTECAKzRBQCyPgCAs/ECALY+AIC6PgCAtlUDAL4+AIDCPgCAteECALpxAwC7eQMAxj4AgMo+AIC+MQMAvz0DALxRAwC9UQMAqCUCAKk1AgCqPQIAqzUCAKwtAgCtkQMArpEDAK+RAwDOPgCA0j4AgNY+AIDaPgCArAAAAN4+AIDiPgCA5j4AgLiZAwC5rQMAuqUDALttAwC8dQMAvX0DAL51AwC/bQMAsPEDALH5AwCywQMAs8EDALSxAwC1vQMAtrUDALepAwDqPgCA7j4AgPI+AID2PgCA+j4AgP4+AIACPwCA76gaAL5oDADhlAEABj8AgOMcBgCADQAAgXEAAIJxAAAKPwCAo/UDAA4/AIASPwCAhEwCABo/AICmUQIApeUDAB4/AICrfQIAqnUCAIbIDACHLA0ArzkCAK41AgCtVQIArFUCAOFQBgAiPwCA4xQHAITADAAmPwCAKj8AgC4/AIAyPwCANj8AgDo/AIA+PwCAQj8AgEY/AIBKPwCA73gbAL74DwBOPwCAUj8AgFY/AICzjQEAWj8AgLWZAQC2jQEAXj8AgFY9AIBiPwCAuoUBALtNAQC8VQEAvV0BAL5VAQC/SQEAo0EOABY/AIBmPwCAaj8AgG4/AICmQQ4ApVUOAHI/AICrgQ4AqkkOAHY/AIB6PwCAr4UOAK6ZDgCtkQ4ArJkOAIBtAACBCQAAgh0AAH4/AIDvGAkAgj8AgIY/AICKPwCA4zwNAI4/AIDhWAwAkj8AgIbQAACHvAMAlj8AgJo/AICokQ4AqZkOAKrJDgCrxQ4ArN0OAK3BDgCuwQ4Ar/UOAIToAACePwCAoj8AgKY/AICqPwCArj8AgLI/AIC2PwCAuMEPALnBDwC6wQ8Au8EPALzBDwC9wQ8AvsEPAL/1DwCwjQ4AsUUOALJNDgCzRQ4AtF0OALVBDgC2QQ4At0EOAKhRDgCpWQ4Aqo0OAKudDgCshQ4ArY0OAK6FDgCvvQ4Auj8AgL4/AIDCPwCAxj8AgMo/AIDOPwCA0j8AgNY/AIC4kQ4AuZkOALqtDgC7RQEAvF0BAL1FAQC+RQEAv3UBALDFDgCxzQ4AssUOALPdDgC0xQ4AtbUOALa9DgC3tQ4AswUOANo/AIDePwCA4j8AgOY/AIC2DQ4AtQ0OAOo/AIC7CQ4AugEOAO4/AIDyPwCAv3EOAL4BDgC9CQ4AvBEOAIJtAACjQQ4AgFUAAIFlAACmSQ4A+j8AgP4/AIClSQ4AqkUOAKtNDgCGSAAAh3gAAK5FDgCvNQ4ArFUOAK1NDgCoXQIAqWECAKplAgCrdQIArG0CAK2xAgCusQIAr7ECAITsBAACQACABkAAgApAAIAOQACAEkAAgBZAAIAaQACAuHEDALlxAwC6cQMAu3EDALzVAwC93QMAvtUDAL/NAwCw0QIAsdECALLRAgCz0QIAtFEDALVRAwC2UQMAt1EDAB5AAICz6QIAIkAAgL6ABAC2NQIAJkAAgCpAAIC14QIAuhECALsRAgAuQACAMkAAgL6RAwC/kQMAvAECAL0BAgA2QACAOkAAgKOlAgA+QACApa0CAEJAAIBGQACApnkCAEpAAIBOQACAq10CAKpdAgCtTQIArE0CAK/dAwCu3QMAqNUCAKndAgCqLQEAqyUBAKw9AQCtJQEAri0BAK8lAQBSQACAVkAAgFpAAIBeQACAYkAAgGpAAIBuQACAckAAgLiFAQC5iQEAup0BALuVAQC8sQEAvbEBAL55AAC/eQAAsF0BALHlAQCy4QEAs/kBALTpAQC13QEAttUBALe9AQDh8A4AdkAAgOMUDgB6QACAgb0AAIC9AAB+QACAgq0AAIYABACH7AUAgkAAgIZAAICKQACAjkAAgO9gDgCSQACAlkAAgJpAAICFXH0AnkAAgKJAAIDjZAEApkAAgOG0AQCqQACA76AOAK5AAICmPgCAhPgFALJAAIC2QACAukAAgLMlBgBmQACAvkAAgMJAAIDGQACAtiUGALU1BgDKQACAu6EGALoZBgDOQACA0kAAgL+ZBgC+rQYAva0GALy1BgCCbQAA7zAEAIBVAACBZQAAvlwDANZAAICG+AAAh2wDANpAAIDeQACA4kAAgOZAAIDqQACA40QEAO5AAIDhjAcAo6UGAPJAAID2QACA+kAAgP5AAICmpQYApbUGAAJBAICrIQYAqpkGAAZBAIAKQQCArxkGAK4tBgCtLQYArDUGAA5BAICz+QcAEkEAgBZBAIC2SQcAGkEAgB5BAIC1UQcAulEHALtRBwAiQQCAJkEAgL41BwC/OQcAvEUHAL09BwCoNQYAqT0GAKo1BgCriQYArJ0GAK2NBgCusQYAr7EGACpBAIAuQQCAMkEAgDZBAICADQAAgbEAAIKxAAA6QQCAuKEGALmtBgC6vQYAu7UGALytBgC9XQEAvlUBAL9NAQCw0QYAsdEGALLVBgCzrQYAtLUGALW5BgC2qQYAt6UGAKO9BgA+QQCAQkEAgISEAgC+kAEApg0GAKUVBgBKQQCAqxUGAKoVBgCGCAAAh3wBAK99BgCucQYArXkGAKwBBgBOQQCAs60BAFJBAIBWQQCAtqkBAFpBAIBeQQCAta0BALptAQC7dQEAYkEAgGZBAIC+XQEAvzUBALxlAQC9VQEAqGECAKlhAgCqYQIAq2ECAKxhAgCtbQIArp0CAK+VAgBqQQCAbkEAgHJBAIB2QQCAekEAgH5BAICCQQCAhkEAgLiVAgC5nQIAuqECALuhAgC8cQMAvXEDAL5xAwC/cQMAsO0CALH1AgCy9QIAs8UCALTdAgC1tQIAtrECALexAgCKQQCAjkEAgJJBAICj5QIAlkEAgKXlAgCm4QIAmkEAgJ5BAICiQQCAqiUCAKs9AgCsLQIArR0CAK4VAgCvfQIApkEAgKpBAICuQQCAhEB8AIAVAACBHQAAggUAALJBAIC+7HwAukEAgIZIfQCHCAMAvkEAgMJBAIDGQQCAykEAgKidAgCpxQIAqsECAKvBAgCsxQIArc0CAK7xAgCv8QIAzkEAgNJBAIDWQQCA2kEAgMkAAADeQQCA4kEAgOZBAIC4wQEAucEBALrBAQC73QEAvM0BAL31AQC+/QEAv50BALBBAQCxQQEAskEBALNBAQC0QQEAtUEBALZBAQC3QQEA4TgGAOpBAIDjaAYA7kEAgPJBAID2QQCA+kEAgISUfQC+rHwA/kEAgAJCAIAGQgCAvrh/AApCAIDvEAEADkIAgBJCAIAWQgCAGkIAgB5CAIDhkAEAIkIAgONEAAAqQgCAgS0AAIAtAADvgAAAgjkAAC5CAIAyQgCA9j8AgDZCAIDhsH8AtkEAgOPUfAA6QgCAJkIAgD5CAICGuAAAh9QCAEJCAIBGQgCASkIAgE5CAIBSQgCAVkIAgO8gfABaQgCAs4l9AF5CAIBiQgCAZkIAgGpCAIC2jX0AtY19AG5CAIC7RX4AukV+AHJCAIB2QgCAv0V+AL5FfgC9VX4AvFV+AKNJfQB6QgCAfkIAgIJCAICGQgCApk19AKVNfQCKQgCAq4V+AKqFfgCOQgCAkkIAgK+FfgCuhX4ArZV+AKyVfgCCbQAAszF+AIBVAACBZQAAtvF/AITcAwCWQgCAtSF+ALrNfwC70X8AhgAEAIfUAAC+dX8Av3l/ALzBfwC9wX8AqOV/AKn1fwCq/X8Aq/V/AKztfwCtNX4Arj1+AK81fgCaQgCAnkIAgKJCAICmQgCAqkIAgK5CAICyQgCAtkIAgLjZfgC54X4AuuF+ALvhfgC85X4Avel+AL6ZfgC/mX4AsE1+ALFRfgCyUX4As1F+ALT1fgC1+X4Atul+ALfpfgCjdX8AukIAgL5CAIDCQgCAxkIAgKa1fgClZX8AykIAgKuVfgCqiX4AzkIAgNJCAICvPX4ArjF+AK2FfgCshX4A1kIAgLMxfgDaQgCA3kIAgLbFAQDiQgCA5kIAgLXRAQC6yQEAu8kBAOpCAIDuQgCAvs0BAL+xAQC8yQEAvckBAKjdfQCp9X0Aqv19AKvxfQCsHQIArQECAK45AgCvOQIA8kIAgPZCAID6QgCA/kIAgIIFAAACQwCAgBEAAIERAAC4EQIAuRkCALohAgC7IQIAvNUCAL3dAgC+1QIAv80CALBJAgCxSQIAslkCALNZAgC0TQIAtTECALYxAgC3MQIAvgADAKNxfQCEiAIAvoAEAKaFAgAKQwCADkMAgKWRAgCqiQIAq4kCAIYoBACHDAMAro0CAK/xAgCsiQIArYkCABJDAICEyAMAhcwFALPlAwAWQwCAteUDALbtAwAaQwCAHkMAgCJDAIC6bQMAu2UDALx9AwC9ZQMAvmUDAL9VAwAmQwCAKkMAgL8ABACjJQIALkMAgKUlAgCmLQIAMkMAgDZDAIA6QwCAqq0CAKulAgCsvQIAraUCAK6lAgCvlQIAPkMAgEJDAIBGQwCASkMAgE5DAIDjzAMAUkMAgOGsAQBWQwCA7xwDAFpDAIBeQwCAYkMAgGZDAIBqQwCAbkMAgOFwfwBGQQCA4wR+AHJDAIB6QwCA4ZQBAH5DAIDjWAEAgNkAAIHZAACCJQAA7+R+AIJDAICGQwCA7+B+AIpDAICzAQEAjkMAgIboBwCHLAQAkkMAgLY1AQC1BQEAlkMAgLvxAAC64QAAmkMAgJ5DAIC/sQAAvtEAAL3ZAAC84QAABkMAgHZDAICiQwCApkMAgKEBBACgEQQAoxkAAKLFBACotQYAqb0GAKrpBgCr/QYArO0GAK3VBgCu3QYArz0HALBFBwCxVQcAslUHALNtBwC0dQcAtRUHALYdBwC3FQcAuC0HALk1BwC6MQcAuw0HALwZBwC9GQcAvgkHAL8JBwCjQQYAqkMAgK5DAICyQwCAtkMAgKZ1BgClRQYAukMAgKuxBwCqoQcAj8ltAL5DAICv8QcArpEHAK2ZBwCsoQcAld11AJTBdACXzXAAli1zAJFdaACQVWgAk9l0AJJNaQCd5XgAnB17AJ9tBwCeuXgAmR1/AJhVcACboXwAmvl8AIJhbACDhWkAwkMAgMZDAICGEXUAhxF1AISVaQCFjWgAij10AIvFcgDKQwCAzkMAgI7dfgCPMX0AjD1xAI2dcQCSGX0Ak716ANJDAIDvkAkAltUGAJdRBQCUXXkAlQl5AJpxBQCbvQUA1kMAgNpDAIDeQwCA4agFAJx5AQDjuAgAoYUBAOJDAICjqQ0AogEMAKUBCACkOQ0Ap6kJAKa9CQCppRUAqAEUAKsBFACq/RUArbkRAKyxEQCvARwArqEQALH9HACw5R0As+kZALIBGAC1ASQAtH0ZAIQUAAC+FAAAgI0AAIGVAACCbQAA6kMAgIZQDwCHZAAA7kMAgPJDAIC61QcAu90HALjBBwC5wQcAvjEEAL8xBAC88QcAvfEHALKtBwCztQcAsK0HALGlBwC2nQcAt/UHALSlBwC1lQcAqmkHAKtpBwCoaQcAqWkHAK5pBwCvaQcArGkHAK1pBwD2QwCA+kMAgP5DAIACRACABkQAgApEAIAORACAEkQAgKgRBQCpHQUAqjkFAKs5BQCsLQUArVEFAK5JBQCvQQUAFkQAgBpEAIAeRACAIkQAgCZEAIAqRACALkQAgDJEAIC4XQIAuWkCALrBAwC7wQMAvPkDAL35AwC+kQMAv7UDALAJBQCxCQUAsuECALPhAgC0dQIAtX0CALZ1AgC3bQIAs7EEAIQAAgC+BA0ANkQAgDpEAIC20QQAtaUEAD5EAIC7zQQAus0EAEJEAIBGRACAv7kDAL6xAwC9NQMAvDUDAEpEAICj9QQATkQAgFJEAICmlQQAWkQAgF5EAICl4QQAqokEAKuJBACHqA0AhswMAK71AwCv/QMArHEDAK1xAwDhUAYA4TQHAONAAADjWAcAgNEAAIHdAACC1QAAYkQAgGZEAIBqRACAbkQAgHJEAIB2RACAekQAgO+cAADvyAcAfkQAgIJEAICzNQIAhkQAgLW1AQCKRACAjkQAgLa1AQC+7AwAkkQAgLuRAQC6mQEAvVEBALyJAQC/UQEAvlkBAKjtDQCp/Q0AqvUNAKttDgCsdQ4ArX0OAK51DgCvbQ4AVkQAgJZEAICaRACAnkQAgKJEAICmRACAqkQAgK5EAIC49Q4Auf0OALr1DgC7QQ8AvEEPAL1JDwC+cQ8Av3EPALAVDgCxHQ4AshUOALPNDgC01Q4Atd0OALbVDgC3zQ4Ao30NALJEAIC2RACAukQAgL5EAICm/Q4Apf0OAMJEAICr2Q4AqtEOAISoAgDGRACArxkOAK4RDgCtGQ4ArMEOAIBNAACBVQAAglUAALNRDwDKRACAtXEPALZxDwDORACAhuAAAIcEAwC6XQ8Auy0PALw1DwC9OQ8Avi0PAL8lDwCoVQ4AqV0OAKqVDgCrrQ4ArLUOAK29DgCutQ4Ar60OANJEAIDWRACA2kQAgN5EAIDiRACA5kQAgOpEAIDuRACAuGkBALlpAQC6eQEAu3kBALxpAQC9aQEAvt0BAL/VAQCw1Q4AsaUOALKtDgCzoQ4AtKUOALWtDgC2nQ4At1kBAKMdDgDyRACA9kQAgOZDAID6RACApj0OAKU9DgD+RACAq2EOAKoRDgACRQCABkUAgK9pDgCuYQ4ArXUOAKx5DgAKRQCADkUAgBJFAIAWRQCAGkUAgB5FAIAiRQCAJkUAgIANAACBFQAAgh0AACpFAIAuRQCAMkUAgIR4AQC+FAAA4xQPADpFAIDh4A0AhAADAIawBACHFAMAPkUAgEJFAIBGRQCASkUAgE5FAIBSRQCA78APAFZFAIBaRQCAXkUAgGJFAIBmRQCAakUAgLNtAwBuRQCAtX0DALZ1AwByRQCAdkUAgHpFAIC6UQMAu1EDALz1AwC9/QMAvukDAL/hAwB+RQCAgkUAgIZFAICKRQCAjkUAgJJFAICWRQCAmkUAgKhxAgCpeQIAqokDAKuJAwCsmQMArZkDAK6JAwCviQMAsPkDALH5AwCyTQMAs0UDALRBAwC1SQMAtnEDALdxAwC4IQMAuSEDALohAwC7IQMAvCEDAL0hAwC+IQMAvyEDAICdAQCBEQAAghEAAIQEBQDvFAAAnkUAgKJFAIC+EAUA48gAAKpFAIDh0AEArkUAgLJFAIC2RQCAukUAgL5FAICqeQIAq3kCAIboBACHYAUArsECAK/JAgCs3QIArdUCAMJFAICjRQIAxkUAgMpFAICmXQIAzkUAgNJFAIClVQIA1kUAgNpFAIDeRQCA4kUAgOZFAIDqRQCA7kUAgO+EDgC+rAQA4dAOAPJFAIDjFAEA9kUAgPpFAID+RQCAAkYAgLPdAQAGRgCACkYAgA5GAIASRgCAtv0BALX9AQAaRgCAu90BALrdAQCE4AQAHkYAgL+hAQC+vQEAvb0BALy9AQCoBQYAqR0GAKoVBgCrLQYArDUGAK09BgCuNQYArykGAKZFAICC9QcAgeUHAIDlBwAWRgCAIkYAgIYcAACHsAMAuCUGALnFBgC6zQYAu8UGALzdBgC9xQYAvs0GAL/FBgCwWQYAsVkGALIpBgCzKQYAtDkGALUlBgC2JQYAtx0GAKOdBgAmRgCAKkYAgC5GAIAyRgCApr0GAKW9BgA2RgCAq50GAKqdBgA6RgCAPkYAgK/hBgCu/QYArf0GAKz9BgBCRgCAs/UHAEZGAIBKRgCAtu0HAE5GAIBSRgCAteUHALqNBwC7kQcAVkYAgFpGAIC+dQcAv30HALyBBwC9fQcAqCUGAKkpBgCqOQYAqzkGAKwpBgCtKQYArnkGAK91BgBeRgCAYkYAgGZGAIBqRgCAbkYAgHJGAIB2RgCAekYAgLjVBgC53QYAuuEGALv9BgC85QYAve0GAL7lBgC/mQYAsA0GALERBgCyEQYAs+0GALT1BgC1/QYAtvUGALftBgCjsQYAgi0AAIEVAACAsQAANkUAgKapBgCloQYAfkYAgKvVBgCqyQYAgkYAgL5oAQCvOQYArjEGAK05BgCsxQYAikYAgLPxAQCGaAAAh3wBALZdAQCORgCAkkYAgLVVAQC6SQEAu0kBAJZGAICaRgCAvj0BAL8hAQC8OQEAvTUBAJ5GAICiRgCAhAQDAL6AHACmRgCA4RwGAKpGAIDjAAYAvwguAK5GAICyRgCA78gHALZGAIC6RgCAvkYAgMJGAIDGRgCAykYAgKN9AgDORgCApdkCANJGAIDWRgCAptECANpGAIDeRgCAq8UCAKrFAgCtuQIArLUCAK+tAgCusQIAqW0FAKhZBQCrDQIAqrkCAK0dAgCsHQIArwUCAK4NAgC+aB0A4kYAgOZGAIDqRgCAgB0AAIEJAACCmQEA7kYAgLnhAwC4KQIAu+EDALrpAwC94QMAvPkDAL/hAwC+6QMAsU0CALBNAgCzIQIAsi0CALUlAgC0OQIAtxECALYlAgCowQIAqdECAKrRAgCr5QIArP0CAK0VAQCuHQEArw0BAPJGAID6RgCA/kYAgAJHAIAGRwCACkcAgA5HAIASRwCAuAUBALkJAQC6HQEAuxUBALwxAQC9MQEAvv0BAL/1AQCweQEAsUEBALJBAQCzXQEAtEUBALVNAQC2RQEAtz0BAIagHQCHxB0AFkcAgO/YAAAaRwCAHkcAgCJHAIDvxAYAhGwcAOH0BgAmRwCA47AGACpHAIDhlAEALkcAgONEBgCzGQIAMkcAgDZHAIA6RwCAhewsALbVAQC1NQIAPkcAgLvFAQC6/QEAQkcAgEZHAIC/yQEAvsEBAL3JAQC81QEAo9kdAPZGAIBKRwCATkcAgFJHAICmFR4ApfUdAFZHAICrBR4Aqj0eAFpHAIBeRwCArwkeAK4BHgCtCR4ArBUeAIBpAACBaQAAggUAAGJHAIBmRwCAakcAgIcQAwCGfAMAbkcAgHJHAIB2RwCAekcAgH5HAICCRwCAhkcAgIpHAICopR8Aqa0fAKqlHwCrvR8ArKUfAK2tHwCupR8ArxUfAI5HAICSRwCAlkcAgJpHAICeRwCAokcAgKZHAICqRwCAuA0fALkZHwC6IR8AuyEfALzZAAC92QAAvskAAL/BAACwcR8AsXEfALJxHwCzRR8AtEEfALVNHwC2PR8AtzUfALMtHgCuRwCAskcAgLZHAIC6RwCAti0eALUtHgC+RwCAu7UeALq1HgDCRwCAxkcAgL+JHgC+hR4AvZEeALylHgCCKQAAo2keAIAdAACBFQAApmkeAMpHAIDORwCApWkeAKrxHgCr8R4A0kcAgITgAQCuwR4Ar80eAKzhHgCt1R4AqNUBAKnlAQCq7QEAq+UBAKz9AQCt5QEAru0BAK/lAQC+oAEAhkYAgNZHAIDaRwCAhhAAAId0AQDeRwCA4kcAgLh9AQC5wQAAusEAALvBAAC8wQAAvckAAL7xAAC/8QAAsJ0BALFFAQCyTQEAs0UBALRdAQC1RQEAtk0BALdFAQDmRwCA6kcAgO5HAIDyRwCA9kcAgO80AgDv7B4A+kcAgOHwHQDj4AIA4zAeAOGEAQD+RwCAAkgAgAZIAIAKSACAsyUCAJQAAAAOSACAEkgAgBZIAIC2JQIAtTUCABpIAIC7wQIAuhkCAB5IAIAiSACAv8ECAL7ZAgC90QIAvNkCACZIAIAqSACALkgAgKPpAgAySACApfkCAKbpAgA2SACAOkgAgD5IAICq1QIAqw0CAKwVAgCtHQIArhUCAK8NAgCAYQAAgWEAAIIFAABCSACASkgAgIQABAC+FAQATkgAgIbABACHUAMAUkgAgFZIAIBaSACAXkgAgGJIAIBmSACAqK0CAKm9AgCqtQIAqw0BAKwVAQCtHQEArhUBAK8NAQCE7AQAakgAgG5IAIBySACAdkgAgHpIAIB+SACAgkgAgLgdAQC5LQEAuiUBALvNAQC81QEAvd0BAL7JAQC/wQEAsH0BALFVAQCyXQEAs1UBALRNAQC1PQEAtjUBALctAQDhGB4AhkgAgOM4HgCKSACAjkgAgJJIAICWSACAmkgAgJ5IAICiSACAvmAEAKZIAICBdQAAgHUAAO/gHwCCbQAAqkgAgK5IAICG6AQAh3wFALJIAIDhkAEAukgAgOOgAAC+SACAwkgAgMZIAIDvtAAAykgAgM5IAIDSSACA1kgAgLUFBgBGSACAtkgAgLYFBgDaSACA3kgAgLOlBQDiSACAvRkGALwRBgC/YQYAvhEGAOZIAIDqSACAuwkGALohBgCj/QUA7kgAgPJIAID2SACA+kgAgKZdBgClXQYA/kgAgKtRBgCqeQYAAkkAgAZJAICvOQYArkkGAK1BBgCsSQYAqFEGAKlZBgCqYQYAq2EGAKxhBgCtYQYArmEGAK9hBgAKSQCADkkAgBJJAIAWSQCAgA0AAIGxAQCCsQEAGkkAgLhNBwC5VQcAul0HALtVBwC8TQcAvXUHAL59BwC/cQcAsMUHALHNBwCyxQcAs90HALTFBwC1zQcAtsUHALd5BwCz6QcAHkkAgCJJAICEwAEAvtgBALbhBwC16QcAJkkAgLsJBgC6AQYAhogAAIesAQC/CQYAvgEGAL0JBgC8EQYAKkkAgKOtBwAuSQCAMkkAgKalBwA2SQCAOkkAgKWtBwCqRQYAq00GAD5JAIBCSQCArkUGAK9NBgCsVQYArU0GAKhZBgCpZQYAqm0GAKtlBgCsYQYArWEGAK5hBgCvYQYAhKwBAEZJAIBKSQCATkkAgFJJAIBWSQCAWkkAgF5JAIC4kQEAuZkBALqhAQC7oQEAvHEBAL1xAQC+cQEAv3EBALDxAQCx8QEAsvUBALPdAQC0xQEAtbEBALaxAQC3sQEAs+UFAGJJAIBmSQCAakkAgG5JAIC24QUAtekFAHJJAIC7NQIAujUCAHZJAIB6SQCAv3UCAL4BAgC9CQIAvCECAH5JAICjoQUAgkkAgIZJAICmpQUAikkAgI5JAIClrQUAqnECAKtxAgCSSQCAvigDAK5FAgCvMQIArGUCAK1NAgCA1QAAgd0AAILhAACaSQCA4yABAJ5JAIDhqAEAokkAgO80AgCmSQCAhggMAIdoAwCsAAAAqkkAgK5JAICySQCAs40DALZJAIC6SQCAhIAMAL5JAIC2vQMAtYEDAMJJAIC7TQMAuk0DAMZJAIDKSQCAv00DAL5NAwC9TQMAvE0DAKhBAgCpTQIAqkUCAKtZAgCsSQIArX0CAK51AgCvuQIAvmgNAM5JAIDSSQCA1kkAgIRsDADaSQCA3kkAgOJJAIC4TQEAuVUBALpVAQC7ZQEAvH0BAL0VAQC+EQEAvxEBALDJAgCxyQIAstkCALPZAgC0yQIAtckCALZ9AQC3dQEA4XgHAOOYAADjuAYA4VwGAOZJAIDqSQCA7kkAgPJJAID2SQCA+kkAgP5JAIACSgCA7AAAAO9cAADv6AYACkoAgIFpAACAYQAAo4UCAIJhAACliQIADkoAgBJKAICmtQIAhkAMAIfEDACrRQIAqkUCAK1FAgCsRQIAr0UCAK5FAgCojQ4AqZEOAKqVDgCrqQ4ArKUOAK2tDgCupQ4Ar9kOAAZKAIAWSgCAGkoAgB5KAIAiSgCAJkoAgCpKAIAuSgCAuHUPALl9DwC6dQ8Au90PALzFDwC9zQ8AvsUPAL/9DwCwqQ4AsbUOALK1DgCzhQ4AtJ0OALVRDwC2UQ8At1EPALMdDgAySgCANkoAgDpKAIA+SgCAti0OALUtDgBCSgCAu3EOALptDgBGSgCASkoAgL+VDwC+WQ4AvVEOALxhDgBOSgCAo1kOAFJKAIBWSgCApmkOAFpKAIBeSgCApWkOAKopDgCrNQ4AYkoAgGZKAICuHQ4Ar9EPAKwlDgCtFQ4AqL0OAKnRDgCq0Q4AqykBAKw5AQCtOQEArikBAK8pAQCADQAAgRUAAIIdAABqSgCAbkoAgHJKAIC+dAIAdkoAgLjtAQC5hQEAuoEBALuBAQC8hQEAvY0BAL6xAQC/sQEAsFkBALFZAQCy7QEAs+UBALT9AQC15QEAtuUBALfVAQB6SgCAtqkBALWhAQB+SgCAs0kOAIJKAICGOAAAh9wBAL8xAQC+KQEAvSEBALwpAQC7jQEAuo0BAJZJAICGSgCAoxkOAIpKAICOSgCAkkoAgJZKAICm+QEApfEBAJpKAICr3QEAqt0BAJ5KAICiSgCAr2EBAK55AQCtcQEArHkBAKZKAIDv3A8AqkoAgK5KAICySgCAtkoAgLpKAIC+SgCAwkoAgMZKAIDKSgCAzkoAgNJKAIDj6A4A1koAgOGMDgCAEQAAgREAAIIRAACEQAIA2koAgN5KAIDiSgCAvhADAIbABACHRAMA6koAgO5KAIDySgCA9koAgPpKAID+SgCA7yQCAAJLAIAGSwCACksAgA5LAIASSwCAFksAgBpLAICE7AQAHksAgCJLAIAmSwCA4+wCACpLAIDhOAEALksAgLNVAwAySwCANksAgDpLAIA+SwCAth0DALUdAwBCSwCAuwkDALo5AwBGSwCASksAgL/9AAC+/QAAvfkAALwRAwCogQIAqYkCAKqdAgCrsQIArNUCAK3dAgCu1QIAr80CAIDNAQCBCQAAghkAAE5LAIBSSwCAWksAgL5wBQBeSwCAuFkBALlZAQC6aQEAu2kBALx5AQC9eQEAvmkBAL9lAQCwvQIAsY0CALKFAgCzbQEAtHkBALV5AQC2aQEAt2kBAIYgBACHCAUAYksAgGZLAIBqSwCAbksAgHJLAIDvXAAAhOwEAOFcDgB2SwCA44wOAHpLAIB+SwCAgksAgIZLAICjVQIAiksAgI5LAICSSwCAlksAgKYdAgClHQIAmksAgKsJAgCqOQIAnksAgKJLAICv/QEArv0BAK35AQCsEQIAqGkGAKlpBgCqeQYAq3kGAKxpBgCtaQYArp0GAK+VBgBWSwCApksAgKpLAICuSwCAsksAgLZLAIC6SwCAvksAgLj1BgC5+QYAuo0GALuFBgC8nQYAvYUGAL6FBgC/tQYAsO0GALH1BgCy/QYAs/UGALTtBgC10QYAttEGALfRBgCz8QYAghUAAIG1AACAtQAAwksAgLbpBgC14QYAvtQDALsxBgC6KQYAxksAgMpLAIC/FQYAvikGAL0hBgC8KQYAzksAgKO1BgCGyAAAh8gAAKatBgDSSwCA1ksAgKWlBgCqbQYAq3UGANpLAIDeSwCArm0GAK9RBgCsbQYArWUGAKg1BgCpOQYAqoEGAKuBBgCsgQYArYEGAK6BBgCvtQYA4ksAgOZLAIDqSwCA7ksAgPJLAID2SwCA+ksAgP5LAIC4nQYAua0GALqlBgC7aQEAvHkBAL15AQC+aQEAv2kBALDRBgCx0QYAstEGALPRBgC0tQYAtb0GALa1BgC3rQYAswkGAAJMAIAGTACACkwAgA5MAIC2AQYAtQkGABJMAIC7FQYAuhUGABZMAIAaTACAv3kGAL5xBgC9BQYAvAUGAB5MAICjTQYAIkwAgOZKAICmRQYAJkwAgCpMAIClTQYAqlEGAKtRBgAuTACAMkwAgK41BgCvPQYArEEGAK1BBgCB6QMAgN0DAISIAwCC4QMAhrA8AIeIAgC+VAMAOkwAgD5MAIBCTACARkwAgEpMAIBOTACAUkwAgFZMAIBaTACA4/AGAF5MAIDhMAYAhAA8AGJMAIBmTACAakwAgG5MAIByTACAhTQ9AHZMAIB6TACA77AHAH5MAICCTACAhkwAgIpMAICOTACAkkwAgL7EPACWTACAgp0BAIGdAQCAnQEAqA0CAKllAgCqfQIAq3UCAKxZAgCtWQIArpkDAK+ZAwCw6QMAsekDALL5AwCz+QMAtOkDALXpAwC2XQMAt1UDALhtAwC5dQMAunUDALtFAwC8XQMAvTUDAL4xAwC/KQMAmkwAgJ5MAICiTACAqkwAgOFgAwDv9AMA40QCAK5MAICyTACA4zwDAO/0NwDh/AEAtkwAgLpMAIC+TACAwkwAgIZkPwCHaD0AhTQhALOZAwDGTACAtb0DALa1AwDKTACAzkwAgNJMAIC6QQIAu0ECALxBAgC9QQIAvkECAL9BAgDWTACA2kwAgN5MAIDiTACA5kwAgOpMAIDuTACA7/gBAIRoPADhPAYA8kwAgOMcBgD2TACA+kwAgP5MAIACTQCAoxUDAAZNAIAKTQCADk0AgBJNAICmOQMApTEDABpNAICrzQIAqs0CAL5kPgAeTQCAr80CAK7NAgCtzQIArM0CAKgdPgCpJT4Aqi0+AKslPgCsPT4ArSU+AK4tPgCvJT4ApkwAgIL1PwCB5T8AgOU/ABZNAIAiTQCAhgAEAIecAwC4LT4AuTE+ALoxPgC7MT4AvNE+AL3RPgC+0T4Av80+ALBdPgCxIT4Asjk+ALM5PgC0KT4AtSk+ALYZPgC3FT4As6U+ACZNAIAqTQCALk0AgDJNAIC2pT4AtbU+ADZNAIC75T4Aupk+ADpNAIA+TQCAv+0+AL7tPgC97T4AvO0+AEJNAICj4T4ARk0AgEpNAICm4T4ATk0AgFJNAICl8T4Aqt0+AKuhPgBWTQCAWk0AgK6pPgCvqT4ArKk+AK2pPgCPBSUAsyU+AF5NAIBiTQCAtik+AGZNAIBqTQCAtSk+ALp9PgC7RT4Abk0AgHJNAIC+tT4Av70+ALxdPgC9vT4An304AJ5lOQCd8TgAnFE0AJtZNQCaUTUAmfEwAJgNMQCXZTEAlsEwAJVZLQCUTS0Ak+EsAJLZKQCRWSkAkPEoALSlGQC13RgAdk0AgIQIAACwkRUAsQEVALIBGACzvRkAgA0AAIGtAwCCpQMAek0AgKNhAACiHT0AoZk9AKBxPACkxQUApUEEAKYBCACn4QkANkwAgKH1AQCi6QEAo90FAKwBEACtxREArtkRAK85EACoZQgAqQEMAKrZDQCrCQ0AijEuAIuhMwB+TQCAgk0AgI65MwCPETYAjB0yAI1NMgCCJSYAg6krAL5kAwCEYAQAhqEvAIcVLgCEGSoAhZEqAJphPgCb7T4AhsgEAIfcAwCKTQCA4Vw+AJyJAwDjAD4Akmk2AJN5NwCOTQCA7xg+AJZNOwCXuT8AlME7AJVdOgCpnT0AqIk9AKu5PQCqrT0Arak9AKyhPQCvyT0ArqE9AL7oBACSTQCAlk0AgJpNAICeTQCAok0AgKZNAICqTQCAuVk9ALhRPQC7eT0AumU9AL1pPQC8YT0Avx09AL5hPQCxgT0AsLk9ALNpPQCyiT0AtXk9ALRxPQC3aT0AtnE9AKMhPACuTQCAsk0AgLZNAIC6TQCApi08AKUtPAC+TQCAq0E8AKp5PADCTQCAxk0AgK+5PACusTwArbk8AKxZPADKTQCAzk0AgLN9AwDSTQCAtdkDANZNAIDaTQCAttEDAN5NAIDiTQCAu8UDALrFAwC9uQMAvLUDAL+tAwC+sQMA5k0AgOpNAIDuTQCA71wDAIAVAACBHQAAgjEAAO+MPgCE7AQA4fw+APJNAIDjHD4A+k0AgOGUAQD+TQCA4yAAAKP1AwACTgCAh+gEAIZsBAAGTgCAplkDAKVRAwAKTgCAq00DAKpNAwAOTgCAEk4AgK8lAwCuOQMArTEDAKw9AwCGTQCA9k0AgBZOAIAaTgCAHk4AgCJOAIAmTgCAKk4AgKhxBgCpTQYAqo0GAKuFBgCsnQYArYUGAK6NBgCvhQYAsP0GALFBBwCyQQcAs0EHALRBBwC1SQcAtnEHALdxBwC4IQcAuSEHALolBwC7OQcAvCkHAL0VBwC+HQcAv/0HALMlBgAuTgCAMk4AgDZOAIA6TgCAtiUGALU1BgA+TgCAu6UHALoZBgBCTgCARk4AgL+tBwC+pQcAvbUHALy1BwBKTgCAo2EGAE5OAIBSTgCApmEGAFZOAIBaTgCApXEGAKpdBgCr4QcAXk4AgGJOAICu4QcAr+kHAKzxBwCt8QcAqLEGAKm9BgCqzQYAq90GAKzNBgCt/QYArvUGAK8VAQCA+QEAgc0BAILFAQC+ZAIAhpAAAIcAAQBqTgCAbk4AgLjRAQC52QEAuuEBALvhAQC8kQEAvZ0BAL6VAQC/iQEAsG0BALF1AQCyfQEAs3UBALRtAQC18QEAtvEBALfxAQCzRQYAZk4AgHJOAIB2TgCAek4AgLZ9BgC1RQYAfk4AgLuxAQC6qQEAgk4AgIZOAIC/NQEAvqkBAL2hAQC8qQEAik4AgKMBBgCOTgCAkk4AgKY5BgCWTgCAmk4AgKUBBgCq7QEAq/UBAJ5OAICiTgCAru0BAK9xAQCs7QEAreUBAOEoAQCmTgCA41ACAKpOAICuTgCAsk4AgLZOAIC6TgCAvk4AgMJOAIDGTgCAyk4AgIFxAACAGQAA75wCAIJ5AADOTgCA0k4AgITIAgCzxQMA2k4AgLXFAwC2xQMAvhADAIbADACHRAwAuqkDALulAwC8vQMAvaEDAL6hAwC/lQMArhEGAK8ZBgCsAQYArQEGAKqlBgCrEQYAqEU5AKlxOQDeTgCA4k4AgOZOAIDqTgCA7k4AgPJOAID2TgCA+k4AgL7tBwC/TQcAvNEHAL3lBwC63QcAu8EHALg1BgC51QcAtjkGALcNBgC0JQYAtTkGALIxBgCzPQYAsFEGALFRBgCoOQIAqTkCAKqBAgCrgQIArIECAK2JAgCusQIAr7ECAIRsDQD+TgCAvmANAAJPAIAGTwCACk8AgA5PAIASTwCAuE0BALlVAQC6XQEAu1UBALxNAQC9dQEAvn0BAL91AQCwoQIAsa0CALKlAgCzuQIAtKkCALWdAgC2lQIAt3kBAOFUBgDh1AcA4zgGAOOwBwAWTwCAGk8AgB5PAIAiTwCAhOQMACZPAIAqTwCALk8AgDJPAIA2TwCA72wAAO/kBwCjSQIAOk8AgD5PAIBCTwCASk8AgKZJAgClSQIATk8AgKspAgCqJQIAhkgMAIfcDACvGQIAri0CAK0tAgCsMQIAqFEOAKmlDgCqrQ4Aq6UOAKy9DgCtpQ4Arq0OAK+lDgCA5Q8Age0PAILlDwBGTwCAUk8AgFZPAIBaTwCAXk8AgLjVDwC53Q8AutUPALvpDwC8+Q8AvfkPAL7pDwC/6Q8AsN0OALFBDwCyRQ8As10PALRFDwC1TQ8AtkUPALftDwCzJQ4AYk8AgGZPAIBqTwCAbk8AgLYlDgC1NQ4Ack8AgLuFDwC6GQ4Adk8AgHpPAIC/iQ8AvoEPAL2JDwC8kQ8Afk8AgKNhDgCCTwCAhk8AgKZhDgCKTwCAjk8AgKVxDgCqXQ4Aq8EPAJJPAICWTwCArsUPAK/NDwCs1Q8Arc0PAKjRDgCp2Q4AqjkBAKs5AQCsKQEArSkBAK6dAQCvlQEAmk8AgJ5PAICiTwCApk8AgIANAACBtQAAgr0AAKpPAIC4lQEAuZ0BALqhAQC7oQEAvHEAAL1xAAC+cQAAv3EAALDtAQCx9QEAsvUBALPFAQC03QEAtbUBALaxAQC3sQEArk8AgLJPAICzuQEAvsACALWpAQC2TwCAuk8AgLahAQCGgAEAh8QBALs5AQC6IQEAvRkBALwpAQC/eQEAvhEBAKPxAQC+TwCA1k4AgMJPAIDGTwCApukBAKXhAQDKTwCAq3EBAKppAQDOTwCA0k8AgK8xAQCuWQEArVEBAKxhAQDWTwCA2k8AgN5PAIDiTwCA4agBAOZPAIDjQAIA6k8AgL8oFQDuTwCA73QCAPJPAID2TwCA+k8AgP5PAIACUACABlAAgON0DwCEiAMA4TQOAApQAIAOUACAElAAgBZQAICADQAAgRUAAIIRAAAaUACAHlAAgO+kDwAiUACAKlAAgKgZAwCpQQMAqkUDAKtdAwCsTQMArX0DAK51AwCvnQAAhaQVAL58AwCGCAQAhxwDAC5QAIAyUACANlAAgDpQAIC49QAAuf0AALr1AAC7jQAAvIEAAL2BAAC+gQAAv4EAALDlAACx7QAAsuUAALP5AAC07QAAtdEAALbVAAC3zQAAPlAAgEJQAIBGUACAs8ECAEpQAIC1yQIAtvECAE5QAIBSUACAVlAAgLotAQC7JQEAvD0BAL0hAQC+JQEAvxkBAKapAgCESAIAWlAAgKWRAgBeUACAo5kCAGJQAIBmUACArn0BAK9BAQCsZQEArXkBAKp1AQCrfQEAalAAgG5QAIByUACAdlAAgHpQAIB+UACA7+QAAIJQAICGUACAilAAgOMQDgCOUACA4VgOAJJQAICALQAAgREAAIIVAAC+sAUAs3UBAJpQAICHFAUAhmwEAJ5QAIC21QAAtWUBAKJQAIC7/QAAuvUAAKZQAICqUACAv6EAAL69AAC93QAAvN0AAKh9BgCptQYAqr0GAKu1BgCsrQYArRUHAK4dBwCvFQcAllAAgK5QAICyUACAtlAAgLpQAIC+UACAwlAAgMZQAIC4OQcAuTkHALrJBwC7yQcAvNkHAL3ZBwC+zQcAv8UHALBxBwCxeQcAskkHALNJBwC0OQcAtSUHALYhBwC3IQcAozUGAMpQAIDOUACA0lAAgNZQAICmlQcApSUGANpQAICrvQcAqrUHAN5QAIDiUACAr+EHAK79BwCtnQcArJ0HAOZQAIDqUACA7lAAgPJQAID2UACAgj0AAIE9AACAPQAA+lAAgP5QAIACUQCAhKADAL6kAwAGUQCAhvgAAIfgAACoxQYAqdUGAKrVBgCr5QYArP0GAK0xAQCuMQEArzEBAApRAIAOUQCAElEAgBZRAIAaUQCAHlEAgCJRAIAmUQCAuN0BALntAQC65QEAu40BALyVAQC9nQEAvpUBAL+NAQCwUQEAsVEBALJRAQCzUQEAtPUBALX9AQC29QEAt+0BALNdBgAqUQCALlEAgDJRAIA2UQCAtrEBALV1BgA6UQCAu5UBALqVAQA+UQCAQlEAgL85AQC+MQEAvYUBALyFAQClLQYARlEAgEpRAICm6QEATlEAgFJRAICjBQYAVlEAgK3dAQCs3QEAr2EBAK5pAQBaUQCAJlAAgKvNAQCqzQEAXlEAgGJRAICExAMAvwD0AGZRAICCPQAAgT0AAIA9AABqUQCAblEAgHJRAIC+YAMAelEAgH5RAICCUQCAhlEAgIbgHACHAAMA7wwHAIpRAICOUQCAklEAgJZRAICaUQCAnlEAgKJRAICmUQCAqlEAgOHABgCuUQCA4ywHALJRAIC2UQCAulEAgL5RAIDCUQCAxlEAgMpRAIDOUQCA0lEAgKiBAwCpgQMAqoEDAKuBAwCsgQMArYEDAK6BAwCvgQMAsEUDALFNAwCyRQMAs10DALRNAwC1fQMAtnUDALcZAwC4KQMAuTUDALo9AwC7MQMAvAEDAL31AAC+/QAAv+0AALMpAgDWUQCA2lEAgN5RAIDiUQCAtiECALUpAgCEUB0Au6kCALqhAgDqUQCA7lEAgL+ZAgC+qQIAvakCALyxAgCBTQAAgE0AAO+cAwCCXQAAhvAcAId4HQC+EB0A8lEAgPZRAID6UQCA/lEAgAJSAIDhkAEABlIAgONgAwAKUgCADlIAgBJSAIAWUgCAGlIAgB5SAIAiUgCAJlIAgO+UAQCE7BwA4XAGACpSAIDjUAEALlIAgDJSAIA2UgCAOlIAgKPpAgA+UgCAQlIAgEZSAIBKUgCApuECAKXpAgBOUgCAq2kCAKphAgBSUgCAvqgcAK9ZAgCuaQIArWkCAKxxAgCoMR4AqTEeAKoxHgCrMR4ArF0eAK1FHgCuTR4Ar0UeAOZRAICCzR8AgfUfAID9HwBWUgCAWlIAgIYcAACH+AMAuMUeALnNHgC6xR4Au90eALzFHgC9zR4AvsUeAL9ZHwCwPR4AsQUeALINHgCzBR4AtB0eALUBHgC2BR4At/0eALO5HgBeUgCAYlIAgGZSAIBqUgCAtsUeALXVHgBuUgCAu8EeALr5HgByUgCAdlIAgL/FHgC+2R4AvdEeALzZHgB6UgCAo/0eAH5SAICCUgCApoEeAIZSAICKUgCApZEeAKq9HgCrhR4AjlIAgJJSAICunR4Ar4EeAKydHgCtlR4AqCkeAKkpHgCqVR4Aq20eAKx1HgCtfR4ArnUeAK9pHgCWUgCAmlIAgJ5SAICiUgCAplIAgKpSAICuUgCAslIAgLjpHgC59R4Auv0eALv1HgC87R4AvZEeAL6RHgC/kR4AsB0eALHlHgCy7R4As+UeALT9HgC15R4Atu0eALflHgCz3R4AtlIAgLpSAIC+UgCAwlIAgLb9HgC1/R4AhFgBALshHgC62R4AvigAAMpSAIC/IR4AvjkeAL0xHgC8OR4AgU0AAIBNAACjlR4Agl0AAKW1HgDGUgCAzlIAgKa1HgB2UQCA0lIAgKtpHgCqkR4ArXkeAKxxHgCvaR4ArnEeAIYABACHRAMAs4ECANZSAIC1gQIA2lIAgN5SAIC2gQIAiAAAAOJSAIC74QIAuu0CAL3lAgC8+QIAv9ECAL7lAgDmUgCA6lIAgIREAwC+jAMA4UgCAO5SAIDjAAIA7/wfAPJSAIDhPB4A79wCAONgHwD2UgCA+lIAgP5SAIACUwCAqQUCAKixAgCrBQIAqgUCAK0NAgCsBQIArzUCAK41AgCEbAUABlMAgApTAIAOUwCAElMAgBZTAIAaUwCAHlMAgLnpAwC44QMAu/kDALrhAwC96QMAvOEDAL9dAwC+4QMAsSkCALAlAgCzPQIAsiECALUZAgC0LQIAt9kDALYRAgAiUwCAJlMAgCpTAICjhQMALlMAgKWFAwCmhQMAMlMAgDpTAIA+UwCAqukDAKvlAwCs/QMAreEDAK7hAwCv1QMAgEkAAIFVAACCVQAAo6kCAL6YBAClQQEApkEBAEJTAICG4AUAh+AFAKotAQCrOQEArBEBAK0FAQCuDQEArwUBAEZTAIBKUwCATlMAgO/cAABSUwCAVlMAgFpTAIDviB4AhCwHAOHsHgBeUwCA4xweAGJTAIDhlAEAZlMAgOMwAACzJQIAhWDmAGpTAIBuUwCAclMAgLbNAQC1zQEAdlMAgLu1AQC6oQEAelMAgH5TAIC/iQEAvoEBAL2JAQC8nQEANlMAgIJTAICGUwCAilMAgI5TAICSUwCAllMAgJpTAICoAQcAqQEHAKp1BwCrrQcArLUHAK29BwCuqQcAr6kHALDZBwCx7QcAsvkHALP1BwC0mQcAtZkHALaJBwC3gQcAuIkHALmJBwC6bQAAu2UAALx9AAC9ZQAAvm0AAL9lAACBCQAAgJkAAJ5TAICCHQAAolMAgKZTAICqUwCArlMAgKgNBQCpfQUAqk0FAKuhBgCspQYAra0GAK6dBgCv/QYAsIUGALGRBgCyqQYAs70GALSlBgC1rQYAtqUGALd5BgC4SQYAuUkGALpZBgC7WQYAvEkGAL1JBgC++QcAv/kHALNdBgCyUwCAhigCAIcsAQC2UwCAtp0GALWdBgC6UwCAu4kGALq9BgC+UwCAwlMAgL/9BgC+/QYAvYEGALyNBgDGUwCAoxkGAMpTAIDOUwCAptkGANJTAIDWUwCApdkGAKr5BgCrzQYA2lMAgN5TAICuuQYAr7kGAKzJBgCtxQYAqBkBAKkZAQCqjQAAq50AAKyNAACtvQAArrUAAK/dAADiUwCA5lMAgOpTAIDuUwCA8lMAgPZTAID6UwCA/lMAgLhpAAC5aQAAunkAALt5AAC8aQAAvWkAAL7dAwC/1QMAsKkAALGpAACyvQAAs7UAALSZAAC1mQAAtlkAALdZAAC+LAIAAlQAgAZUAIAKVACADlQAgBJUAIAaVACAHlQAgIAtAACBNQAAgj0AACJUAICGkAwAh+gCACZUAIAqVACAs0UDAC5UAIAyVACANlQAgDpUAIC2fQMAtUUDAD5UAIC7LQMAui0DAEJUAIBGVACAvx0DAL4dAwC9IQMAvCkDAKvNAwCqzQMASlQAgE5UAICv/QMArv0DAK3BAwCsyQMAo6UDAFJUAIBWVACAWlQAgF5UAICmnQMApaUDAGJUAIBmVACAalQAgG5UAIByVACAdlQAgII9AACBPQAAgD0AAHpUAIB+VACAglQAgIRgAwCG0AwAhzADAIpUAICOVACAvkQCAJJUAICWVACAmlQAgOEAAACeVACA46gGAKJUAICE7AwAplQAgO/QAwCqVACArlQAgLJUAIC2VACAulQAgLNtAQC+VACAwlQAgMZUAIDKVACAthEBALVlAQDOVACAuz0BALo1AQDSVACA1lQAgL/9AQC+/QEAvRUBALwVAQDaVACA4fwGAN5UAIDjPAcA4lQAgOZUAIDqVACA7lQAgPJUAIC+bAwA+lQAgP5UAIACVQCABlUAgApVAIDvFAYAgV0AAIBdAACj5QEAgm0AAKXtAQAOVQCAElUAgKaZAQCHqAwAhuQMAKu1AQCqvQEArZ0BAKydAQCvdQEArnUBAKgZDgCpGQ4AqiUOAKs1DgCsLQ4ArVEOAK5RDgCvUQ4AhlQAgPZUAIAWVQCAGlUAgB5VAIAiVQCAJlUAgCpVAIC47Q4AufUOALr1DgC7jQ4AvJUOAL2dDgC+lQ4Av40OALAxDgCxOQ4AsgEOALMBDgC0+Q4AtfkOALbdDgC31Q4AqHkOAKl5DgCqjQ8Aq4UPAKydDwCtgQ8AroUPAK+5DwAuVQCAMlUAgDZVAIA6VQCAPlUAgEJVAIBGVQCASlUAgLiRDwC5mQ8AuqEPALuhDwC8UQ8AvV0PAL5JDwC/SQ8AsM0PALHVDwCy3Q8As9UPALTNDwC1sQ8AtrEPALexDwCzBQ4ATlUAgFJVAIBWVQCAWlUAgLYBDgC1FQ4AXlUAgLsRDgC6CQ4AYlUAgISgAQC/dQ4AvgkOAL0BDgC8CQ4AgmkAAKNBDgCAWQAAgVEAAKZFDgC+WAEAZlUAgKVRDgCqTQ4Aq1UOAIbIAACHrAEArk0OAK8xDgCsTQ4ArUUOAGpVAIBuVQCAclUAgHZVAIB6VQCAflUAgBZUAICCVQCAqAkOAKkJDgCqGQ4AqxkOAKwJDgCtYQ4ArmEOAK+VAQCw7QEAsfUBALL9AQCz9QEAtO0BALV1AQC2fQEAt3UBALhNAQC5VQEAul0BALtVAQC8TQEAvfEAAL7xAAC/8QAAhlUAgIpVAICOVQCAklUAgJZVAIDj6A4AmlUAgOE0DgC+AAQA79wPAJ5VAICiVQCAplUAgKpVAICuVQCAslUAgLPxDQC2VQCAulUAgL5VAIDCVQCAtoENALXhDQDGVQCAu1ECALpJAgDKVQCAzlUAgL/RAgC+SQIAvUECALxJAgCjMQ0A0lUAgISIAwDaVQCA3lUAgKZBDQClIQ0A4lUAgKuRAgCqiQIA5lUAgOpVAICvEQIArokCAK2BAgCsiQIAgKkAAIGpAACCTQAA7lUAgOFkEgDjTAIA4wgLAOGsAQDyVQCA7zwCAO8YFgD2VQCAhlAGAIdIAwD6VQCA/lUAgKiBAgCpgQIAqoECAKuBAgCsgQIArYECAK6FAgCvHQEAAlYAgAZWAIAKVgCADlYAgBJWAIAWVgCAGlYAgIS4BQC4dQEAuX0BALp1AQC7CQEAvBkBAL0ZAQC+CQEAvwEBALBlAQCxbQEAsmUBALN9AQC0aQEAtV0BALZVAQC3TQEAHlYAgCJWAIAmVgCAKlYAgC5WAIAyVgCA7zQAAO/ADgDhXA4A4UwPAOOUAADjnA4ANlYAgIJlAACBfQAAgH0AADpWAIA+VgCAvsQHALNFAgBCVgCAtUUCALZNAgBKVgCAhkAGAIeQBAC67QEAu+UBALz9AQC95QEAvuEBAL/VAQCflQgAngUIAJ3dDQCcPQwAmzEMAJr1DQCZ7RAAmD0QAJfVEQCWsRUAlQUUAJTlFQCTtRkAkjEYAJE5GACQDRwAj2EcANZVAICz1QYATlYAgLX9BgBGVgCAUlYAgLaRBgBWVgCAWlYAgLuVBgC6lQYAvVUHALxVBwC/VQcAvlUHAF5WAIBiVgCAqo0GAKuFBgCsnQYArYUGAK6BBgCvtQYAhKgAAGZWAIBqVgCAoyUFAG5WAIClJQUApi0FAHJWAIB2VgCAelYAgH5WAICCVgCAhlYAgIpWAICOVgCAklYAgJZWAICaVgCAnlYAgKJWAICjqQUAotEEAKHZBACgZQUAgiEdAIM1HQCmVgCAqlYAgIaVGACH3RQAhBkZAIUZGQCKDRUAi7EUAK5WAICyVgCAjsURAI/VDACMzRAAjR0RAJJhDQCTdQ0AvkwAALpWAICWxQkAl80EAJSNDACVXQkAmkEFAJtBBQCGyP8Ah0wAAIFZAACAeQAAnCEEAIJRAAChxQEAvlYAgKMB/ACi2QEApRX9AKS1/QCnufkApgH4AKkJ+AColfkAqwX1AKqt9QCtsfEArAHwAK8d8ACurfEAseHtALAB7ACzAegAsv3sALVd6QC09ekAwlYAgMZWAIDKVgCAzlYAgNJWAIDWVgCA2lYAgN5WAIDiVgCA5lYAgKiNBACplQQAqpUEAKulBACsvQQArdkEAK75BACv8QQAhGz8AOpWAIDuVgCA8lYAgPZWAID6VgCA/lYAgAJXAIC4eQUAucUFALrNBQC7xQUAvN0FAL3FBQC+zQUAv+0FALCZBACxmQQAskkFALNJBQC0WQUAtVkFALZJBQC3SQUAox0EAL7M/AAGVwCAClcAgA5XAICmWQQApTUEABJXAICrXQQAql0EABZXAIAaVwCAr50FAK6dBQCtnQUArJ0FAB5XAICznQIAIlcAgCpXAIC2UQIALlcAgDJXAIC1uQIAukkCALtVAgCGSP0Ah8D8AL41AgC/PQIAvEUCAL09AgCo3QQAqUkDAKpRAwCrbQMArHUDAK2VAwCunQMAr7kDAICNAQCB5QEAguEBADZXAIA6VwCAPlcAgEJXAIBGVwCAuJUDALmdAwC6lQMAu60DALy1AwC9vQMAvrUDAL9VAgCwyQMAsdUDALLVAwCzrQMAtLUDALW9AwC2tQMAt60DAEpXAIBOVwCAo9EDAFJXAICl9QMAVlcAgFpXAICmHQMAXlcAgGJXAICrGQMAqgUDAK1xAwCsCQMAr3EDAK55AwDhKAcAZlcAgOPkBgBqVwCA4SgGAG5XAIDjaAEAclcAgHZXAIB6VwCA71gAAH5XAICCVwCAhlcAgO/IBgCKVwCAqE39AKmB/QCq0f0Aq9H9AKzx/QCt8f0ArvH9AK/x/QAmVwCAghEAAIEZAACA0f8AjlcAgJJXAICEdAMAvnQDALh1/gC5ff4AunX+ALvF/gC83f4AvcX+AL7F/gC/9f4AsJH9ALGR/QCykf0As5H9ALRV/gC1Xf4AtlX+ALdN/gCzWf0AllcAgIasAACHRAMAmlcAgLZx/QC1ef0AnlcAgLtV/QC6Vf0AolcAgKZXAIC/mf4AvpH+AL1F/QC8Rf0AqlcAgKMd/QCuVwCAslcAgKY1/QC2VwCAulcAgKU9/QCqEf0AqxH9AL5XAIDCVwCArtX+AK/d/gCsAf0ArQH9AKjN/wCp0f8AqtH/AKsh/gCsIf4ArSH+AK4h/gCvIf4AxlcAgMpXAIDOVwCA0lcAgNZXAIDaVwCA3lcAgOJXAIC4jf4AuZH+ALqV/gC7rf4AvLX+AL25/gC+qf4Av6n+ALDh/gCx4f4AsuX+ALP5/gC06f4AtdX+ALbd/gC3uf4As1n/AOZXAIC2VgCA6lcAgO5XAIC2of4Atan+APJXAIC7Jf4AuiX+APZXAID6VwCAvxH+AL4t/gC9Lf4AvDH+AIIZAACjHf8AgGUAAIEZAACm5f4A/lcAgAJYAICl7f4AqmH+AKth/gCEZAEAviAAAK5p/gCvVf4ArHX+AK1p/gAKWACA4zT+AA5YAIDhfP0AhrAEAIcIAwASWACAFlgAgBpYAIAeWACAhCQDAIQkBAAiWACA70j+ACZYAIAqWACAs+kCAC5YAIC+RAQAvkAFADJYAIC2nQIAtZkCADZYAIC7iQIAur0CADpYAIA+WACAv1kDAL5RAwC9WQMAvJECAKkdAgCoFQIAqyUCAKolAgCtWQIArFUCAK9NAgCuUQIAvmQGAEJYAIBGWACASlgAgE5YAIBSWACAVlgAgFpYAIC5+QMAuPEDALtNAwC68QMAvUEDALxZAwC/cQMAvkEDALEJAgCwPQIAs8kDALIBAgC12QMAtNEDALfJAwC20QMA4ZABAF5YAIDj8AAAYlgAgGZYAICCPQAAgT0AAIA9AABqWACAblgAgHJYAIB6WACAflgAgIJYAIDvLAAAhlgAgKPpAwCKWACAhugEAIdgBQCOWACApp0DAKWZAwCSWACAq4kDAKq9AwCWWACAmlgAgK9ZAgCuUQIArVkCAKyRAwCeWACAolgAgKZYAICqWACArlgAgLJYAIC2WACA71gBAISgBADhVP8AulgAgOOEAQC+WACAwlgAgMZYAIDKWACAs9kBAM5YAICFzBkA0lgAgNZYAIC28QEAtfkBANpYAIC7pQEAutkBAN5YAIDiWACAv50BAL6dAQC9pQEAvK0BAKgBBgCpDQYAqhEGAKsRBgCsMQYArTEGAK4pBgCvJQYAdlgAgILJBwCBwQcAgPEHAOZYAIDqWACAhhwAAIf8AwC47QYAufUGALr9BgC79QYAvO0GAL1RBwC+VQcAv00HALBdBgCxIQYAsjkGALMxBgC0GQYAtRkGALbdBgC31QYAo5kGAO5YAIDyWACA9lgAgPpYAICmsQYApbkGAP5YAICr5QYAqpkGAAJZAIAGWQCAr90GAK7dBgCt5QYArO0GAApZAICz8QcADlkAgBJZAIC2gQcAFlkAgBpZAIC1mQcAuo0HALtlBwAeWQCAIlkAgL59BwC/ZQcAvH0HAL11BwCoLQYAqTUGAKo9BgCrMQYArFUGAK1FBgCuRQYAr3UGACZZAIAqWQCALlkAgDJZAIA2WQCAOlkAgD5ZAIBCWQCAuOkGALn1BgC6/QYAu/UGALztBgC9kQYAvpUGAL+NBgCwDQYAseUGALLtBgCz5QYAtP0GALXlBgC27QYAt+UGAKO1BgBGWQCASlkAgE5ZAIBSWQCApsUGAKXdBgAGWACAqyEGAKrJBgBWWQCAWlkAgK8hBgCuOQYArTEGAKw5BgCASQAAgUkAAIJZAACzRQEAXlkAgLVFAQC2RQEAYlkAgIZAAACHZAAAuikBALslAQC8PQEAvSEBAL4hAQC/FQEAZlkAgGpZAICEBAMAvgAMAOMoBgDv4AIA4RAGAG5ZAIDvkAYA4zwCAHJZAIDh1AEAdlkAgHpZAIB+WQCAglkAgIZZAICKWQCAo8ECAI5ZAIClwQIAklkAgJZZAICmwQIAmlkAgJ5ZAICroQIAqq0CAK2lAgCsuQIAr5ECAK6lAgCpBQIAqLECAKsFAgCqBQIArQ0CAKwFAgCvNQIArjUCAISoDACiWQCAplkAgKpZAICuWQCAslkAgLZZAIC6WQCAuekDALjhAwC7+QMAuuEDAL3pAwC84QMAv10DAL7hAwCxKQIAsCUCALM9AgCyIQIAtRkCALQtAgC32QMAthECAKitAgCp1QIAqtUCAKsNAQCsFQEArQkBAK4xAQCvLQEAvlkAgMJZAIDKWQCAzlkAgNJZAIDWWQCA2lkAgN5ZAIC4IQEAuSEBALrtAQC75QEAvP0BAL3lAQC+7QEAv+UBALBVAQCxXQEAslUBALMtAQC0NQEAtTkBALYtAQC3JQEAgD0BAIGlAACCrQAA79QHAOJZAIDmWQCA6lkAgO8oBwC+LAwA4fQGAO5ZAIDjkAcA8lkAgOGUAQD2WQCA4wwGALMdAgD6WQCAh0QNAIZMDQD+WQCAtskBALXdAQACWgCAu9kBALrRAQAGWgCACloAgL+9AQC+sQEAvbkBALzBAQDGWQCADloAgBJaAIAWWgCAGloAgB5aAIAiWgCAJloAgKgJDwCpCQ8AqhkPAKsZDwCsCQ8ArQkPAK6pDwCvqQ8AsNkPALHtDwCy+Q8As/UPALSVDwC1hQ8AtoUPALe1DwC4jQ8AuWEAALphAAC7YQAAvGEAAL1hAAC+YQAAv2EAAKNdDQCCLQAAgRUAAIAdAAAqWgCApokOAKWdDgAuWgCAq5kOAKqRDgAyWgCANloAgK/9DgCu8Q4ArfkOAKyBDgA6WgCAs/UPAIboAwCHvAMAtu0PAD5aAIBCWgCAteUPALp5DwC7TQ8ARloAgEpaAIC+NQ8AvyUPALxJDwC9RQ8AozEOAE5aAIBSWgCAVloAgFpaAICmKQ4ApSEOAF5aAICriQ4Aqr0OAGJaAIBmWgCAr+EOAK7xDgCtgQ4ArI0OAGpaAIBuWgCAcloAgHZaAIB6WgCAfloAgIJaAICGWgCAiloAgI5aAICSWgCAlloAgIANAACB1QAAgt0AAJpaAICoQQEAqVEBAKpRAQCrZQEArH0BAK2RAACukQAAr5EAAJ5aAICiWgCAhGQBAL5kAQCGkAEAh4QAAKpaAICuWgCAuJEAALmRAAC6kQAAu5EAALyxAAC9sQAAvrEAAL+xAACw8QAAsfkAALLBAACzwQAAtLEAALWxAAC2sQAAt7EAALPZAgCyWgCAvnADAL5EBAC2WgCAthEDALX1AgC6WgCAuz0DALo1AwC+WgCAwloAgL91AwC+dQMAvRUDALwVAwDGWgCAo50CAMpaAIDOWgCAplUDANJaAIDWWgCApbECAKpxAwCreQMA2loAgN5aAICuMQMArzEDAKxRAwCtUQMAqDkDAKk5AwCqjQAAq50AAKyNAACtvQAArrUAAK/dAADiWgCA5loAgOpaAIDuWgCA8loAgPZaAID6WgCA/loAgLhpAAC5aQAAunkAALt5AAC8aQAAvWkAAL7ZAQC/2QEAsKkAALGpAACyvQAAs7UAALSZAAC1mQAAtlkAALdZAAACWwCABlsAgApbAIAOWwCA70QAABJbAICGmAUAh+QCAOOYAACEqAIA4fgBABpbAICAOQAAgTkAAIItAAAeWwCAs0UBACJbAIAmWwCAKlsAgC5bAIC2fQEAtUUBADJbAIC7LQEAui0BADZbAIA6WwCAvx0BAL4dAQC9IQEAvCkBAD5bAIDhUA4AQlsAgOM8DwBGWwCASlsAgE5bAIBSWwCAVlsAgFpbAIDjAAAAXlsAgGJbAIBmWwCAhPQFAO/kDgCuqQEAr6kBAKydAQCtlQEAqpkBAKuZAQBqWwCAblsAgKbJAQByWwCAdlsAgKXxAQCC/QcAo/EBAID9BwCB9QcAFlsAgHpbAIB+WwCAglsAgIZbAICKWwCAhrgDAIeQAwCoDQcAqRkHAKptBwCrZQcArH0HAK1lBwCuZQcAr1UHALAtBwCxxQcAssEHALPdBwC0xQcAtc0HALbFBwC3/QcAuMUHALnJBwC62QcAu9kHALypBwC9qQcAvp0HAL+VBwCzxQcAjlsAgJJbAICWWwCAmlsAgLbFBwC11QcAnlsAgLshBwC6yQcAolsAgKZbAIC/KQcAviEHAL0pBwC8NQcAqlsAgKOBBwCuWwCAslsAgKaBBwC2WwCAulsAgKWRBwCqjQcAq2UHAL5bAIDCWwCArmUHAK9tBwCscQcArW0HAKgVAQCpgQEAqoEBAKuBAQCsgQEArYkBAK6xAQCvsQEAxlsAgMpbAIDOWwCA0lsAgNZbAIDaWwCA3lsAgOJbAIC4ZQAAuW0AALplAAC7fQAAvGUAAL1tAAC+ZQAAv90AALChAQCxrQEAsqUBALO5AQC0qQEAtZ0BALaVAQC3XQAA5lsAgIIdAACBHQAAgB0AAOpbAIDuWwCA8lsAgL5YAQCErAIA9lsAgIcIAQCGjAEA+lsAgKZaAID+WwCAAlwAgLNJAQAGXACAClwAgA5cAIASXACAtkkBALVJAQAWXACAuykBALolAQAaXACAHlwAgL8ZAQC+LQEAvS0BALwxAQC+2AMAIlwAgO/4BgAmXACAKlwAgC5cAIDv4AIAMlwAgOGUAQA2XACA43QCADpcAIDhmAUAPlwAgOMMBwBCXACARlwAgEpcAICjwQIAhIwDAKXBAgBOXACAUlwAgKbBAgBWXACAWlwAgKuhAgCqrQIAraUCAKy5AgCvkQIArqUCAKgxAwCpPQMAqjUDAKtJAwCsWQMArVkDAK5JAwCvQQMAgMUAAIEJAACCGQAAXlwAgGJcAIBqXACAh2wDAIYcHAC47QAAufEAALr1AAC7jQAAvJUAAL2BAAC+gQAAv70AALAJAwCxCQMAsu0AALPhAAC04QAAteEAALblAAC32QAAblwAgHJcAIB2XACAs7ECAHpcAIC13QIAttUCAH5cAICCXACAhlwAgLrBAgC7wQIAvDUBAL05AQC+KQEAvykBAKaNAgCKXACAjlwAgKWFAgCSXACAo+kCAJZcAICaXACArnEBAK9xAQCsbQEArWEBAKqZAgCrmQIAnlwAgKJcAICmXACA4YQGAKpcAIDjJAYArlwAgOGUAQCyXACA4ywAAL7oHQC2XACAulwAgO/IAACE/B0AvvAcAL5cAIDvSAcAwlwAgMZcAIDKXACAzlwAgIEdAACAHQAA0lwAgIIFAACGQBwAh8QcANpcAIDeXACA4lwAgOZcAIDqXACA7lwAgKi1HgCpBR8Aqg0fAKsFHwCsAR8ArQkfAK45HwCvOR8A1lwAgPJcAID2XACA+lwAgP5cAIACXQCABl0AgApdAIC4yR8AudUfALrRHwC76R8AvPkfAL3tHwC+mR8Av5kfALAlHwCxLR8AsjkfALM1HwC0LR8AtQ0fALYFHwC3/R8As4UfAA5dAIASXQCAFl0AgBpdAIC2iR8AtYkfAB5dAIC76R8AuuEfACJdAIAmXQCAv8kfAL7pHwC94R8AvO0fACpdAICjwR8ALl0AgDJdAICmzR8ANl0AgDpdAIClzR8AqqUfAKutHwA+XQCAQl0AgK6tHwCvjR8ArKkfAK2lHwCo6R4AqekeAKr5HgCr+R4ArOkeAK3pHgCuPQEArzUBAID5AQCBzQEAgsUBAIRgAgBGXQCASl0AgIdoAQCGnAAAuNEBALnZAQC64QEAu+EBALyRAQC9nQEAvpUBAL+JAQCwTQEAsVUBALJdAQCzVQEAtE0BALXxAQC28QEAt/EBALNxHgBOXQCAUl0AgFZdAIBaXQCAtmkeALVhHgBeXQCAu5EBALqJAQBiXQCAZl0AgL81AQC+iQEAvYEBALyJAQBqXQCAZlwAgKM5HgBuXQCApSkeAHJdAIB2XQCApiEeAHpdAIB+XQCAq9kBAKrBAQCtyQEArMEBAK99AQCuwQEAgl0AgIZdAICKXQCAjl0AgJJdAICWXQCAml0AgJ5dAICiXQCApl0AgKpdAICuXQCAsl0AgLpdAIC+XQCAvnADAOHkHgCESAIA4+gfAIQABACAeQAAgXkAAIJpAADCXQCAhsAEAIdEAwDGXQCAyl0AgM5dAIDSXQCA7yAfANZdAIDaXQCA3l0AgOJdAIDvSAIA5l0AgOpdAIDuXQCA8l0AgL7oBAD2XQCA+l0AgP5dAIACXgCA4ZABAAZeAIDj6AIAs0kDAApeAIAOXgCAEl4AgBZeAIC2SQMAtUkDABpeAIC7LQMAuiUDAB5eAIAiXgCAvxUDAL4VAwC9IQMAvCkDAKg1AgCpgQIAqoECAKuBAgCsgQIArYkCAK6xAgCvsQIAgP0BAIHNAQCCxQEAKl4AgIaQBACHBAUALl4AgIRwBAC4SQEAuUkBALpZAQC7WQEAvEkBAL1JAQC+eQEAv3kBALChAgCxqQIAsr0CALO1AgC0kQIAtZECALZ5AQC3eQEAMl4AgDZeAIA6XgCAPl4AgEJeAIBGXgCASl4AgO/QHgC+6AQA4VweAE5eAIDjkAAAUl4AgFZeAIBaXgCAXl4AgKNJAgBiXgCAZl4AgGpeAIBuXgCApkkCAKVJAgByXgCAqy0CAKolAgB2XgCAel4AgK8VAgCuFQIArSECAKwpAgCoNQYAqT0GAKpVBgCrZQYArH0GAK1lBgCubQYAr2EGACZeAIB+XgCAgl4AgIZeAICADQAAgbEAAIKxAACKXgCAuOkGALnpBgC6+QYAu/UGALyVBgC9nQYAvpUGAL+NBgCw4QYAseEGALLhBgCz/QYAtOUGALXtBgC25QYAt9kGALPdBgCOXgCAkl4AgJZeAICaXgCAtuUGALX1BgCeXgCAuyUGALolBgCGmAAAh6wAAL8pBgC+IQYAvSkGALw1BgCiXgCAo5kGAKZeAICqXgCApqEGAK5eAICyXgCApbEGAKphBgCrYQYAtl4AgLpeAICuZQYAr20GAKxxBgCtbQYAqC0GAKk9BgCqiQYAq4kGAKyZBgCtmQYArokGAK+JBgC+XgCAwl4AgMZeAIDKXgCAzl4AgNJeAIDWXgCA2l4AgLiNBgC5lQYAupUGALulBgC8vQYAvXEBAL5xAQC/cQEAsPkGALHNBgCy2QYAs9kGALTJBgC1yQYAtr0GALe1BgCzAQYA3l4AgOJeAIDmXgCA6l4AgLYZBgC1EQYA7l4AgLsJBgC6PQYA8l4AgPZeAIC/DQYAvg0GAL0NBgC8DQYA+l4AgKNFBgC2XQCA/l4AgKZdBgACXwCAhFgAAKVVBgCqeQYAq00GAL5oAQAGXwCArkkGAK9JBgCsSQYArUkGAIDBAwCByQMAgt0DAKPNAgAKXwCApdkCAKbNAgAOXwCAhoANAIeUAwCqxQIAqw0DAKwVAwCtHQMArhUDAK8NAwDhnBcA4xgGAOMUAwDhNAYA7xgCABJfAIAWXwCAGl8AgOPQAgAeXwCA4VACACJfAIAmXwCA7ywGAO/kJQAqXwCArE0CAK1RAgCuUQIAr2UCAKgBAgCpCQIAqlkCAKtVAgCE7A0ALl8AgDJfAIA2XwCAvvgNADpfAIA+XwCAQl8AgLxRAwC9WQMAvmEDAL9hAwC47QMAuVEDALpRAwC7UQMAtM0DALXVAwC23QMAt9UDALAdAgCx1QMAst0DALPVAwDjyAAARl8AgOG4AQBKXwCAhFQPAE5fAIBSXwCAVl8AgKHpAgCgFQYAo6UDAKINAwDvIAAAWl8AgF5fAIBiXwCAZl8AgGpfAICFNCYAs40DAG5fAIC1mQMAto0DAHJfAICGwA8Ah5QNALqFAwC7TQIAvFUCAL1dAgC+VQIAv00CAHpfAIB+XwCAgl8AgIZfAICKXwCAjl8AgI/d6wDvxAYAvuAPAOGMBgCSXwCA44AGAID1AACB5QAAguUAAJZfAICZbR8AmMUfAJvJGwCaeRoAnXUaAJzFGwCf+QcAnhkGAJFpFgCQsesAk20XAJLNFwCV0RMAlGkSAJdREgCWzRMAg1XkAIJB5AB2XwCAml8AgIeNHQCGkRgAhTkYAISVGQCLERwAigUcAJ5fAICiXwCAj4UVAI6ZEACNORAAjJUdAJNRFACSRRQApl8AgKpfAICXYQkAlnUIAJWdCQCU+RUAm0EMAJqtDQCuXwCAsl8AgLZfAIC6XwCAvl8AgJzxDAChbQ0Awl8AgKMBBACihQAApZkEAKSRBACnGTgApsUFAKkJOACoKTgAq4k8AKoBPACtATAArB08AK8pMACunTAAseE0ALABNACzASgAsv00ALXZKAC00SgAxl8AgMpfAIDOXwCA0l8AgNZfAIDaXwCAgB0AAIEJAACC2QEA3l8AgKgRDwCpGQ8Aql0PAKtVDwCsTQ8ArXEPAK51DwCvbQ8A4l8AgOpfAICGiAAAhxABAO5fAIDyXwCA9l8AgPpfAIC4TQ4AuVEOALpRDgC7UQ4AvGUOAL1tDgC+ZQ4Avx0OALAdDwCxwQ8AssEPALPBDwC0xQ8Atc0PALbFDwC3eQ4As9UPAP5fAIACYACABmAAgApgAIC28Q8AtcUPAA5gAIC7BQ8AutkPABJgAIAWYACAvwkPAL4BDwC9FQ8AvBUPABpgAICjkQ8AHmAAgCJgAICmtQ8AJmAAgCpgAIClgQ8Aqp0PAKtBDwAuYACAMmAAgK5FDwCvTQ8ArFEPAK1RDwCogQ0AqYENAKqBDQCrgQ0ArIENAK2BDQCusQ0Ar6ENADZgAIA6YACAPmAAgEJgAIBGYACAgrkAAIG9AACAvQAAuDUCALk9AgC6zQIAu5UCALyNAgC9tQIAvr0CAL+1AgCwbQIAsU0CALJFAgCzJQIAtD0CALUdAgC2FQIAtw0CAEpgAIBOYACAswENAFJgAIC1AQ0AWmAAgISUAwC2CQ0AviwEAF5gAIC7gQIAuqECAL35AgC8mQIAv9ECAL7xAgBiYACAZmAAgGpgAICjRQ0AbmAAgKVFDQCmTQ0AcmAAgIbgBACHpAQAquUCAKvFAgCs3QIArb0CAK61AgCvlQIAqCUCAKk1AgCqPQIAqzUCAKwtAgCtkQIArpECAK+RAgB2YACAemAAgH5gAICCYACAzAAAAIZgAICKYACAjmAAgLiZAgC5rQIAuqUCALttAQC8dQEAvX0BAL51AQC/bQEAsPECALH5AgCywQIAs8ECALSxAgC1vQIAtrUCALepAgCSYACA44QOAJZgAIDh9A4AmmAAgJ5gAICiYACApmAAgIQgBQCqYACArmAAgLJgAIC2YACA7+wOALpgAIC+YACAs/UCAMJgAICG6AQAh4wEAL5cBAC2UQIAteUCAMpgAIC7fQIAunUCAM5gAIDSYACAvzkCAL41AgC9VQIAvFUCAKM1BQBWYACAxmAAgNZgAIDaYACAppEFAKUlBQDeYACAq70FAKq1BQDiYACA5mAAgK/5BQCu9QUArZUFAKyVBQCA+QcAgfkHAIKNBwCzjQYA6mAAgLWdBgC2iQYA7mAAgPJgAID2YACAuk0HALtFBwC8XQcAvUEHAL5BBwC/QQcA+mAAgP5gAIDmXwCAAmEAgAZhAIAKYQCADmEAgBJhAICoNQYAqQEGAKppBgCraQYArHkGAK1lBgCuZQYAr50HALDlBwCx7QcAsuUHALP5BwC06QcAtekHALZZBwC3VQcAuHEHALlxBwC6cQcAu3EHALxVBwC9XQcAvlUHAL9NBwCjwQcAFmEAgBphAIAeYQCAImEAgKbFBwCl0QcAJmEAgKsJBgCqAQYAKmEAgC5hAICvDQYArg0GAK0NBgCsEQYAgGkAAIFpAACCBQAAMmEAgL6YAQCEmAEANmEAgDphAICGADwAh8QBAD5hAIBCYQCARmEAgEphAIBOYQCAUmEAgKhdBgCpbQYAqmUGAKuBAQCsgQEArYkBAK6xAQCvsQEAVmEAgFphAIBeYQCAYmEAgGZhAIBqYQCAbmEAgHJhAIC4VQEAuV0BALpVAQC7yQAAvNkAAL3ZAAC+yQAAv8EAALCxAQCxuQEAsokBALOJAQC0cQEAtXEBALZ1AQC3bQEAs+0FAHZhAIB6YQCAfmEAgIJhAIC2CQIAtQkCAIZhAIC7fQIAunUCAIphAICOYQCAv7UCAL61AgC9XQIAvF0CAL5gAgCjqQUAkmEAgJZhAICmTQIAmmEAgJ5hAIClTQIAqjECAKs5AgCiYQCAhOADAK7xAgCv8QIArBkCAK0ZAgC+iDwAqmEAgKotAwCrJQMArD0DAK0lAwCuLQMAryUDAID1AACB/QAAgsEAAKPBAwCuYQCApcEDAKbBAwCyYQCAhmA8AIdUAwC2YQCAumEAgL5hAIDjqAIAwmEAgOGkAQDGYQCA71wCAMphAIDOYQCA0mEAgNZhAIDaYQCA3mEAgOJhAIDjjAcA5mEAgOE8BADqYQCA7mEAgPJhAID2YQCAhCACAPphAID+YQCAAmIAgAZiAIDvbAcACmIAgA5iAICzLQIAhEQ9ABJiAIAaYgCAHmIAgLYtAgC1LQIAImIAgLvJAgC6wQIAJmIAgCpiAIC/yQIAvsECAL3JAgC80QIA4XgHAOPAAADjOAYA4VwGAICpAACBqQAAgtEAAC5iAIAyYgCANmIAgL6kPAA6YgCAPmIAgO8cAADvkAYAQmIAgIZgPACHBD0ARmIAgLNxAQBKYgCAtRkBALYJAQBOYgCAUmIAgFZiAIC6AQEAuwEBALwBAQC9AQEAvgEBAL8BAQCohT4AqbU+AKq1PgCrxT4ArN0+AK3FPgCuwT4Ar/0+AFpiAIBeYgCAYmIAgGZiAIBqYgCAbmIAgHJiAIB2YgCAuFE/ALlRPwC6UT8Au1E/ALx1PwC9fT8AvnU/AL9tPwCwiT4AsYk+ALKZPgCzmT4AtIk+ALWJPgC2eT8At3U/AKZhAICjOT4AemIAgBZiAICmQT4AfmIAgIJiAIClUT4Aqkk+AKtJPgCGYgCAimIAgK5JPgCvST4ArEk+AK1JPgCASQAAgVEAAIJRAACzkT8AjmIAgLW5PwC2RT8AkmIAgIZAAACHBAMAukU/ALtdPwC8TT8AvT0/AL4pPwC/IT8AqE0+AKlVPgCqVT4Aq2U+AKx9PgCtiT4Arrk+AK+5PgCWYgCAmmIAgJ5iAICiYgCApmIAgKpiAICuYgCAsmIAgLhhAQC5YQEAumEBALthAQC8YQEAvWEBAL5hAQC/YQEAsM0+ALHVPgCy1T4As6U+ALShPgC1qT4Atpk+ALeZPgCj3T4AtmIAgLpiAIC+YgCAwmIAgKYJPgCl9T4AxmIAgKsRPgCqCT4AymIAgM5iAICvbT4ArmU+AK1xPgCsAT4A0mIAgNZiAIDaYgCA3mIAgOJiAIDmYgCA6mIAgO5iAICAOQAAgTkAAIIFAADyYgCAvrgBAIS4AQD6YgCA/mIAgKitAgCp1QIAqtUCAKstAwCsNQMArT0DAK41AwCvLQMAAmMAgAZjAIAKYwCADmMAgBJjAIAWYwCAGmMAgB5jAIC46QMAuekDALqJAwC7iQMAvJkDAL2ZAwC+iQMAv4kDALBVAwCxXQMAslUDALPpAwC0+QMAtfkDALbpAwC34QMAs10CACJjAICGKAQAh8wDACZjAIC2vQMAtb0DACpjAIC7mQMAupEDAC5jAIAyYwCAvz0DAL49AwC9PQMAvIEDAIUAFACjGQIANmMAgDpjAICm+QMAPmMAgEJjAICl+QMAqtUDAKvdAwBGYwCASmMAgK55AwCveQMArMUDAK15AwDjVD4A4dw/AOHQPgDjPD4ATmMAgO8cAABSYwCAVmMAgFpjAIDjwAAAXmMAgOHUAQDvYD4AYmMAgGpjAIDvRD8AgGEAAIFtAACCfQAAhAAFAIbwBACHnAUAvhAFAG5jAIByYwCAdmMAgHpjAIB+YwCAgmMAgIZjAICKYwCAjmMAgLiJPQC5iT0Aupk9ALuRPQC8uT0Avbk9AL7RPQC/0T0AsAU+ALENPgCyBT4Asx0+ALQFPgC1DT4AtgU+ALe5PQConT4Aqa0+AKqlPgCrvT4ArKU+AK2tPgCupT4Ar30+AISsBAC+rAQAkmMAgJZjAICaYwCAnmMAgKJjAICmYwCAqPkFAKn5BQCqKQYAqykGAKw5BgCtOQYArikGAK8pBgBmYwCAqmMAgK5jAICyYwCAtmMAgLpjAIC+YwCAwmMAgLiNBgC5kQYAupEGALulBgC8vQYAvUUHAL5BBwC/QQcAsFkGALFZBgCy7QYAs/0GALTtBgC13QYAttUGALe1BgCzoQYAxmMAgMpjAIDOYwCA0mMAgLa5BgC1sQYA2mMAgLudBgC6nQYA1mMAgPZiAIC/GQYAvikGAL0pBgC8OQYAglEAAKPlBgCAQQAAgUEAAKb9BgDeYwCA4mMAgKX1BgCq2QYAq9kGAIZIAACHbAAArm0GAK9dBgCsfQYArW0GAKg5BgCpWQYAqmkGAKtpBgCseQYArXkGAK5pBgCvaQYA5mMAgOpjAIDuYwCA8mMAgPZjAID6YwCA/mMAgAJkAIC4ZQEAuW0BALplAQC7fQEAvGUBAL1tAQC+ZQEAv9kBALAZBgCxGQYAsoEGALOBBgC0gQYAtYEGALaBBgC3gQYAs+EGAAZkAIAKZACADmQAgBJkAIC2+QYAtfEGABZkAIC73QYAut0GABpkAIAeZACAv0UGAL5FBgC9VQYAvFUGACJkAICjpQYAJmQAgCpkAICmvQYALmQAgDJkAICltQYAqpkGAKuZBgA2ZACAOmQAgK4BBgCvAQYArBEGAK0RBgConQIAqdECAKrRAgCrLQMArDUDAK09AwCuNQMAry0DAD5kAIBCZACAvmQCAEpkAIBOZACAUmQAgFZkAIBaZACAuOkDALnpAwC6iQMAu4UDALydAwC9gQMAvoEDAL+1AwCwVQMAsV0DALJVAwCz6QMAtPkDALX5AwC26QMAt+EDAIBtAwCBpQAAgq0AALNVAgBeZACAtbEDALaxAwBiZACAhOACAGZkAIC6nQMAu5UDALyNAwC9MQMAvjEDAL8xAwCjGQIAamQAgIVwaQBuZACAcmQAgKb9AwCl/QMAdmQAgKvZAwCq0QMAhkgMAIe8AwCvfQMArn0DAK19AwCswQMAemQAgH5kAICCZACAhmQAgO+wBgDvxAMAimQAgI5kAIDjfAYA45QDAOG4BwDh3AEAkmQAgJZkAICaZACAnmQAgKJkAICmZACAhEQCAL5YDQCADQAAgTUAAII9AACqZACArmQAgLJkAICGyAwAh1wNALpkAIC+ZACAwmQAgMZkAIDKZACAzmQAgNJkAIDWZACA2mQAgN5kAIDiZACA74AGAISsDQDh7AYA5mQAgONcBgDqZACA7mQAgPJkAID2ZACAs/UBAPpkAID+ZACAAmUAgAZlAIC2RQEAteUBAAplAIC7LQEAuiEBAA5lAIASZQCAv/UAAL71AAC9JQEAvC0BAKgtDgCpNQ4Aqj0OAKs1DgCsLQ4ArYUOAK6FDgCvuQ4AtmQAgBZlAIAaZQCAHmUAgIAZAACBGQAAggUAACJlAIC4WQ8AuVkPALp5DwC7eQ8AvGkPAL1pDwC+GQ8AvxkPALClDgCxqQ4AsrkOALOxDgC0cQ8AtXEPALZxDwC3cQ8Apb0OAL6IAwAqZQCAph0OACZlAIAuZQCAo60OADJlAICtfQ4ArHUOAK+tDwCurQ8ARmQAgDZlAICrdQ4AqnkOALO5DwA6ZQCAhmgAAIcMAwA+ZQCAtlEPALVZDwBCZQCAu3UPALp1DwBGZQCASmUAgL9FDwC+RQ8AvVEPALxlDwCocQ4AqXEOAKpxDgCrcQ4ArJEOAK2RDgCukQ4Ar5EOAE5lAIBSZQCAVmUAgFplAIBeZQCAYmUAgGZlAIBqZQCAuIUOALmNDgC6hQ4Au50OALyNDgC9vQ4AvrUOAL95AQCw8Q4AsfEOALLxDgCzxQ4AtMEOALXBDgC2wQ4At8EOAKP5DgBuZQCAcmUAgHZlAIB6ZQCAphEOAKUZDgB+ZQCAqzUOAKo1DgCCZQCAhmUAgK8FDgCuBQ4ArREOAKwlDgCADQAAgRUAAIIdAACKZQCAjmUAgJJlAICElAEAvpQBAIZABwCH5AAAmmUAgJ5lAICiZQCApmUAgKplAICuZQCAqIkCAKmRAgCqlQIAq7kCAKzVAgCtxQIArsUCAK/1AgCyZQCAtmUAgLplAIC+ZQCAvnwDAMJlAIDGZQCAymUAgLh9AwC5wQMAusEDALvBAwC8wQMAvckDAL7xAwC/8QMAsI0CALFFAwCyTQMAs0UDALRdAwC1RQMAtk0DALdFAwCzHQIAzmUAgNJlAIDWZQCA2mUAgLZFAgC1XQIA3mUAgLuBAwC6SQIA4mUAgOZlAIC/gQMAvpkDAL2RAwC8mQMA6mUAgKNZAgDuZQCA8mUAgKYBAgD2ZQCA+mUAgKUZAgCqDQIAq8UDAP5lAIACZgCArt0DAK/FAwCs3QMArdUDAIDZAQCB7QEAguUBAO+4DgAKZgCA4cQBAISYAgDj1AAADmYAgL7sBAASZgCA7wgAABZmAIDhxA8AGmYAgONkDgCGAAUAh2gFAB5mAICzvQIAImYAgLWtAgC2pQIAJmYAgCpmAIAuZgCAukEBALtBAQC8RQEAvU0BAL5FAQC/+QEAMmYAgDZmAIA6ZgCAPmYAgEJmAIBGZgCASmYAgO/gAQCEbAQA4dQOAE5mAIDjHA4AUmYAgFZmAIBaZgCAXmYAgKMxAgBiZgCAhCQHAGZmAIBqZgCApikCAKUhAgBuZgCAq80BAKrNAQByZgCAemYAgK91AQCuyQEArcEBAKzJAQCo6QUAqekFAKr5BQCr+QUArOkFAK3pBQCuOQYArzkGAAZmAICCzQcAgfUHAID9BwB2ZgCAfmYAgIYYAwCHkAMAuNEGALnZBgC64QYAu+EGALyRBgC9nQYAvpUGAL+JBgCwSQYAsUkGALJdBgCzVQYAtE0GALXxBgC28QYAt/EGALDhBwCx4QcAsgkHALMJBwC0GQcAtRkHALYJBwC3CQcAuDkHALkNBwC6GQcAuxkHALwJBwC9CQcAvn0HAL9xBwCCZgCAlmUAgIZmAICKZgCAjmYAgJJmAICWZgCAmmYAgKjxBwCpxQcAqsEHAKvdBwCsyQcArb0HAK6pBwCvoQcAsykGAJ5mAICiZgCApmYAgKpmAIC2XQYAtSEGAK5mAIC7RQYAukUGALJmAIC2ZgCAv70GAL69BgC9vQYAvL0GALpmAICjbQYAvmYAgMJmAICmGQYAxmYAgMpmAIClZQYAqgEGAKsBBgDOZgCA0mYAgK75BgCv+QYArPkGAK35BgCobQYAqbEBAKpJAQCrRQEArF0BAK1FAQCuTQEAr0UBANZmAICCHQAAgR0AAIAdAADaZgCA3mYAgOJmAIC+VAEAuIEAALmNAAC6hQAAu5kAALyJAAC9vQAAvrUAAL99AACwPQEAseEAALLhAACz4QAAtOEAALXpAAC20QAAt9EAALsFAwC62QIAhiwCAIcsAwC/DQMAvgUDAL0VAwC8FQMAs+ECAOpmAIDuZgCAhCwDAPJmAIC25QIAtfUCAPZmAICqnQIAq0EDAPpmAID+ZgCArkEDAK9JAwCsUQMArVEDAAJnAICjpQIABmcAgApnAICmoQIADmcAgBJnAIClsQIAqakAAKihAACrtQAAqr0AAK3dAACs3QAAr/EAAK79AAC+LBwAFmcAgBpnAIAeZwCAImcAgCZnAIAqZwCALmcAgLl9AAC4fQAAu80BALrNAQC93QEAvN0BAL/NAQC+zQEAsZUAALCJAACzTQAAspUAALVdAAC0XQAAt00AALZNAAAyZwCANmcAgDpnAIA+ZwCAQmcAgEZnAIBKZwCATmcAgIA5AACBOQAAggUAAFJnAIBaZwCAXmcAgIf4AgCGfB0A4bgEAL7IHADjQAYAYmcAgGZnAIBqZwCAbmcAgHJnAIB2ZwCAemcAgH5nAICCZwCAhmcAgIpnAIDvsAcAjmcAgJJnAICWZwCAmmcAgO/IAACeZwCAomcAgKZnAIDvQAYAqmcAgOH8BgCuZwCA4xwGALJnAIDhlAEAtmcAgONkBgCAEQAAgRkAAIIpAACz/QEAumcAgLWdAQC2lQEAvmcAgMJnAICEbB0AuoUBALuZAQC8iQEAvVEBAL5RAQC/UQEAozEeAFZnAIDGZwCAymcAgM5nAICmWR4ApVEeANJnAICrVR4AqkkeAIYIAwCHbAMAr50eAK6dHgCtnR4ArEUeANZnAICzCR8A2mcAgN5nAIC2CR8A4mcAgOZnAIC1CR8AugUfALsNHwDqZwCA7mcAgL4FHwC/CR8AvBUfAL0NHwCw5R8Ase0fALLlHwCz/R8AtOUfALXpHwC2GR8AtxkfALgpHwC5NR8Auj0fALs1HwC8ER8AvR0fAL4JHwC/BR8A8mcAgPZnAIDmZgCA+mcAgP5nAIACaACABmgAgApoAICo0R8AqdEfAKqlHwCrvR8ArKUfAK2tHwCupR8Ar50fAKNNHgAOaACAEmgAgBZoAIAaaACApk0eAKVNHgAeaACAq0keAKpBHgAiaACAJmgAgK9NHgCuQR4ArUkeAKxRHgCADQAAgRUAAIIdAAAqaACALmgAgDJoAICEtAEAvrQBAL/oAQA6aACAhkgHAIc0AACEvAYAPmgAgEJoAIC+tAYAqI0BAKmVAQCqlQEAq80BAKzZAQCt2QEArs0BAK/FAQBGaACASmgAgE5oAIBSaACAVmgAgFpoAIBeaACAYmgAgLgdAQC5wQAAusEAALvBAAC8wQAAvckAAL7xAAC/8QAAsIkBALGJAQCyKQEAsykBALQ9AQC1JQEAti0BALclAQC7bQIAum0CAGZoAIBqaACAv8ECAL7ZAgC93QIAvN0CALM9AgBuaACAcmgAgHZoAICE/AYAtnkCALVxAgB6aACAqikCAKspAgB+aACAgmgAgK6dAgCvhQIArJkCAK2ZAgCGaACAo3kCAIpoAICOaACApj0CAJJoAICWaACApTUCAIJtJwCDjSoAhqgFAIdsAwCGmS4Ah80vAIQRLgCFmS4AiiESAIspEgCaaACAnmgAgI6RFgCPHRYAjBESAI0RFgCScRoAk+UaAKJoAIDvlHYAlvEeAJflHgCUSRoAlRkeAJopAgCb4QIAqmgAgK5oAICyaACA4SASAJzxAgDjIBYAnyEfAJ7BHwCdmRsAnC0bAJuhGwCavRcAmTkXAJixFwCXiRMAlqkTAJWpEwCUdS4AkzkvAJIxLwCRsS8AkDUrAI+tJgDjeB8A0gAAAOFcHwCCmQEAtmgAgIDxAQCB8QEAvqgHALpoAIC+aACAwmgAgIS8BgDvLB8AxmgAgMpoAIDhpB4A48wAAON8HgDhvAEAzmgAgNJoAIDWaACAhJwGANpoAIC+bAYA3mgAgOJoAIDmaACA7xAAAO8EHgDqaACA7mgAgPJoAID2aACA+mgAgP5oAIACaQCABmkAgAppAICAPQAAgQkAAILJBwAOaQCAo/kDAKLxAwChMQMAoM0fALBJcQCxAXwAsgl8ALMhfQC0AXgAtRV4ADZoAICmaACAEmkAgL4oDgCGDAAAh4wDABZpAIAaaQCAHmkAgCJpAIAmaQCAoV0AAKJVAACjfQAApAEMAKUVDACm9QwApwEIAKghCACpxQgAqgF0AKsJdACsAXQArR11AK55cACveXAAqOUFAKnxBQCq8QUAqy0FAKw1BQCtPQUArjUFAK8tBQAqaQCALmkAgDJpAIA2aQCAOmkAgD5pAIBCaQCARmkAgLj9BgC5jQYAuoUGALutBgC8uQYAvbkGAL6tBgC/pQYAsFUFALFdBQCyVQUAs+UGALT9BgC10QYAttEGALfRBgCzeQQASmkAgE5pAIBSaQCAVmkAgLa9BAC1vQQAWmkAgLuZBAC6kQQAXmkAgGJpAIC/FQcAvjkHAL0xBwC8gQQAZmkAgKM9BABqaQCAbmkAgKb5BAByaQCAdmkAgKX5BACq1QQAq90EAHppAIB+aQCArn0HAK9RBwCsxQQArXUHAKhpBwCpaQcAqnkHAKvZBgCs9QYArf0GAK71BgCv5QYAgMkAAIHJAACCBQAAgmkAgIZwDwCHNAAAimkAgI5pAIC4fQYAuQUGALoNBgC7BQYAvB0GAL0FBgC+DQYAvwUGALCdBgCxdQYAsn0GALN1BgC0UQYAtV0GALZVBgC3TQYAs/EEAJJpAICWaQCAmmkAgJ5pAIC2fQUAtX0FAKJpAIC7sQUAulkFAKZpAICqaQCAv5kFAL6VBQC9oQUAvKkFAK5pAICjtQQAsmkAgLZpAICmOQUAumkAgL5pAIClOQUAqh0FAKv1BQDCaQCAxmkAgK7RBQCv3QUArO0FAK3lBQCpuQIAqLECAKvJAgCqsQIArTUCAKw1AgCvNQIArjUCAMppAIDOaQCA0mkAgNZpAIDaaQCA3mkAgOJpAIDmaQCAuekDALjZAwC7iQMAuuEDAL2dAwC8nQMAv4EDAL6JAwCxVQIAsFUCALNVAgCyVQIAtfkDALTxAwC36QMAtvEDALM9AwDqaQCA7mkAgPJpAID6aQCAtrEDALW5AwD+aQCAu5UDALqVAwCGiAwAh6ANAL85AgC+MQIAvYUDALyFAwACagCAo3kDAAZqAIAKagCApvUDAA5qAIASagCApf0DAKrRAwCr0QMAFmoAgBpqAICudQIAr30CAKzBAwCtwQMAgIUAAIGNAACChQAA79AGAOOwBwDj9AQA4QgHAOHsBADvOAYA7yAEAL6kDAAeagCAImoAgOGEAQAmagCA49wGACpqAIAuagCAhMANALPJAQAyagCAtdkBALbJAQA2agCAOmoAgD5qAIC6xQEAu60BALy5AQC9uQEAvq0BAL+lAQCwLQ4AsUUOALJBDgCzQQ4AtEUOALVNDgC2cQ4At3EOALiBDgC5gQ4AuoEOALuBDgC8gQ4AvYEOAL6BDgC/gQ4A9mkAgEJqAIBGagCASmoAgIZpAIBOagCAUmoAgFZqAICo2Q0AqdkNAKptDgCrZQ4ArH0OAK1lDgCuZQ4Ar1UOAKOFDgCCLQAAgRUAAIAdAABaagCApoUOAKWVDgBeagCAq+EOAKqJDgBiagCAZmoAgK/pDgCu4Q4ArfUOAKz1DgBqagCAs4UPAIZoAACHHAMAtoUPAG5qAIByagCAtZEPALqNDwC7SQ8AdmoAgHpqAIC+MQ8AvzEPALxJDwC9RQ8AqBEOAKkZDgCqSQ4Aq0UOAKxdDgCtQQ4ArkEOAK91DgB+agCAgmoAgIZqAICKagCAjmoAgJJqAICWagCAmmoAgLihDgC5oQ4Aug0BALsFAQC8HQEAvQEBAL4BAQC/AQEAsA0OALHJDgCy2Q4As9UOALSxDgC1sQ4AtqkOALehDgCjwQ4AnmoAgKJqAICmagCAqmoAgKbBDgCl1Q4ArmoAgKsNDgCqyQ4AsmoAgLZqAICvdQ4ArnUOAK0BDgCsDQ4AumoAgL5qAIDCagCAxmoAgIANAACBNQAAgj0AAMpqAIDOagCA0moAgISEAQC+hAEAhjAHAIf4AADaagCA3moAgKjBAgCp0QIAqtECAKvlAgCs/QIArTUDAK49AwCvNQMA4moAgOZqAIDqagCA7moAgPJqAID2agCA+moAgP5qAIC40QMAudkDALrhAwC74QMAvJEDAL2RAwC+kQMAv5EDALBNAwCxVQMAsl0DALNVAwC0TQMAtfEDALbxAwC38QMAu7EDALqpAwACawCAvoQDAL8VAwC+qQMAvaEDALypAwCzeQIABmsAgAprAIAOawCAEmsAgLaVAwC1VQIAFmsAgKrtAwCr9QMAGmsAgB5rAICu7QMAr1EDAKztAwCt5QMAImsAgKM9AgAmawCAKmsAgKbRAwAuawCAMmsAgKURAgA2awCAgiEAAIEVAACAFQAA7wQAAISUAgA6awCAPmsAgOPYAABCawCA4fgBAEprAIBOawCAUmsAgFZrAIBaawCAhmAFAIcIBQBeawCAs20BAGJrAIC1fQEAtnUBAGZrAIBqawCAbmsAgLpRAQC7UQEAvPkBAL3RAQC+0QEAv9EBAHJrAICjpQEAdmsAgHprAICmvQEAfmsAgIJrAICltQEAqpkBAKuZAQCGawCAimsAgK4ZAQCvGQEArDEBAK0ZAQCOawCA4fQOAJJrAIDjFA4A9AAAAOF8DACWawCA41AKAJprAICeawCAviAEAO8wDQCiawCApmsAgIQ0BADvrA4AsDkGALE5BgCygQYAs6kGALS5BgC1uQYAtqkGALehBgC46QYAuekGALrJBgC7xQYAvN0GAL3BBgC+wQYAvz0HAEZrAICCHQAAgR0AAIAdAACqawCArmsAgLJrAIDWagCAqJkFAKmZBQCqSQYAq0kGAKxZBgCtWQYArkkGAK9JBgCorQcAqbUHAKq9BwCrtQcArK0HAK3dBwCuyQcAr8EHALZrAIC6awCAhogDAIcQAwC+awCAwmsAgMZrAIDKawCAuG0HALkFBwC6AQcAuxUHALwxBwC9MQcAvikHAL8pBwCwgQcAsYEHALJpBwCzZQcAtH0HALVhBwC2YQcAt1UHALM1BgDOawCA0msAgNZrAIDaawCAtl0GALUlBgDeawCAu0UGALpFBgDiawCA5msAgL+lBgC+uQYAvbEGALy9BgDqawCAo3EGAO5rAIDyawCAphkGAPZrAID6awCApWEGAKoBBgCrAQYA/msAgAJsAICu/QYAr+EGAKz5BgCt9QYAqCUBAKk1AQCqPQEAqzUBAKwtAQCtkQAArpEAAK+RAAAGbACACmwAgA5sAIASbACAFmwAgIK9AwCBvQMAgL0DALiZAAC5rQAAuqUAALttAAC8dQAAvX0AAL51AAC/bQAAsPEAALH5AACywQAAs8EAALSxAAC1vQAAtrUAALepAAAabACAHmwAgCJsAICEgAIAvhwCACpsAICG+HwAh8wCAISsAwAubACAMmwAgDZsAIA6bACAPmwAgEJsAIBGbACAs/UCAEpsAIBObACAkgAAAFJsAIC2UQMAteUCAFZsAIC7fQMAunUDAFpsAIBebACAvzkDAL41AwC9VQMAvFUDAKM1AgBibACAZmwAgGpsAIBubACAppEDAKUlAgBybACAq70DAKq1AwB2bACAemwAgK/5AwCu9QMArZUDAKyVAwC+wAMAfmwAgIJsAICGbACAgA0AAIE1AACCPQAAimwAgI5sAICSbACAhsh8AIcAAwCabACAnmwAgKJsAICmbACAqmwAgK5sAICybACAtmwAgLpsAIC+bACAwmwAgO/0AwCE7HwA4ZQBAMZsAIDjMAMAymwAgM5sAIDSbACA1mwAgLNpAQDabACA3mwAgOJsAIDmbACAtmEBALVpAQDqbACAuykBALohAQDubACA8mwAgL8dAQC+HQEAvSUBALwtAQD2bACA+mwAgP5sAICjpQEAAm0AgKWlAQCmrQEAvlR8AIaAfACH7HwAqu0BAKvlAQCs4QEArekBAK7RAQCv0QEACm0AgOGcBgCEBH8A4yQGAOPUBgAObQCA4TAEABJtAIDvlAcAgnUAAIFhAACAaQAAFm0AgBptAIAebQCA7+wGALiNfgC5lX4AupV+ALulfgC8vX4AvdF+AL7RfgC/0X4AsGV+ALFtfgCyeX4As3F+ALRZfgC1WX4Atr1+ALe1fgCoVX4AqWF+AKphfgCrYX4ArGF+AK1hfgCuYX4Ar2F+ACJtAICWbACAJmwAgCZtAIAGbQCAKm0AgC5tAIAybQCAqHF+AKlxfgCqcX4Aq3F+AKyRfwCtkX8ArpF/AK+RfwA2bQCAOm0AgD5tAIBCbQCARm0AgEptAIBObQCAUm0AgLiFfwC5jX8AuoV/ALudfwC8jX8Avb1/AL61fwC/XX8AsPF/ALHxfwCy8X8As8V/ALTBfwC1wX8AtsF/ALfBfwCz+X8AVm0AgFptAIBebQCAYm0AgLYRfgC1GX4AZm0AgLs1fgC6NX4Aam0AgG5tAIC/BX4AvgV+AL0RfgC8JX4AghUAAKO9fwCAYQAAgWEAAKZVfgBybQCAvpABAKVdfgCqcX4Aq3F+AHZtAIB6bQCArkF+AK9BfgCsYX4ArVV+AKhBfgCpUX4AqlV+AKt9fgCsZX4ArW1+AK75AQCv8QEAhgAAAIc0AQB+bQCAgm0AgIZtAICKbQCAjm0AgJJtAIC4dQEAuX0BALp1AQC7yQAAvNkAAL3ZAAC+yQAAv8EAALCVAQCxnQEAspUBALNNAQC0VQEAtV0BALZVAQC3TQEAs919AJZtAICabQCAnm0AgKJtAIC27X0Ate19AKZtAIC7WQIAulECAKptAICubQCAv5kCAL6RAgC9mQIAvEECALJtAICjmX0Atm0AgLptAICmqX0Avm0AgMJtAIClqX0AqhUCAKsdAgDGbQCAym0AgK7VAgCv3QIArAUCAK3dAgDObQCA0m0AgNZtAIDabQCAgB0AAIEJAACCOQAA3m0AgOJtAIC+AAQA6m0AgO5tAIDybQCA9m0AgPptAID+bQCAhIwDAAJuAICHCAMAhuwEAAZuAIDviAIACm4AgA5uAICEbAQA4zQCABJuAIDhVAEAFm4AgBpuAIAebgCAIm4AgKhtAgCprQIAqqUCAKu9AgCspQIAra0CAK6lAgCvGQEAvqwEACZuAIAqbgCALm4AgDJuAIA2bgCAOm4AgD5uAIC4DQEAuREBALoRAQC7JQEAvD0BAL3VAQC+3QEAv9UBALBpAQCxaQEAsnkBALNxAQC0WQEAtVkBALY5AQC3NQEAsy0CAEJuAIBGbgCASm4AgE5uAIC2LQIAtS0CAFJuAIC7rQEAuq0BAFpuAIBebgCAv50BAL6dAQC9pQEAvK0BAIBNAACBVQAAglUAAO9sAABibgCA7+x/AO+8fgBmbgCA4RB/AOPUfwDj2H4A4ex/AGpuAIDhTH4Abm4AgOMkfgDmbQCAVm4AgKsFBgCqBQYArQ0GAKwFBgCvNQYArjUGAIYAAwCHKAMAo4UFAHJuAIClhQUAdm4AgHpuAICmhQUAs/EGAH5uAICCbgCAhm4AgIpuAIC26QYAteEGAI5uAIC7vQYAur0GAJJuAICWbgCAv4kGAL6BBgC9iQYAvJUGAKgpBgCpKQYAqjkGAKs5BgCsKQYArSkGAK5dBgCvTQYAmm4AgJ5uAICibgCApm4AgKpuAICubgCAsm4AgLZuAIC46QcAuekHALr5BwC7+QcAvOkHAL3pBwC+XQcAv1UHALA5BgCxOQYAsgEGALMdBgC0BQYAtQ0GALYFBgC32QcAo7EHAIItAACBFQAAgB0AALpuAICmqQcApaEHAL5uAICr/QcAqv0HAMJuAICEpAIAr8kHAK7BBwCtyQcArNUHAL7MAQCzlQYAxm4AgMpuAIC2qQYAzm4AgNJuAIC1rQYAulkBALshAQCGyAAAhwwBAL4hAQC/KQEAvDEBAL0xAQCoKQYAqSkGAKpZBgCrUQYArGEGAK1tBgCutQEAr6kBAITgAQDWbgCA2m4AgN5uAIDibgCA5m4AgOpuAIDubgCAuGEBALlhAQC6YQEAu2EBALxhAQC9YQEAvmEBAL9hAQCw2QEAsaEBALKhAQCzoQEAtKEBALWpAQC2kQEAt5EBAKPRBQDybgCA9m4AgPpuAID+bgCApu0FAKXpBQACbwCAq2UCAKodAgAGbwCACm8AgK9tAgCuZQIArXUCAKx1AgAObwCAEm8AgBZvAIAabwCAHm8AgCJvAIAmbwCAKm8AgIA9AACBCQAAghkAAC5vAIAybwCAOm8AgL48AwA+bwCAhgAMAIcUAwBCbwCAs9UDAEZvAIC1PQMAtjUDAEpvAIBObwCAv4wKALoRAwC7EQMAvLUAAL29AAC+tQAAv60AAFJvAIDjdAEAVm8AgOG8AQBabwCAXm8AgGJvAIBmbwCAam8AgG5vAIBybwCAdm8AgHpvAIDvdAIAfm8AgIJvAICoTQIAqVECAKpRAgCrqQIArLkCAK25AgCuqQIAr6kCAIRsDQCGbwCAim8AgI5vAICSbwCAlm8AgJpvAIC+dA0AuG0BALkFAQC6DQEAuwUBALwdAQC9BQEAvg0BAL8FAQCw2QIAsdkCALJtAQCzZQEAtH0BALVlAQC2ZQEAt1UBAOG4AQDhUAcA47QAAON8BwCAqQAAgQkAAII5AACebwCAom8AgKpvAICubwCAsm8AgO4AAAC2bwCA7wAAAO9kBgCGYAwAh+QMAKORAgC6bwCApXkCAL5vAIDCbwCApnECAMZvAIDKbwCAq1UCAKpVAgCt+QEArPEBAK/pAQCu8QEApm8AgDZvAIDObwCA0m8AgNZvAIDabwCA3m8AgOJvAICoVQ4AqVkOAKqhDgCrvQ4ArK0OAK2VDgCu+Q4Ar/UOALCRDgCxkQ4AspEOALORDgC0sQ4AtbEOALaxDgC3sQ4AuJEOALmdDgC6lQ4Au0kPALxZDwC9WQ8AvkkPAL9JDwCzCQ4A5m8AgOpvAIDubwCA8m8AgLY1DgC1BQ4A9m8AgLt1DgC6dQ4A+m8AgP5vAIC/VQ4AvlUOAL1lDgC8ZQ4AAnAAgKNNDgAGcACACnAAgKZxDgAOcACAEnAAgKVBDgCqMQ4AqzEOAISkAwC+pAMArhEOAK8RDgCsIQ4ArSEOAKilDgCprQ4AqqUOAKu5DgCs3Q4ArcEOAK7BDgCv/Q4AgO0BAIHxAQCC8QEAFnAAgIaQAQCHtAEAGnAAgB5wAIC4yQEAuckBALrZAQC70QEAvPkBAL35AQC+mQEAv5UBALCFDgCxbQEAsmUBALN9AQC0ZQEAtW0BALZlAQC3+QEAsy0OACJwAIAmcACAKnAAgC5wAIC2QQ4AtVUOADJwAIC7qQEAukEOADZwAIA6cACAv6kBAL6hAQC9qQEAvLEBAD5wAICjaQ4AQnAAgEZwAICmBQ4ASnAAgE5wAIClEQ4AqgUOAKvtAQBScACAVnAAgK7lAQCv7QEArPUBAK3tAQCoOQMAqTkDAKqNAwCrhQMArJ0DAK2FAwCuhQMAr7UDAFpwAIBecACAYnAAgGZwAIBqcACAbnAAgHJwAIB2cACAuGEAALlhAAC6YQAAu2EAALxhAAC9YQAAvmEAAL9hAACwzQMAsaUDALKhAwCzoQMAtKUDALWtAwC2kQMAt5EDAIANAACBEQAAghEAAHpwAIDv9AIAfnAAgIJwAIC+HAMA4xQCAISIAgDhgAEAinAAgI5wAICScACAh8gDAIY8BAC7AQMAumkDAJZwAICacACAvwkDAL4BAwC9FQMAvBUDALNlAwCecACAonAAgKZwAICqcACAtmUDALV1AwCucACAsnAAgLZwAIC6cACAo4kCAL5wAIClmQIApokCAMJwAICELAIAxnAAgKqFAgCr7QIArPkCAK35AgCu7QIAr+UCAMpwAIDOcACAvkQFAIRMBQDScACA1nAAgNpwAIDecACA4nAAgOZwAIDqcACA7nAAgIAZAACBGQAAggUAAPJwAIDhGA8A4VwOAOO4DgDjdAEA+nAAgP5wAIACcQCABnEAgIYABACHZAUACnEAgA5xAIAScQCAFnEAgO98DgDvqAEAs3UBABpxAIAecQCAInEAgCZxAIC2MQEAtRUBACpxAIC7HQEAuhUBAC5xAIAycQCAv+EAAL79AAC9/QAAvP0AAPZwAIA2cQCAOnEAgD5xAICGcACAQnEAgEZxAIBKcQCAqI0GAKmVBgCqnQYAq+UGAKz9BgCt0QYArtEGAK/RBgCwsQYAsbkGALJJBwCzSQcAtFkHALVFBwC2RQcAt3kHALghBwC5IQcAujkHALs5BwC8KQcAvSkHAL4ZBwC/GQcAozUGAE5xAIBScQCAVnEAgFpxAICmcQYApVUGAF5xAICrXQYAqlUGAGJxAIC+oAMAr6EHAK69BwCtvQcArL0HAIBRAACBWQAAgmEAALNVBwCF9AAAtX0HALZ1BwBmcQCAhgAcAIfkAQC6LQcAuyUHALw9BwC9JQcAviUHAL8VBwCokQYAqZEGAKqRBgCrkQYArLkGAK25BgCuqQYAr6kGAGpxAIBucQCAcnEAgHZxAICiIQEAozUBAKA5BQChEQQAuEkBALlJAQC6XQEAu1UBALxNAQC90QEAvtEBAL/RAQCwpQYAsa0GALKlBgCzvQYAtK0GALWdBgC2lQYAt3kBAKMZBgCPnXkAenEAgH5xAICCcQCApjkGAKUxBgCGcQCAq2kGAKphBgCKcQCAjnEAgK9ZBgCuaQYArWkGAKxxBgCeiQgAn8EFAJzJCQCdyQkAmqENAJu9DACYsQ0AmbkNAJahcQCXRXEAlEV1AJWxcQCSoXUAk7V1AJDleQCRzXkAil1yAItFcgCScQCAvoAcAI51DgCPZQ4AjLlyAI11DgCCOXoAgzl6AJZxAICacQCAhnF2AIeZdgCECXoAhW12AJptBwCbVQIAnnEAgKJxAICmcQCA4ZAAAJxZAgDjCBoAkgkPAJNlCgCqcQCA7zgWAJZ1BgCXdQYAlH0KAJU1CwCpjRYAqIUWAKsBEACqMRYArXESAKy1EgCvuS4ArgEsAKF9AgCucQCAo6EeAKKpHgClsRoApPUfAKflGwCmsRoAhMwDAIRMHACycQCAtnEAgLpxAIC+cQCAwnEAgMZxAICxASgAsNkuALONKgCy6SoAtfUmALQBJACEcB0AynEAgID9AQCBFQAAgh0AAL6AHADOcQCA0nEAgIe4AgCGPB0A2nEAgN5xAIDicQCA5nEAgOpxAIDucQCA8nEAgPZxAID6cQCA/nEAgAJyAIAGcgCA44ADAApyAIDhoAEADnIAgO+UAwAScgCAFnIAgBpyAIAecgCAInIAgCZyAIAqcgCALnIAgOE8BgAycgCA49AGADZyAIDhMAcAOnIAgOOsBgCAOQAAgRUAAIIdAADvHAYAPnIAgEJyAIC+uB8A7+gBALPpAgBKcgCAh8QcAIbsHABOcgCAtlkCALVRAgBScgCAu00CALpNAgBWcgCAWnIAgL+5AQC+2QEAvdEBALz1AQCjKR0A1nEAgEZyAIBecgCAYnIAgKaZHQClkR0AZnIAgKuNHQCqjR0AanIAgG5yAICveR4ArhkeAK0RHgCsNR4AcnIAgLNtHwB2cgCAenIAgLZlHwB+cgCAgnIAgLVtHwC6IR8AuyEfAIZyAICKcgCAviUfAL8pHwC8MR8AvTEfAKihHwCpoR8AqqEfAKuhHwCsoR8AraEfAK6hHwCvoR8AjnIAgJJyAICWcgCAmnIAgJ5yAICicgCApnIAgKpyAIC4rR8AubUfALq9HwC7tR8AvK0fAL1VHwC+UR8Av00fALChHwCxoR8AsqEfALOhHwC0pR8AtakfALadHwC3lR8AoykeAIIZAACBGQAAgLEBAK5yAICmIR4ApSkeALJyAICrZR4AqmUeAIaIAACH/AEAr20eAK5hHgCtdR4ArHUeALZyAICzmR4AunIAgL5yAIC2XQEAwnIAgMZyAIC1sR4AukkBALtJAQDKcgCAznIAgL49AQC/IQEAvDkBAL01AQCoRR4AqVUeAKpVHgCrZR4ArH0eAK2ZAQCuiQEAr4EBAISsAADScgCA1nIAgNpyAIDecgCA4nIAgOZyAIDqcgCAuK0BALllAQC6bQEAu2UBALx9AQC9ZQEAvm0BAL9lAQCwyQEAsckBALKpAQCzpQEAtL0BALWhAQC2oQEAt5UBALhpHAC5oRwAusEcALvBHAC8wRwAvcEcAL7BHAC/wRwAsIkfALGJHwCyIRwAswUcALQdHAC1fRwAtnUcALdtHACoYR8AqWEfAKphHwCrYR8ArNkfAK3ZHwCuyR8Ar8EfAO5yAIDycgCA9nIAgPpyAID+cgCAAnMAgAZzAIAKcwCADnMAgBJzAIC+AAQAo1EdABZzAICleR0AppUCABpzAIAecwCAInMAgKqBAgCrgQIArPECAK39AgCu9QIAr+kCACpzAIDh9AEALnMAgON8AQCATQAAgXUAAIJ9AAAycwCAhsAEAIekBAA2cwCAOnMAgD5zAIBCcwCARnMAgO+MAgCoSQIAqUkCAKpdAgCrVQIArHkCAK15AgCuvQIAr7UCAISgBQBKcwCATnMAgFJzAIC+vAQAVnMAgFpzAIBecwCAuC0BALk1AQC6PQEAuzUBALwtAQC91QEAvt0BAL/NAQCwzQIAsdUCALLdAgCz1QIAtM0CALUVAQC2HQEAtxUBAOGEHgDjbB8A41wfAOFYHgBicwCAZnMAgGpzAIBucwCAcnMAgHZzAIB6cwCAfnMAgOkAAADv9B4A70weAIJzAICzlQIAhnMAgIpzAICOcwCAknMAgLa5AgC1sQIAmnMAgLtRAgC6SQIAhsgEAIesBAC/kQEAvkkCAL1BAgC8SQIAJnMAgKNRBQCecwCAlnMAgKZ9BQCicwCApnMAgKV1BQCqjQUAq5UFAKpzAICucwCAro0FAK9VBgCsjQUArYUFAICJBwCBiQcAgpkHALORBgCycwCAtbkGALapBgC2cwCAunMAgL5zAIC6TQcAu0UHALxdBwC9QQcAvkEHAL9BBwCoQQYAqU0GAKpVBgCrZQYArH0GAK1lBgCubQYAr2UGAMJzAIDGcwCAynMAgM5zAIDScwCA1nMAgNpzAIDecwCAuFkHALlZBwC6aQcAu2kHALx5BwC9eQcAvmUHAL8ZBwCwxQcAsc0HALLFBwCz2QcAtMkHALXJBwC2aQcAt2kHAKPdBwDicwCA5nMAgOpzAIDucwCApuUHAKX1BwDycwCAqwkGAKoBBgD2cwCA+nMAgK8NBgCuDQYArQ0GAKwRBgCAbQAAgQkAAIIZAAD+cwCAAnQAgISYAQC+kAEABnQAgIbAAACH5AEACnQAgA50AIASdACAFnQAgBp0AIAedACAqF0GAKmNAQCqnQEAq5UBAKy5AQCtuQEArskBAK/BAQCEoAAAInQAgCZ0AIAqdACALnQAgDJ0AIA2dACAOnQAgLh5AQC5eQEAus0AALvFAAC83QAAvcUAAL7FAAC/9QAAsIEBALGBAQCySQEAs0kBALRZAQC1WQEAtkkBALdJAQCzFQIAPnQAgEJ0AIBGdACASnQAgLY5AgC1MQIATnQAgLtFAgC6RQIAUnQAgFZ0AIC/nQIAvp0CAL2dAgC8nQIAhXw+AKNRAgBadACAXnQAgKZ9AgBidACAZnQAgKV1AgCqAQIAqwECAGp0AIBudACArtkCAK/ZAgCs2QIArdkCAIDpAACB6QAAggUAAHJ0AIC+AAwAenQAgIeoAwCGvAwAfnQAgIJ0AICGdACAinQAgI50AICSdACAlnQAgJp0AICedACAonQAgKZ0AICqdACA42ABAK50AIDhoAEAsnQAgO+IAgC2dACAunQAgL50AIDCdACAxnQAgMp0AIDOdACAqGkCAKlpAgCqeQIAq3kCAKxpAgCtaQIArr0CAK+1AgC+rAwA0nQAgNZ0AIDadACAgB0AAIEJAACCqQAA3nQAgLhRAQC5WQEAumEBALthAQC8GQEAvRkBAL4NAQC/BQEAsM0CALHVAgCy3QIAs9UCALTNAgC1cQEAtnEBALdxAQDjxAAA4XwHAOF4BgDjvAYA4nQAgIQYDQCGuAwAhzwNAL4sDwDqdACA7nQAgPJ0AIDvEAAA9nQAgPp0AIDvdAYA/nQAgAJ1AIAGdQCAs70CAAp1AIC1rQIAtqUCAA51AIASdQCAFnUAgLpFAgC7XQIAvEUCAL1NAgC+RQIAv/kBAHZ0AIClfQ0ApnUNAOZ0AIAadQCAHnUAgCJ1AICjbQ0ArJUNAK2dDQCulQ0ArykOACZ1AIAqdQCAqpUNAKuNDQCz5Q4ALnUAgDJ1AIA2dQCAOnUAgLblDgC19Q4APnUAgLuhDgC62Q4AQnUAgEZ1AIC/pQ4AvrkOAL2xDgC8uQ4AqBUOAKklDgCqLQ4AqyUOAKw9DgCtJQ4Ari0OAK8lDgCADQAAgRUAAIIdAABKdQCATnUAgFJ1AICEMAMAVnUAgLgpDgC5KQ4AujkOALs5DgC8KQ4AvSkOAL79DwC/9Q8AsF0OALElDgCyLQ4AsyUOALQ9DgC1IQ4AtiUOALcZDgCjpQ8AWnUAgIYoAQCHTAEAXnUAgKalDwCltQ8AYnUAgKvhDwCqmQ8AZnUAgGp1AICv5Q8ArvkPAK3xDwCs+Q8AbnUAgLPpDgBydQCAdnUAgLaRDgB6dQCAfnUAgLXlDgC6sQ4Au7kOAIJ1AICGdQCAvmEBAL9hAQC8mQ4AvZkOAKglDgCpLQ4AqiUOAKs5DgCsKQ4ArVUOAK5dDgCvVQ4AinUAgI51AICSdQCAlnUAgJp1AICedQCAonUAgKZ1AIC49QEAuYEBALqBAQC7gQEAvIEBAL2JAQC+sQEAv7EBALAxDgCxOQ4AsgkOALMJDgC04QEAteEBALbhAQC3zQEAo60NAKp1AICudQCAsnUAgLZ1AICm1Q0ApaENALp1AICr/Q0AqvUNAL51AIDCdQCAryUCAK4lAgCt3Q0ArN0NAIBdAACBbQAAgmUAALNRAwC+nAMAtXkDALYZAwDKdQCAhOACAM51AIC6PQMAuzUDALwZAwC9GQMAvtkDAL/ZAwCohQMAqZUDAKqVAwCrpQMArL0DAK3VAwCu0QMAr9EDAIYABACHNAMAv6AzANJ1AIDWdQCA2nUAgN51AIDidQCAuHEDALlxAwC6cQMAu3EDALzVAAC93QAAvtUAAL/NAACwtQMAsb0DALKBAwCzgQMAtFEDALVRAwC2UQMAt1EDAO+oAwDmdQCA6nUAgO51AICEHAIA8nUAgPZ1AID6dQCAviwFAP51AIACdgCABnYAgONAAwAKdgCA4SgAAA52AICjXQIAEnYAgBZ2AIAadgCAHnYAgKYVAgCldQIAInYAgKs5AgCqMQIAJnYAgCp2AICv1QIArtUCAK0VAgCsFQIA4ygBAOEADwDhCA4A4wgOAID9AACBCQAAgjkAAC52AIAydgCAOnYAgD52AIBCdgCA7+gOAEZ2AIBKdgCA72QOALNtAQBOdgCAhugEAIcMBQBSdgCAtm0BALVtAQBWdgCAu+0AALrtAABadgCAXnYAgL/VAAC+6QAAveEAALzpAACoXQYAqWEGAKqlBgCrvQYArKUGAK2tBgCupQYArxkHADZ2AIBidgCAZnYAgGp2AIBudgCAcnYAgHZ2AIB6dgCAuHUHALl5BwC6DQcAuwUHALwdBwC9BQcAvgUHAL81BwCwaQcAsWkHALJ9BwCzdQcAtG0HALVRBwC2UQcAt1EHAKMtBgB+dgCAgnYAgIZ2AICKdgCApi0GAKUtBgCOdgCAq60HAKqtBwCSdgCAlnYAgK+VBwCuqQcAraEHAKypBwCADQAAgRUAAIIdAACadgCAnnYAgKJ2AICEVAMAvlwAAKZ2AICqdgCAhugAAIdMAwCudgCAsnYAgLZ2AIC6dgCAvnYAgOMEBADCdgCA4bQFAMZ2AIDKdgCAznYAgNJ2AIDWdgCA2nYAgN52AIDidgCA5nYAgO/sBADqdgCA7nYAgLPtBgDydgCA9nYAgPp2AID+dgCAtpEGALXhBgACdwCAu40GALqNBgAGdwCACncAgL9BAQC+WQEAvVEBALxZAQCoJQYAqS0GAKolBgCrOQYArCkGAK1RBgCuSQYAr0EGAIDNAACBCQAAghkAAA53AIASdwCAhCwBAL40AAAadwCAuP0BALlBAQC6QQEAu0EBALxBAQC9SQEAvnEBAL9xAQCwCQYAsQkGALLNAQCzxQEAtN0BALXFAQC2zQEAt8UBAIagPACHRAMAHncAgKOhBQAidwCApa0FAKbdBQAmdwCAKncAgL4oPACqwQUAq8EFAKwVAgCtHQIArhUCAK8NAgC2QQMALncAgDJ3AIC1sQIANncAgLOhAgA6dwCAPncAgL5FAwC/TQMAvHUDAL1NAwC6ZQMAu20DAEJ3AIBGdwCASncAgE53AIDGdQCAUncAgFZ3AIBadwCAXncAgGJ3AICoRQIAqVUCAKpdAgCrVQIArE0CAK21AwCusQMAr60DALDVAwCx3QMAstUDALPtAwC09QMAtf0DALb1AwC37QMAuNkDALnZAwC6rQMAu6UDALy9AwC9pQMAvqUDAL+VAwCj9QMAZncAgGp3AIBudwCAcncAgKYVAgCl5QMAdncAgKs5AgCqMQIAencAgH53AICvGQIArhECAK0ZAgCsIQIAgGkAAIFpAACCBQAAgncAgIp3AICOdwCAkncAgO8cAACEbAIA4ZQBAJZ3AIDjyAAAmncAgJ53AICGWDwAh1A9AKJ3AICmdwCAqncAgISEPQCudwCAsncAgLZ3AIDvuAEAvmw8AOF0BgC6dwCA42QBAL53AIDCdwCAxncAgMp3AICz0QEAzncAgNJ3AIDWdwCA2ncAgLaRAQC1+QEA3ncAgLu9AQC6vQEA4ncAgOZ3AIC/dQEAvnUBAL2FAQC8hQEAqL09AKkNPgCqGT4AqxE+AKwxPgCtUT4ArlE+AK9NPgCGdwCAgh0AAIEdAACAHQAA6ncAgO53AIDydwCA9ncAgLjVPgC53T4AutU+ALtJPwC8WT8AvVk/AL5JPwC/QT8AsDk+ALE5PgCyET4AsxE+ALTxPgC18T4AtvU+ALftPgCjkT4A+ncAgIYoAACHwAMA/ncAgKbRPgCluT4AAngAgKv9PgCq/T4ABngAgAp4AICvNT4ArjU+AK3FPgCsxT4ADngAgLOdPwASeACAFngAgLalPwAaeACAHngAgLWtPwC6aT8Au3U/ACJ4AIAmeACAvlk/AL9FPwC8bT8AvWU/ACp4AIAueACAMngAgDZ4AIDjYDwAOngAgOEAPQA+eACA7/w9AEJ4AIBGeACASngAgE54AIBSeACAVngAgFp4AICjGT4AghkAAIEZAACAcQAAXngAgKYhPgClKT4AYngAgKvxPgCq7T4AhCQBAL4kAQCvwT4Art0+AK3hPgCs6T4AqNE+AKnRPgCq0T4Aq+U+AKzhPgCt4T4Arhk+AK8ZPgCGAAAAh4QAAGp4AIBueACAcngAgHZ4AIB6eACAfngAgLh9PgC5AT4AugE+ALsBPgC8AT4AvQk+AL4xPgC/MT4AsGk+ALF1PgCyfT4As3U+ALRZPgC1RT4Atk0+ALdFPgCohQIAqZUCAKqVAgCrpQIArL0CAK3VAgCu0QIAr9ECAIJ4AICGeACAingAgL8k5gGOeACAkngAgJZ4AICaeACAuFUDALlZAwC6bQMAu2UDALx9AwC9ZQMAvm0DAL9lAwCwtQIAsb0CALKBAgCzgQIAtHEDALVxAwC2cQMAt3EDALMdAgCeeACAongAgKZ4AICEiAMAtlUCALU1AgAWdwCAu3kCALpxAgCqeACArngAgL+1AwC+tQMAvVUCALxVAgCyeACAo1kCALZ4AIC6eACAphECAL54AIDCeACApXECAKo1AgCrPQIAxngAgMp4AICu8QMAr/EDAKwRAgCtEQIAqKkCAKmpAgCquQIAq7kCAKypAgCtqQIArjkBAK85AQCAzQEAgQkAAIIZAADOeACA0ngAgL64BQDaeACA3ngAgLjpAQC56QEAuokBALuFAQC8nQEAvYEBAL6BAQC/tQEAsEkBALFVAQCyXQEAs1UBALRNAQC18QEAtvEBALfxAQDvFAAA4ngAgIaoBQCH3AUA5ngAgIRYBADqeACA78Q+AO54AIDhxD4A8ngAgOMwPgDjyAAA9ngAgOEoAQD6eACAtn0CAP54AIACeQCAtXUCAAZ5AICzZQIACnkAgA55AIC+3QEAv2EBALzdAQC91QEAutkBALvFAQASeQCAFnkAgKOxBQDWeACAGnkAgB55AIAieQCApqkFAKWhBQAmeQCAqxEGAKoNBgAqeQCALnkAgK+1BgCuCQYArQEGAKwJBgAyeQCANnkAgDp5AIA+eQCAgBkAAIEZAACCBQAAQnkAgL5sAwBGeQCAhsgAAIccAwBKeQCATnkAgFJ5AIBWeQCAqLkHAKm5BwCqDQcAqx0HAKwJBwCtNQcArjEHAK8pBwCEqAMAWnkAgF55AIBieQCAZnkAgGp5AIBueQCAcnkAgLjJAAC5yQAAutkAALvRAAC8+QAAvfkAAL6ZAAC/mQAAsF0HALEhBwCyIQcAsz0HALQpBwC1KQcAtgEHALcBBwCzhQYAdnkAgHp5AIB+eQCAgnkAgLa1BgC1gQYAhnkAgLvlBgC6mQYAinkAgI55AIC/7QYAvu0GAL3pBgC89QYAknkAgJZ5AICaeQCAnnkAgKJ5AICmeQCAqnkAgO+QBACueQCA4dwGALJ5AIDj7AUAgCkAAIEVAACCEQAAvnwBAKMFBgC6eQCAhigAAIdMAQC+eQCApjUGAKUBBgDCeQCAq2UGAKoZBgDGeQCAynkAgK9tBgCubQYArWkGAKx1BgDOeQCAs70BANJ5AIDWeQCAtnkBANp5AIDeeQCAtXkBALpVAQC7XQEA4nkAgOZ5AIC++QAAv/kAALxFAQC9+QAAqHECAKlxAgCqcQIAq3ECAKy1AgCtvQIArrUCAK+tAgCE7AwA6nkAgO55AIDyeQCA9nkAgPp5AID+eQCAAnoAgLhpAwC5aQMAugkDALsJAwC8GQMAvRkDAL4JAwC/CQMAsNUCALHdAgCy1QIAs2kDALR5AwC1eQMAtmkDALdhAwAGegCACnoAgA56AICj9QIAEnoAgKUxAgCmMQIAFnoAgBp6AIAeegCAqh0CAKsVAgCsDQIArbEDAK6xAwCvsQMAgGEAAIFhAACCBQAAInoAgIbwDACHYAMAvhAMACp6AIBmeACALnoAgDJ6AIA2egCAOnoAgD56AIBCegCARnoAgKiFAgCplQIAqpUCAKulAgCsvQIArdUCAK7RAgCv0QIASnoAgE56AIBSegCAVnoAgFp6AIBeegCAYnoAgGZ6AIC4dQEAuX0BALp1AQC7zQEAvNUBAL3dAQC+yQEAv8EBALC1AgCxvQIAsoECALOBAgC0VQEAtV0BALZVAQC3TQEA4RAGAIRIDADjDAYAanoAgISYDABuegCAcnoAgHZ6AIB6egCAfnoAgIJ6AICGegCAgXUAAIB1AADvIAEAgnUAAIp6AICOegCAknoAgL7ADACFtA4A4RACAO9cAADjABYA4ZABAJp6AIDjWAEA7zwHAJ56AICiegCAhgAIAIe4DACznQ0AJnoAgKZ6AICqegCArnoAgLbVDQC1tQ0AsnoAgLv5DQC68Q0AtnoAgLp6AIC/GQ4AvhEOAL3VDQC81Q0AvnoAgKPZDQDCegCAxnoAgKaRDQDKegCAznoAgKXxDQCqtQ0Aq70NANJ6AIDWegCArlUOAK9dDgCskQ0ArZENAKhdDgCpYQ4AqmEOAKthDgCsYQ4ArWEOAK5hDgCvYQ4A2noAgN56AIDiegCA5noAgOp6AIDuegCA8noAgPZ6AIC4TQ8AuVEPALpRDwC7UQ8AvHEPAL1xDwC+cQ8Av3EPALDBDwCxwQ8AssEPALPBDwC0wQ8AtcEPALbBDwC3wQ8As+kPAPp6AIC+gAEA/noAgJZ6AIC24Q8AtekPAAJ7AIC7BQ4AugUOAAp7AIAGewCAvwUOAL4FDgC9FQ4AvBUOAIFNAACAQQAA72gNAIJRAACG8AcAh9QBAA57AIASewCAFnsAgIRwAQAaewCAHnsAgOHgDgAiewCA40gNACZ7AICjaQ8AKnsAgC57AIAyewCANnsAgKZhDwClaQ8AOnsAgKuFDgCqhQ4APnsAgEJ7AICvhQ4AroUOAK2VDgCslQ4ARnsAgLMxDgBKewCATnsAgLbBAQBSewCAVnsAgLXRAQC6zQEAu6UBAFp7AIBeewCAvqUBAL+tAQC8sQEAvbEBAI/dJgCj8Q0AYnsAgGZ7AICmAQIAansAgG57AIClEQIAqg0CAKtlAgByewCAviAEAK5lAgCvbQIArHECAK1xAgCfoQwAnnkKAJ1pCgCc0QgAm7E2AJp1NgCZ0TQAmOEyAJdtMgCWZTIAlTU/AJRhPgCTcT4AkjU7AJFxOgCQeToAgJUAAIGdAACCoQAAensAgO9EAgDhdA8AfnsAgOMcDwDj1AEAgnsAgOHgAQDvXAEAo7UCAKJBAACh3Q4AoLkOALWpAwCGewCAhMAEALahAwCG8AUAh+QEALOFAwCKewCAvXEDALxpAwC/QQMAvnEDAI57AIC2eQCAu3EDALp5AwCC3ScAgwE7AL6EBwC+wAYAhhE/AIcZPwCEETsAhV06AIp9PgCLJTMAknsAgJZ7AICOuTUAjxU3AIw1MwCNgTMAkqE3AJPZCQC+xBkAmnsAgJaxDQCXUQ8AlHkLAJVhCwCaBQ8Am5EBAJ57AICiewCApnsAgN0AAACcfQMAqnsAgOFIDwCuewCA4xwOALJ7AIC2ewCAunsAgL57AIDCewCAsUEXALChFwCzqesBsgHoAbUB7AG0EesB74wOAMZ7AICpxR8AqAEcAKsBEACqkR8ArdkTAKzREwCv2RcArgUTAKHxAgDKewCAo8kHAKLBAgClARgApGUHAKehGwCm+RsAqCkFAKldBQCqVQUAq20FAKx5BQCteQUArm0FAK9hBQB2ewCAznsAgNJ7AIDWewCAgA0AAIGxAACCsQAA2nsAgLiJBQC5iQUAup0FALuVBQC8uQUAvbkFAL5RBgC/UQYAsOUFALHtBQCy5QUAs/0FALTtBQC13QUAttUFALe9BQCj3QUA3nsAgOJ7AICEDAAA5nsAgKb5BQCl8QUA6nsAgKspBQCqIQUAhpgAAIegAACvGQUArikFAK0pBQCsMQUA7nsAgLNhBgDyewCA9nsAgLYhBgD6ewCA/nsAgLUBBgC6rQcAu40HAAJ8AIAGfACAvo0HAL9xBwC8lQcAvY0HAL65BQC/uQUAvLkFAL25BQC6uQUAu7kFALi5BQC5uQUAtkkFALdJBQC0fQUAtXUFALJ5BQCzeQUAsBUFALF9BQCuXQUAr20FAKxFBQCtXQUAqqUKAKtdBQCovQoAqa0KAAp8AIAOfACAEnwAgBZ8AIAafACAHnwAgCJ8AIAmfACAqA0HAKkdBwCqLQcAq0kHAKxNBwCtZQcArrEGAK+xBgAqfACALnwAgDJ8AIA2fACAOnwAgD58AIBCfACARnwAgLhVBgC5XQYAulUGALtxBgC8NQYAvfEBAL7xAQC/8QEAsK0GALGNBgCyhQYAs50GALSNBgC1cQYAtnUGALdtBgCjpQQAgi0AAIEVAACAHQAASnwAgKblBAClxQQATnwAgKtJBQCqaQUAUnwAgFp8AICvtQUArkkFAK1JBQCsUQUAhmAcAIcIAwBefACAs4UCAGJ8AIC1gQIAtoECAGZ8AIBqfACAbnwAgLoJAwC7CQMAvBkDAL0ZAwC+CQMAvwkDAKxVAgCtXQIArmECAK9hAgCoDQIAqVUCAKpRAgCrUQIAhKwDAHJ8AIB2fACAenwAgIT8HQB+fACAgnwAgIZ8AIC8cQMAvXEDAL5xAwC/cQMAuHEDALlxAwC6cQMAu3EDALSRAwC1kQMAtpEDALeRAwCwkQMAsZEDALKRAwCzkQMAinwAgI58AICSfACAlnwAgJp8AIDhpAEAnnwAgOOAAQC+aBwAonwAgKZ8AIDv2AYAqnwAgK58AICyfACAtnwAgKOJAwCCLQAAgRUAAIAdAAC6fACApo0DAKWNAwC+fACAqwUCAKoFAgDCfACAynwAgK8FAgCuBQIArRUCAKwVAgCGIBwAh8QdAM58AIDSfACA1nwAgNp8AIDefACA72wGAOJ8AIDhbAcA5nwAgON0BwDqfACA7nwAgPJ8AID2fACAs5EBAPp8AID+fACAAn0AgAZ9AIC2sQEAtbkBAAp9AIC7VQEAukkBAA59AIASfQCAv/UAAL71AAC9RQEAvEUBAKNRHgDGfACAFn0AgBp9AIAefQCApnEeAKV5HgAifQCAq5UeAKqJHgAmfQCAKn0AgK81HwCuNR8ArYUeAKyFHgCAbQAAgRUAAIIdAADv/BkALn0AgDJ9AIA2fQCAOn0AgIbAAACHrAMAPn0AgEJ9AIBGfQCA4SwcAEp9AIDjzBwAqK0eAKnNHgCq2R4Aq9EeAKzxHgCt8R4Arj0eAK81HgCE7AAATn0AgFJ9AIBWfQCAWn0AgF59AIBifQCAZn0AgLjRHwC53R8Auu0fALvlHwC84R8AveEfAL7hHwC/4R8AsE0eALFRHgCyUR4As1EeALTxHwC18R8AtvEfALfxHwCobR4AqY0eAKqFHgCrnR4ArIUeAK2NHgCuuR4Ar7UeAGp9AIBufQCAcn0AgHZ9AIB6fQCAfn0AgIJ9AICGfQCAuJ0eALmtHgC6pR4Au0UBALxdAQC9RQEAvkUBAL91AQCw0R4AsdEeALLRHgCz0R4AtLUeALW9HgC2tR4At60eALMNHgCKfQCAjn0AgJJ9AICWfQCAtg0eALUNHgCafQCAuxUeALoVHgCefQCAon0AgL95HgC+cR4AvQUeALwFHgCCbQAAo0keAIBVAACBZQAApkkeAL6cAQCqfQCApUkeAKpRHgCrUR4Ah3wAAIZMAACuNR4Arz0eAKxBHgCtQR4AqF0CAKltAgCqZQIAq30CAKxpAgCtsQIArrECAK+xAgCE7AQArn0AgLJ9AIC2fQCAun0AgL59AIDCfQCAxn0AgLhxAwC5cQMAunEDALtxAwC81QMAvd0DAL7VAwC/zQMAsNECALHRAgCy0QIAs9ECALRRAwC1UQMAtlEDALdRAwCz7QIAyn0AgM59AIC+gAQA0n0AgLYxAgC14QIA1n0AgLsVAgC6FQIA2n0AgN59AIC/lQMAvpUDAL0FAgC8BQIA4n0AgKOpAgDmfQCA6n0AgKZ1AgDufQCA8n0AgKWlAgCqUQIAq1ECAPZ9AID6fQCArtEDAK/RAwCsQQIArUECAKjZAgCpIQEAqiEBAKshAQCsIQEArSEBAK4hAQCvIQEA/n0AgAJ+AIAGfgCAviAEAAp+AIAOfgCAEn4AgBp+AIC4jQEAuZEBALqRAQC7pQEAvL0BAL11AAC+fQAAv3UAALDlAQCx7QEAsvkBALPxAQC02QEAtdkBALa5AQC3tQEA4RgeAB5+AIDjKB8AIn4AgIGlAACApQAAJn4AgIKlAACGAAQAh/QFACp+AIAufgCAMn4AgDZ+AIDvYB4AOn4AgD5+AIBCfgCAhfD0AUZ+AIBKfgCA42QBAE5+AIDhpAEAUn4AgO/IAABWfgCAWn4AgFZ8AICE/AUAXn4AgGJ+AICzKQYAFn4AgGZ+AIBqfgCAbn4AgLYhBgC1KQYAcn4AgLupBgC6oQYAdn4AgHp+AIC/nQYAvp0GAL2lBgC8rQYA4bQHAH5+AIDjeAQAgn4AgIB9AACBEQAAghUAAIZ+AICGwAAAh1gDAIp+AICOfgCAkn4AgJZ+AIDvDAQAmn4AgKOpBgCefgCAon4AgKZ+AICqfgCApqEGAKWpBgCufgCAqykGAKohBgCyfgCAtn4AgK8dBgCuHQYArSUGAKwtBgC6fgCAs0kHAL5+AIDCfgCAtn0HAMZ+AIDKfgCAtXUHALpdBwC7JQcAzn4AgNJ+AIC+IQcAvy0HALw9BwC9MQcAqD0GAKmBBgCqhQYAq5UGAKy5BgCtuQYArqkGAK+pBgDWfgCA2n4AgN5+AIDifgCA5n4AgIK5AACBsQAAgLkAALitBgC5vQYAurUGALtFAQC8XQEAvUUBAL5FAQC/dQEAsN0GALGlBgCyrQYAs6EGALShBgC1rQYAtpkGALeVBgCjDQYA6n4AgO5+AIDyfgCAhJgCAKY5BgClMQYAvpwBAKthBgCqGQYAhggAAId8AQCvaQYArmUGAK11BgCseQYA+n4AgLO1AQD+fgCAAn8AgLZVAQAGfwCACn8AgLWhAQC6cQEAu3kBAA5/AIASfwCAvjEBAL89AQC8UQEAvVEBAKhpAgCpaQIAqnkCAKt5AgCsbQIArZECAK6RAgCvkQIAFn8AgBp/AIAefwCAIn8AgCZ/AIAqfwCALn8AgDJ/AIC4mQIAua0CALqlAgC7bQMAvHUDAL19AwC+dQMAv20DALDxAgCx+QIAssECALPBAgC0sQIAtb0CALa1AgC3qQIANn8AgDp/AIA+fwCAo/0CAEJ/AICl6QIAph0CAEZ/AIBKfwCATn8AgKo5AgCrMQIArBkCAK0ZAgCueQIAr3UCAFJ/AIBWfwCAWn8AgIQADACAGQAAgQkAAII5AABefwCAYn8AgGp/AIBufwCAvuAMAHJ/AIB2fwCAhlgNAIcMAwCowQIAqc0CAKrFAgCr2QIArMkCAK39AgCu9QIArz0BAHp/AIB+fwCAgn8AgIZ/AICKfwCAjn8AgJJ/AIC+MAwAuMUBALnNAQC62QEAu9EBALzxAQC98QEAvpkBAL+ZAQCwRQEAsU0BALJFAQCzXQEAtEUBALVNAQC2RQEAt/0BAOE4BgCWfwCA42wGAJp/AICefwCAon8AgKZ/AICqfwCAhKgNAK5/AICyfwCAtn8AgL6wDwC6fwCA72wGAL5/AIDCfwCApn0AgMZ/AIDKfwCA41AAAM5/AIDhoAEA0n8AgO+EAADafwCAhyANAIZMDwCAPQAAgSEAAIIlAADefwCAs80NAGZ/AIDWfwCA4n8AgOZ/AIC2/Q0AtcENAOp/AIC7CQ4AugEOAO5/AIDyfwCAvwkOAL4BDgC9CQ4AvBEOAPZ/AIDjmAwA+n8AgOH8DwD+fwCAAoAAgAaAAIAKgACADoAAgBKAAIAWgACAGoAAgB6AAIDvYAwAIoAAgCaAAICjTQ0AKoAAgC6AAIAygACANoAAgKZ9DQClQQ0AOoAAgKuJDgCqgQ4APoAAgEKAAICviQ4AroEOAK2JDgCskQ4Agm0AALM1DgCAVQAAgWUAALb1DwCE3AMARoAAgLX9DwC60Q8Au9EPAIYABACH3AAAvn0PAL9lDwC8wQ8AvXkPAKjlDwCp7Q8AqvkPAKv5DwCsMQ4ArTEOAK4xDgCvMQ4ASoAAgE6AAIBSgACAVoAAgFqAAIBegACAYoAAgGaAAIC43Q4AueEOALrhDgC74Q4AvOUOAL3pDgC+mQ4Av5UOALBRDgCxUQ4AslEOALPpDgC0/Q4AteUOALbtDgC35Q4Ao3EPAGqAAIBugACAcoAAgHaAAICmsQ4ApbkOAHqAAICrlQ4AqpUOAH6AAICCgACAryEOAK45DgCtPQ4ArIUOAIaAAICzyQEAioAAgI6AAIC2+QEAkoAAgJaAAIC1wQEAuqkBALu1AQCagACAnoAAgL6tAQC/lQEAvK0BAL2lAQCo5Q0AqfkNAKoFAgCrHQIArA0CAK09AgCuNQIAr10CAKKAAICmgACAqoAAgK6AAICAGQAAgRkAAIIFAACygACAuC0CALk1AgC6MQIAuzECALzVAgC93QIAvtUCAL/NAgCwKQIAsTUCALI9AgCzNQIAtC0CALUVAgC2HQIAtxUCALqAAICEnAIAvoAAgKOBAgDCgACApYkCAKaxAgDGgACAhiAEAIfUAwCq4QIAq/0CAKzlAgCt7QIAruUCAK/dAgC29QMAvkQDAIWM/QG1/QMAyoAAgLP9AwDOgACA0oAAgL59AwC/TQMAvGUDAL19AwC6dQMAu30DANaAAIDagACA3oAAgOKAAICEBAIAoyUCAOaAAIClJQIApi0CAOqAAIDugACA8oAAgKqtAgCrpQIArL0CAK2lAgCupQIAr5UCAPaAAID6gACA/oAAgAKBAIAGgQCA48ADAAqBAIDhrAEADoEAgO9YAwASgQCAFoEAgIANAACB5QAAgu0AABqBAIDhYA8A40ABAOM4DgDheA4AHoEAgCKBAIC+lAUAKoEAgIYABACHZAUALoEAgDKBAIA2gQCA7/wOAO98DgA6gQCAs1EBAD6BAID2fgCAQoEAgEaBAIC2DQEAtQkBAEqBAIC74QAAuhkBAE6BAIBSgQCAv9EAAL7pAAC96QAAvPkAALaAAIAmgQCAVoEAgFqBAIBegQCAYoEAgGaBAIBqgQCAqKEGAKmtBgCquQYAq7EGAKzhBgCt7QYAruUGAK/FBgCwvQYAsUUHALJNBwCzXQcAtE0HALV1BwC2fQcAtx0HALglBwC5LQcAuiUHALs9BwC8KQcAvRUHAL4RBwC/EQcAoxEGAG6BAIBygQCAdoEAgHqBAICmTQYApUkGAH6BAICroQcAqlkGAIKBAICGgQCAr5EHAK6pBwCtqQcArLkHAIANAACBFQAAgh0AAIqBAICOgQCAkoEAgISUAwC+lAMAloEAgJqBAICGyAAAh4wAAJ6BAICigQCApoEAgKqBAIConQYAqa0GAKqlBgCrvQYArK0GAK3RBgCu1QYAr80GAK6BAICygQCAtoEAgLqBAIC+gQCAwoEAgMaBAIDKgQCAuF0BALnBAQC6wQEAu8EBALzBAQC9yQEAvvEBAL/xAQCwvQYAsY0GALKFBgCzZQEAtH0BALVlAQC2bQEAt2UBALMtBgDOgQCA0oEAgNaBAIDagQCAtlEGALUlBgDegQCAu0kGALp5BgDigQCA5oEAgL+hAQC+uQEAvbEBALxRBgDqgQCAo2kGAO6BAIDygQCAphUGAPaBAID6gQCApWEGAKo9BgCrDQYA/oEAgAKCAICu/QEAr+UBAKwVBgCt9QEAutUHALvdBwC4wQcAucEHAL4xBAC/MQQAvPEHAL3xBwCyrQcAs7UHALCtBwCxpQcAtp0HALf1BwC0pQcAtZUHAKppBwCraQcAqGkHAKlpBwCuaQcAr2kHAKxpBwCtaQcAgLkDAIGNAwCChQMAhKgDAIZQ/AGHCAMAvjQDAAqCAICoZQIAqXUCAKp9AgCrdQIArG0CAK21AwCuvQMAr7UDAA6CAIASggCAFoIAgBqCAIAeggCAIoIAgCaCAIAqggCAuFEDALlZAwC6YQMAu2EDALwRAwC9HQMAvhUDAL8JAwCwzQMAsdUDALLdAwCz1QMAtM0DALVxAwC2cQMAt3EDAC6CAIAyggCAs/0DADaCAIC17QMAOoIAgD6CAIC2PQIAQoIAgEaCAIC7GQIAugECAL0JAgC8AQIAv70CAL4BAgBKggCAToIAgITE/QG+wPwBUoIAgFaCAIBaggCA79wDAF6CAIDhlAEAYoIAgOMQAwBmggCAgu0AAIHtAACA7QAA4TgGAOE8BwDjQAEA45QGAGqCAIBuggCAcoIAgHqCAICGgPwBh+j9AX6CAICCggCAhoIAgIqCAIDvnAEA79wGAKM1AwCOggCAkoIAgJaCAICaggCApvUCAKUlAwCeggCAq9ECAKrJAgCiggCApoIAgK91AgCuyQIArcECAKzJAgB2ggCAqoIAgK6CAICyggCA76T9AbaCAIC6ggCAvoIAgON4/QHCggCA4UD8AcaCAIDKggCAzoIAgNKCAIDWggCAs+X+AYItAACBFQAAgB0AANqCAIC25f4BtfX+Ad6CAIC7Yf8Butn+AeKCAICE5AMAv2n/Ab5h/wG9df8BvHn/Aaj9/gGpJf4Bqi3+Aasl/gGsPf4BrSX+Aa4t/gGvJf4BviwAAOaCAICGiAAAh+wAAOqCAIDuggCA8oIAgPaCAIC4gf8BuYH/AbqZ/wG7mf8BvIn/Ab21/wG+sf8Bv63/AbBd/gGx5f8Bsu3/AbPh/wG05f8Bte3/AbbZ/wG32f8Bo6X/AfqCAID+ggCAAoMAgAaDAICmpf8BpbX/AQqDAICrIf4Bqpn/AQ6DAIASgwCAryn+Aa4h/gGtNf4BrDn+ARaDAICz6f4BGoMAgB6DAIC2lf4BIoMAgCaDAIC16f4BurH+Abu5/gEqgwCALoMAgL51AQC/fQEAvJH+Ab2R/gGoHf4BqS3+Aaol/gGrPf4BrCX+Aa1R/gGuUf4Br1H+ATKDAIA2gwCAOoMAgD6DAIBCgwCARoMAgEqDAIBOgwCAuNkBALnZAQC67QEAu+EBALzhAQC94QEAvuEBAL/hAQCwMf4BsTn+AbIB/gGzAf4BtPUBALX9AQC29QEAt+kBAKOt/QFSgwCAvkwDAFqDAIBegwCAptH9AaWt/QFigwCAq/39Aar1/QFmgwCAaoMAgK85AgCuMQIArdX9AazV/QGA+QMAgfkDAIJNAACFdCAAboMAgITYAwCE1AQAcoMAgIZABACHVAMAdoMAgHqDAIB+gwCAgoMAgIaDAIC+8AUAqDECAKkxAgCqMQIAqzECAKyVAwCtnQMArpUDAK+NAwCKgwCAjoMAgJKDAICWgwCAhHwHAJqDAICegwCAooMAgLipAwC5qQMAumkDALtpAwC8eQMAvXkDAL5pAwC/aQMAsP0DALHNAwCyxQMAs60DALS5AwC1uQMAtq0DALelAwCmgwCAqoMAgK6DAICygwCAtoMAgLqDAIDv6AMAvoMAgOGQAQDCgwCA42wDAMqDAICAJQAAgSkAAIIdAADOgwCAs/kDANKDAICGaAcAh1wFANaDAIC2XQIAtV0CANqDAIC7SQIAunkCAN6DAIDigwCAvz0CAL49AgC9OQIAvFECAOaDAIDhPP4BvkAGAOPwAQDqgwCA7oMAgPKDAID2gwCA+oMAgP6DAIAChACABoIAgAaEAIAKhACADoQAgO/kAQAShACAFoQAgKNxAwAahACApdUCAB6EAIAihACAptUCACaEAIAqhACAq8ECAKrxAgCtsQIArNkCAK+1AgCutQIA4dz8AcaDAIDjUAQA74gEAID1BwCBCQAAgj0AAC6EAICEJAEAMoQAgDaEAIA6hACAPoQAgOFMBADv5BwA43QEALNdBgBChACAhgAMAIfgAwBGhACAtgUGALV1BgBKhACAuxEGALoJBgBOhACAUoQAgL/VBgC+1QYAvQEGALwJBgCojQYAqZUGAKqVBgCrpQYArL0GAK3FBgCuxQYAr/UGAFaEAIBahACAXoQAgGKEAIBmhACAaoQAgG6EAIByhACAuHUGALl9BgC6dQYAu80HALzVBwC93QcAvtUHAL/NBwCwjQYAsZUGALKdBgCzlQYAtFEGALVRBgC2UQYAt1EGAKMdBwCPFewBdoQAgHqEAIB+hACApkUHAKU1BwCChACAq1EHAKpJBwCGhACAioQAgK+VBwCulQcArUEHAKxJBwCeRfkBn6X5AZyR/QGdTfkBmlX9AZtd/QGYBfEBmZX+AZal8gGXYfEBlG31AZU19QGS4ekBk4X2AZBV7AGRXekBsbEdALClHQCziRkAskEcALUBJAC09RkAjoQAgJKEAICWhACAgqkDAIGhAwCAaQAAohUFAKMFAgCgFQYAob0FAKHFAQCahACAo80NAKLlAQClAQgApN0NAKfRCQCm2QkAqQEUAKilCACrxRQAqs0VAK3REQCsARAArwEcAK51EQCCEe8BgynvAZ6EAICihACAhuH1AYcR9gGEOeoBhY3qAYp59gGL4fEBvqQMAKqEAICO+f0BjzH+AYw98gGNYfIBkkn+AZOd/gGHCAwAhmwMAJax+gGX+QUAlFn6AZVZ+gGaYQYAm8EGAK6EAICyhACAtoQAgLqEAICcyQEAvoQAgKitBQCpuQUAqs0FAKvdBQCszQUArf0FAK71BQCvHQUAwoQAgMaEAIDKhACAzoQAgNKEAIDWhACA2oQAgN6EAIC4dQUAuX0FALoJBQC7CQUAvB0FAL0BBQC+AQUAvz0FALBxBQCxcQUAsnEFALNxBQC0UQUAtVEFALZRBQC3TQUAs0UEAOKEAIDmhACA6oQAgO6EAIC2fQQAtUUEAPKEAIC7tQQAurUEAPaEAID6hACAv5UEAL6VBAC9pQQAvKUEAP6EAICjAQQAAoUAgAaFAICmOQQACoUAgA6FAIClAQQAqvEEAKvxBAAShQCAhOwNAK7RBACv0QQArOEEAK3hBADh0AYAhAwMAOMoBwC+AAwAGoUAgO9EAwCGuAwAhywNAB6FAIDjlAEAIoUAgOH8AQBWgwCAJoUAgO/IBgAqhQCALoUAgDKFAICzjQMANoUAgLWNAwA6hQCAPoUAgLa1AwBChQCARoUAgLtBAwC6SQMAvUEDALxZAwC/QQMAvkkDAKNFDACmhACAFoUAgEqFAIBOhQCApn0MAKVFDABShQCAq4kMAKqBDABWhQCAWoUAgK+JDACugQwArYkMAKyRDACAFQ8AgR0PAIIhDwCzIQ4AXoUAgLUhDgC2JQ4AYoUAgGaFAIBqhQCAusEOALvBDgC8wQ4AvcEOAL7BDgC/wQ4AqK0OAKntDgCq5Q4Aq/0OAKzlDgCt6Q4ArjkOAK85DgBuhQCAcoUAgHaFAIB6hQCAgB0AAIEJAACCvQEAfoUAgLjNDwC51Q8AutUPALvlDwC8/Q8AvZUPAL6RDwC/kQ8AsEkOALFJDgCyWQ4As1kOALRJDgC1SQ4Atv0PALf1DwCjbQ8AgoUAgL6EAQCKhQCAjoUAgKZpDwClbQ8AkoUAgKuNDwCqjQ8AhogAAIdsAQCvjQ8Aro0PAK2NDwCsjQ8AloUAgLPtDgCahQCAnoUAgLaRDgCihQCApoUAgLXhDgC6tQ4Au70OAKqFAICuhQCAvn0BAL9lAQC8mQ4AvZkOAKgRDgCpJQ4AqiEOAKs5DgCsLQ4ArVUOAK5dDgCvUQ4AhKgAALKFAIC2hQCAuoUAgL6FAIDChQCAxoUAgMqFAIC47QEAuZUBALqVAQC7rQEAvLUBAL11AQC+fQEAv3UBALA1DgCxPQ4AsgkOALMJDgC0/QEAteUBALblAQC31QEAo6kNAM6FAIDShQCA1oUAgNqFAICm1Q0ApaUNAN6FAICr+Q0AqvENAOKFAIDmhQCAryECAK45AgCt3Q0ArN0NAIANAACBFQAAgh0AAOqFAIDuhQCA8oUAgIeQAwCGfAQAvuwEAPqFAID+hQCAAoYAgAaGAIAKhgCADoYAgBKGAICyLQ4AszUOALAtDgCxJQ4Ati0OALedDwC0LQ4AtSUOALq9DwC7jQ8AuKUPALm9DwC+LQ8AvxUPALyVDwC9JQ8AFoYAgBqGAIAehgCAIoYAgCaGAIAqhgCALoYAgDKGAICqpQ4Aq7UOAKjFDgCp3Q4Arp0OAK9VDgCspQ4ArZUOAKgNAgCpFQIAqhUCAKtNAgCsWQIArVkCAK5NAgCvRQIAhKgFADaGAIA6hgCAPoYAgIS4BABChgCARoYAgEqGAIC4/QIAuUEBALpBAQC7QQEAvEEBAL1JAQC+cQEAv3EBALAJAgCxCQIAss0CALPFAgC03QIAtcUCALbNAgC3xQIA4dQPAOMQDgDj9A4A4QwOAE6GAIBShgCAVoYAgFqGAIBehgCAYoYAgL4kBABqhgCA7AAAAO9EAADvzA4AboYAgIJlAACz2QIAgFUAAIFtAAC2nQIAcoYAgHaGAIC1lQIAuokCALuJAgCGqAQAh+AEAL5dAgC/RQIAvF0CAL1VAgCjHQUA9oUAgGaGAIB6hgCAfoYAgKZZBQClUQUAgoYAgKtNBQCqTQUAhoYAgIqGAICvgQUArpkFAK2RBQCsmQUAjoYAgLMpBgCShgCAloYAgLYpBgCahgCAnoYAgLUpBgC6pQYAu60GAKKGAICmhgCAvqUGAL+tBgC8tQYAva0GAKjlBgCp7QYAquUGAKv9BgCs5QYAre0GAK7lBgCvXQYAqoYAgK6GAICyhgCAtoYAgLqGAIC+hgCAwoYAgMaGAIC46QcAuekHALr9BwC79QcAvO0HAL1FBwC+TQcAv0UHALAlBgCxLQYAsiUGALM9BgC0JQYAtS0GALYlBgC32QcAo20HAIItAACBFQAAgB0AAMqGAICmbQcApW0HAM6GAICr6QcAquEHANKGAIC+oAEAr+kHAK7hBwCt6QcArPEHANaGAICzkQYAhugAAIcsAQC2QQEA2oYAgN6GAIC1UQEAuk0BALslAQDihgCA5oYAgL4lAQC/LQEAvDEBAL0xAQCwrQEAscUBALLBAQCzwQEAtMUBALXNAQC28QEAt/EBALgBAQC5AQEAugEBALsBAQC8AQEAvQEBAL4BAQC/AQEA6oYAgO6GAIDyhgCA9oYAgIaFAID6hgCA/oYAgAKHAICoTQYAqVkGAKo9BgCrNQYArP0BAK3lAQCu5QEAr9UBAKPVBQAGhwCACocAgA6HAIAShwCApgUCAKUVAgAWhwCAq2ECAKoJAgAahwCAHocAgK9pAgCuYQIArXUCAKx1AgAihwCAJocAgCqHAIAuhwCAMocAgOFkBQA2hwCA4+wFAIARAACBEQAAghEAAO/0BgA6hwCAPocAgEKHAIC+MAMAhMQCAEqHAICz4QMAhMAcALVRAwBOhwCAUocAgLZZAwBWhwCAWocAgLtxAwC6eQMAvbUAALxpAwC/tQAAvrUAAF6HAIDhlAEAYocAgONcAgCGcBwAh0QDAGaHAIBqhwCAbocAgHKHAIB2hwCAeocAgH6HAICChwCAhocAgO94AgCoVQIAqV0CAKphAgCrYQIArNECAK3RAgCu0QIAr9ECAIqHAICOhwCAkocAgJaHAICahwCAnocAgKKHAICmhwCAuGkBALlpAQC6CQEAuwkBALwZAQC9GQEAvgkBAL8FAQCwtQIAsb0CALK1AgCzaQEAtHkBALV5AQC2aQEAt2EBAOHEBwDjpAYA47gGAOF8BgCADQAAgTUAAII9AACqhwCArocAgLKHAIC+4B0AuocAgL6HAIDvYAAA7+gGAMKHAICjqQIAxocAgMqHAIDOhwCA0ocAgKYRAgClGQIA1ocAgKs5AgCqMQIAhkgcAIfMHACv/QEArv0BAK39AQCsIQIAqIUeAKmRHgCqkR4Aq60eAKy1HgCt1R4ArtEeAK/FHgC2hwCA2ocAgN6HAIDihwCA5ocAgOqHAIDuhwCA8ocAgLhhHwC5YR8AumEfALthHwC8YR8AvWEfAL5hHwC/YR8AsL0eALGFHgCyjR4As4UeALSdHgC1hR4Ato0eALeFHgCzGR4A9ocAgPqHAID+hwCAAogAgLZVHgC1PR4ABogAgLtBHgC6eR4ACogAgA6IAIC/QR4AvlkeAL1RHgC8WR4AEogAgKNdHgAWiACAGogAgKYRHgAeiACAIogAgKV5HgCqPR4AqwUeAISkAwC+qAMArh0eAK8FHgCsHR4ArRUeAKitHgCptR4AqrUeAKvJHgCs2R4ArdkeAK7JHgCvwR4AgO0BAIHxAQCC8QEAJogAgIaQAACHdAEAKogAgC6IAIC4yQEAuckBALrZAQC70QEAvPkBAL35AQC+mQEAv5UBALBFAQCxTQEAskUBALNdAQC0RQEAtU0BALZFAQC3+QEAsz0eADKIAIA2iACAOogAgD6IAIC2WR4AtVEeAEKIAIC7iQEAuoEBAEaIAIBKiACAv4kBAL6BAQC9iQEAvJEBAE6IAIBSiACAo3UeAFaIAIClGR4AWogAgF6IAICmER4ARocAgGKIAICrwQEAqskBAK3BAQCs2QEAr8EBAK7JAQBmiACAaogAgG6IAIByiACAdogAgIQYAgB6iACAfogAgIKIAICGiACAiogAgI6IAICSiACAmogAgJ6IAIC+cAMAgGkAAIFpAACCeQAAhAAEAIbwBACHdAMAoogAgO8MHwCmiACA4aweAKqIAIDj8B4ArogAgLKIAIC2iACAuogAgL6IAIDCiACAxogAgMqIAIDvVAIAzogAgNKIAIDWiACA46QCANqIAIDhgAEA3ogAgOKIAIDmiACA6ogAgO6IAICzRQMA8ogAgPaIAID6iACA/ogAgLZFAwC1VQMAAokAgLshAwC6SQMAvqAEAAqJAIC/KQMAviEDAL01AwC8OQMAqDkCAKk5AgCqjQIAq4UCAKydAgCthQIAroUCAK+1AgCA7QEAgfUBAIL1AQAOiQCAhpAEAIcEBQASiQCAFokAgLhFAQC5TQEAukUBALtdAQC8SQEAvUkBAL55AQC/eQEAsM0CALGlAgCyrQIAs6ECALSlAgC1rQIAtp0CALd9AQAaiQCAHokAgCKJAIAmiQCAKokAgC6JAIAyiQCA74gBAITsBADhVB4ANokAgONUAQA6iQCAPokAgEKJAIBGiQCAo0UCAEqJAIBOiQCAUokAgFaJAICmRQIApVUCAFqJAICrIQIAqkkCAF6JAIBiiQCArykCAK4hAgCtNQIArDkCAKg1BgCpPQYAqlEGAKttBgCseQYArWUGAK5tBgCvZQYABokAgGaJAIBqiQCAbokAgIAZAACBGQAAggUAAHKJAIC45QYAuekGALr5BgC7+QYAvOkGAL3pBgC+nQYAv5UGALAdBgCx5QYAsu0GALPlBgC0/QYAteEGALbhBgC34QYAs9kGAL7QAwB2iQCAeokAgH6JAIC25QYAtfEGAIKJAIC7IQYAutkGAIaYAACHeAMAvyUGAL45BgC9MQYAvDkGAIaJAICjnQYAiokAgI6JAICmoQYAkokAgJaJAICltQYAqp0GAKtlBgCaiQCAnokAgK59BgCvYQYArH0GAK11BgCo7QcAqSkGAKoxBgCrMQYArJEGAK2RBgCukQYAr5EGAKKJAICmiQCAqokAgK6JAICyiQCAtokAgLqJAIC+iQCAuIUGALmNBgC6hQYAu50GALyNBgC9vQYAvrUGAL95AQCw8QYAsfEGALLxBgCzxQYAtMEGALXBBgC2wQYAt8EGALO5BgDCiQCAxokAgMqJAIDOiQCAthEGALUZBgDSiQCAuzUGALo1BgDWiQCA2okAgL8FBgC+BQYAvREGALwlBgClQQYA3okAgOKJAICmSQYAgRUAAIB5AACj4QYAghUAAK1JBgCsfQYAr10GAK5dBgCENAEAlogAgKttBgCqbQYAvswDAOqJAICzlQIA7okAgLXZAgDyiQCA9okAgLbRAgCGgAwAhzgDALvFAgC6xQIAvRUDALwVAwC/FQMAvhUDAPqJAID+iQCA71gGAIRAAwACigCABooAgAqKAIAOigCAEooAgBaKAIAaigCAHooAgOE4BgAiigCA4yQGAL5wDACsSQIArUkCAK5dAgCvVQIAqB0CAKkFAgCqBQIAq10CAISoDAAmigCAKooAgC6KAIC+vA0AMooAgDaKAIA6igCAvE0DAL1VAwC+VQMAv2UDALjpAwC56QMAul0DALtVAwC0yQMAtckDALbZAwC32QMAsBkCALEZAgCy2QMAs9kDAD6KAIDj5AAAQooAgOG8AQBGigCAgj0AAIE9AACAPQAASooAgE6KAIBSigCAWooAgF6KAIDvzAMAYooAgGaKAICj3QMAaooAgIboDACHYA0AbooAgKaZAwClkQMAcooAgKuNAwCqjQMAdooAgHqKAICvXQIArl0CAK1dAgCsXQIAfooAgIKKAICGigCAiooAgI6KAICSigCAlooAgO/gAQCEvAwA4YwGAJqKAIDjHAYAnooAgKKKAICmigCAqooAgLPVAQCuigCAsooAgLaKAIC6igCAtpEBALWZAQC+igCAu70BALq9AQDCigCAyooAgL+dAQC+nQEAvZ0BALydAQCoBQ4AqQkOAKodDgCrFQ4ArFEOAK1RDgCuSQ4Ar0kOAFaKAICCzQ8AgfUPAID9DwDGigCAzooAgIYcAACHsAMAuOkOALnpDgC6/Q4Au/UOALztDgC9VQ8AvlEPAL9NDwCwOQ4AsTkOALIJDgCzCQ4AtBkOALUZDgC2DQ4At9kOAKOVDgDSigCA1ooAgNqKAIDeigCAptEOAKXZDgDiigCAq/0OAKr9DgDmigCA6ooAgK/dDgCu3Q4Ard0OAKzdDgDuigCAs/0PAPKKAID2igCAtoEPAPqKAID+igCAtZkPALqNDwC7ZQ8AAosAgAaLAIC+fQ8Av2UPALx9DwC9dQ8AqC0OAKk1DgCqMQ4AqzEOAKxVDgCtRQ4ArkUOAK91DgAKiwCADosAgBKLAIAWiwCAGosAgB6LAIAiiwCAJosAgLjpDgC59Q4Auv0OALv1DgC87Q4AvZEOAL6RDgC/kQ4AsA0OALHlDgCy7Q4As+UOALT9DgC15Q4Atu0OALflDgCjuQ4Agi0AAIEVAACAHQAAKosAgKbFDgCl3Q4ALosAgKshDgCqyQ4AMosAgL4sAQCvIQ4ArjkOAK0xDgCsOQ4AOosAgLZVAQC1RQEANosAgLNVAQA+iwCAhngAAIdcAAC/OQEAvjEBAL0lAQC8JQEAuzEBALpZAQDmiQCAQosAgEaLAIBKiwCAhAQDAKOJAgBOiwCApZkCAKaJAgBSiwCAvyg5AFaLAICqhQIAq+0CAKz5AgCt+QIAru0CAK/lAgDjWAIA78AOAOGIAQBaiwCAXosAgGKLAIBmiwCAaosAgG6LAIByiwCAdosAgHqLAIDvKAIA4ygOAH6LAIDhRA4AqbUCAKhpDQCrAQIAqgkCAK0BAgCsGQIArzECAK4BAgC+AAQAgosAgIaLAICKiwCAjosAgJKLAICWiwCAmosAgLnlAwC45QMAu+UDALrlAwC95QMAvOUDAL/lAwC+5QMAsSECALBJAgCzJQIAsiUCALUpAgC0IQIAtxUCALYVAgCowQIAqdECAKr1AgCrDQEArBUBAK0FAQCuBQEArzkBAJ6LAICiiwCAqosAgK6LAICyiwCAtosAgLqLAIC+iwCAuC0BALk9AQC67QEAu+UBALz9AQC95QEAvu0BAL/lAQCwLQEAsTUBALI9AQCzNQEAtC0BALUVAQC2HQEAtxUBAIA9AQCBpQAAgq0AAO/YAACGsAUAh9gFAMKLAIDv1A8AhGwEAOH0DgDGiwCA4xwPAMqLAIDhlAEAzosAgOMMDgCzPQIA0osAgNaLAIDaiwCA3osAgLbFAQC13QEA4osAgLuxAQC6qQEA5osAgOqLAIC/kQEAvqkBAL2hAQC8qQEAposAgO6LAICqRQYAq10GAKxFBgCtTQYArkUGAK99BgDyiwCA9osAgPqLAICj0QUA/osAgKUxBgCmKQYAAowAgAaMAICCHQAAgR0AAIAdAAAKjACADowAgBKMAIC+lAMAFowAgBqMAICGSAMAh8wDAB6MAIAijACAJowAgCqMAICoqQcAqakHAKq5BwCruQcArKkHAK2pBwCuAQcArzUHAC6MAIAyjACANowAgDqMAIA+jACAQowAgEaMAIBKjACAuC0HALnBAAC66QAAu+kAALz5AAC95QAAvuUAAL+dAACwUQcAsV0HALItBwCzJQcAtD0HALUlBwC2JQcAtxUHALMxBgBOjACAUowAgFaMAIBajACAtikGALUhBgBejACAu5kGALqVBgBijACAZowAgL/hBgC++QYAvfEGALz5BgBqjACAo3UGAG6MAIByjACApm0GAHaMAIB6jACApWUGAKrRBgCr3QYAfowAgIKMAICuvQYAr6UGAKy9BgCttQYAqOUBAKn1AQCq/QEAq/UBAKztAQCtNQEArj0BAK81AQCA+QAAgc0AAILFAACEYAEAvngBAIqMAICHrAAAhpABALjRAAC52QAAuuEAALvhAAC8kQAAvZ0AAL6VAAC/iQAAsE0BALFVAQCyXQEAs1UBALRNAQC18QAAtvEAALfxAACzdQIAjowAgJKMAICWjACAmowAgLa1AgC1ZQIAnowAgLuRAgC6iQIAoowAgKaMAIC/NQMAvokCAL2BAgC8iQIAqowAgKMxAgCujACAhMADAKbxAgCyjACAtowAgKUhAgCqzQIAq9UCALqMAIC+jACArs0CAK9xAwCszQIArcUCAKuNAACqjQAAqY0AAKg5AwCvvQAArr0AAK2FAACsjQAAqgAAAKsAAADCjACAxowAgMqMAIDOjACA0owAgNaMAIC7fQAAun0AALl9AAC4fQAAv90BAL7dAQC93QEAvN0BALO5AACysQAAsaEAALCtAAC3XQAAtl0AALWVAAC0lQAA2owAgN6MAIDijACA5owAgIE1AACADQAA6owAgII1AAC+rD0A7owAgPKMAICFaD0A+owAgP6MAICGODwAh8ACALNJAQACjQCA0AAAAAaNAIAKjQCAtkkBALVJAQAOjQCAuykBALolAQASjQCAFo0AgL8dAQC+HQEAvSEBALwpAQDjNDYA4QwGAOGwAgDjPAYAGo0AgB6NAIAijQCAJo0AgIQsPwC+oD8AKo0AgC6NAIDvfDcAMo0AgDaNAIDvGAEAOo0AgD6NAICGaD4Ah8w/AEKNAIBGjQCASo0AgO+UAABOjQCA4ZQBAFKNAIDjUAAAVo0AgILpPwCB6T8AgPE/AKMJPgCPASQA9owAgFqNAIBejQCApgk+AKUJPgBijQCAq2k+AKplPgBmjQCAao0AgK9dPgCuXT4ArWE+AKxpPgCeYTgAn3U4AJzBNACdtTkAmqU1AJt1NACYeTAAmXExAJYhLQCXhTEAlG0sAJVlLACSeSgAk6UtAJBRJACReSgAsQ0UALAFFACzARgAslUUALV5GAC0tRgAbo0AgHKNAIB2jQCAeo0AgH6NAICCjQCAotE8AKMlAQCgdTkAob08AKHJAACGjQCAowEEAKLlAAClHQQApPUEAKf5CACmAQgAqQEMAKhtCACrzQwAqs0MAK3REACsARAAr9URAK7ZEACCBSUAgy0lAIqNAICOjQCAhsEsAIcRLQCEHSkAhRUpAIopLQCLZSwAko0AgJaNAICOHTAAj8E0AIzZMACNHTEAkmE1AJPNNQCajQCAno0AgJZhOQCXmTgAlKE4AJV9OQCaYT0AmwU9AKKNAICmjQCAqo0AgK6NAICc6QAAso0AgLaNAIC6jQCAvo0AgMKNAICGjACAxo0AgMqNAIDOjQCAqJE+AKmRPgCq7T4Aq+E+AKzhPgCt6T4ArtE+AK/RPgCwUT4AsVE+ALJRPgCzUT4AtHk+ALV5PgC2bT4At2U+ALghPgC5IT4Aujk+ALs5PgC8KT4AvRU+AL4RPgC/DT4AgJkDAIGZAwCCBQAA0o0AgL5UAwDhsD0A2o0AgONAPgCEOAIA3o0AgOKNAIDv9D8A5o0AgOqNAICGmAQAhxwDALMFPQCECAQA7o0AgPKNAID2jQCAtgk9ALUJPQD6jQCAu/U9ALr1PQD+jQCAAo4AgL/dPQC+3T0AveU9ALzlPQAGjgCACo4AgKPNPQC+xAQApcE9AA6OAIASjgCApsE9ABaOAIAajgCAqz09AKo9PQCtLT0ArC09AK8VPQCuFT0AtmkCAB6OAIAijgCAtWkCACaOAICzSQIAKo4AgC6OAIC+qQMAv6kDALzBAwC9wQMAuvkDALv5AwAyjgCANo4AgKgtAwCpnQMAqpUDAKutAwCstQMArb0DAK61AwCv2QMAgA0AAIEVAACCHQAAOo4AgD6OAIBCjgCAh7QFAIacBAC4MQIAuTECALo1AgC7zQIAvNUCAL3dAgC+1QIAv8kCALBpAgCxaQIAskECALNBAgC0OQIAtTkCALYRAgC3EQIASo4AgOM0PgBOjgCA4aw+AFKOAIDvfAMAVo4AgFqOAIBejgCA45QDAGKOAIDhfD4AZo4AgO/oPgBqjgCAbo4AgHKOAIB2jgCAo1UDAHqOAICldQMAfo4AgIKOAICmdQMAho4AgIqOAICr5QIAquUCAK3dAgCs3QIAr7UCAK61AgCoGQYAqSEGAKohBgCrPQYArCUGAK1dBgCuVQYAr00GAEaOAICOjgCAko4AgJaOAICajgCAno4AgKKOAICmjgCAuOUGALmBBgC6gQYAu50GALyJBgC9iQYAvqEGAL+hBgCwPQYAsQ0GALIFBgCz7QYAtPUGALXhBgC24QYAt90GALOpBgCCLQAAgRUAAIAdAACqjgCAtt0GALWtBgCujgCAu8kGALr5BgCyjgCAhOADAL8lBgC+MQYAvTkGALzRBgC+iAMAo+0GANaNAIC2jgCAppkGALqOAIC+jgCApekGAKq9BgCrjQYAhkgAAIdsAACudQYAr2EGAKyVBgCtfQYAqIEGAKmNBgCqmQYAq5UGAKyNBgCttQYArrEGAK+tBgDCjgCAxo4AgMqOAIDOjgCA0o4AgNaOAIDajgCA3o4AgLilBgC5YQEAumEBALthAQC8YQEAvWEBAL5hAQC/YQEAsNkGALHZBgCyqQYAs6kGALS9BgC1oQYAtqEGALedBgCzEQYA4o4AgOaOAIDqjgCA7o4AgLY1BgC1BQYA8o4AgLsdBgC6HQYA9o4AgPqOAIC/ZQYAvnkGAL19BgC8fQYA/o4AgKNVBgACjwCABo8AgKZxBgAKjwCADo8AgKVBBgCqWQYAq1kGABKPAIAWjwCArj0GAK8hBgCsOQYArTkGAKjVAgCp3QIAqikDAKspAwCsOQMArTkDAK4pAwCvKQMAGo8AgB6PAIAijwCAKo8AgC6PAIAyjwCAvrgDADaPAIC47QMAuYUDALqBAwC7gQMAvIUDAL2NAwC+sQMAv7EDALBZAwCxWQMAsu0DALPlAwC0/QMAteUDALblAwC31QMAgKEAAIGhAACCoQAAvoAMADqPAICEmAIAPo8AgEKPAICGAAwAh/QDAEaPAIBKjwCATo8AgFKPAIBWjwCAhLADALPhAwBajwCAXo8AgGKPAIBmjwCAtvkDALXxAwBqjwCAu90DALrdAwBujwCAco8AgL9hAwC+eQMAvXEDALx5AwB2jwCAeo8AgH6PAICjLQIAgo8AgKU9AgCmNQIAho8AgIqPAICOjwCAqhECAKsRAgCstQIArb0CAK61AgCvrQIA48QDAOMQBwDhuAEA4WwHAIBxAACBcQAAggUAAJKPAICGwAwAh1QNAJqPAICejwCA77ADAO8ABwCijwCApo8AgKqPAICujwCAso8AgLaPAIC6jwCAvo8AgMKPAIDvpAEAhKANAOGABgDGjwCA4xABAMqPAIDOjwCA0o8AgNaPAICz9QEA2o8AgN6PAIDijwCA5o8AgLZNAQC1SQEA6o8AgLtRAQC6SQEA7o8AgPKPAIC/OQEAvjEBAL1BAQC8SQEAqC0OAKk1DgCqPQ4AqzEOAKyBDgCtjQ4AroUOAK+1DgCWjwCA9o8AgPqPAID+jwCAgBkAAIEZAACCBQAAApAAgLidDgC5rQ4AuqUOALtNDwC8VQ8AvV0PAL5JDwC/QQ8AsM0OALHVDgCy3Q4As9UOALS1DgC1vQ4AtrUOALetDgCjtQ4AvogDAAaQAIAKkACADpAAgKYNDgClCQ4AEpAAgKsRDgCqCQ4AhggAAIdsAwCveQ4ArnEOAK0BDgCsCQ4AFpAAgBqQAIAekACAs7UPACKQAIC1VQ8Atl0PACaPAIAmkACAKpAAgLp5DwC7eQ8AvGkPAL1dDwC+SQ8Av0kPAKhpDgCpaQ4AqnEOAKtxDgCskQ4ArZEOAK6RDgCvkQ4ALpAAgDKQAIA2kACAOpAAgD6QAIBCkACARpAAgEqQAIC4hQ4AuY0OALqFDgC7nQ4AvI0OAL29DgC+tQ4Av3kBALDxDgCx8Q4AsvEOALPFDgC0wQ4AtcEOALbBDgC3wQ4Ao/kOAE6QAIBSkACAVpAAgFqQAICmEQ4ApRkOAF6QAICrNQ4AqjUOAGKQAIBmkACArwUOAK4FDgCtEQ4ArCUOAIANAACBFQAAgh0AAGqQAIBukACAcpAAgISUAQC+lAEAhkAHAIf0AAB6kACAfpAAgIKQAICGkACAipAAgI6QAICojQIAqZUCAKqVAgCrzQIArNUCAK3dAgCuyQIAr/0CAJKQAICWkACAmpAAgJ6QAIC/ABQAopAAgKaQAICqkACAuH0DALnBAwC6wQMAu8EDALzBAwC9yQMAvvEDAL/xAwCwhQIAsUUDALJNAwCzRQMAtF0DALVFAwC2TQMAt0UDALMdAgCukACAspAAgLaQAIC6kACAtl0CALVdAgC+kACAu4EDALpBAgDCkACAxpAAgL+BAwC+mQMAvZEDALyZAwDKkACAo1kCAM6QAIDSkACAphkCANaQAIDakACApRkCAKoFAgCrxQMA3pAAgOKQAICu3QMAr8UDAKzdAwCt1QMA6pAAgOPMAACEBAIA4bwBAIDJAQCB/QEAgvUBAL4QBQDukACAvigEAPKQAID2kACA+pAAgO8QAAD+kACAApEAgIbgBACH9AIABpEAgAqRAIDj/A8ADpEAgOHgDwASkQCA7xQPABaRAIAakQCAHpEAgCKRAIAmkQCAKpEAgC6RAIAykQCANpEAgDqRAIA+kQCAQpEAgEaRAIBKkQCA7+ABAIUEEgDh3A4ATpEAgOMcDgCAKQAAgR0AAIIFAABSkQCAszECAFqRAICEzAUAXpEAgGKRAIC2KQIAtSECAGaRAIC7zQEAus0BAGqRAIBukQCAv3UBAL7JAQC9wQEAvMkBAKjpBQCp6QUAqvkFAKv5BQCs6QUArekFAK45BgCvOQYA5pAAgFaRAICGiAAAhwADAHKRAIB2kQCAepEAgH6RAIC40QYAudkGALrhBgC74QYAvJEGAL2dBgC+lQYAv4kGALBJBgCxSQYAsl0GALNVBgC0TQYAtfEGALbxBgC38QYAo3EFAIKRAICGkQCAipEAgI6RAICmaQUApWEFAJKRAICrjQYAqo0GAJaRAICakQCArzUGAK6JBgCtgQYArIkGAJ6RAICikQCAs+EHAKaRAIC14QcAqpEAgK6RAIC25QcAdpAAgLKRAIC7vQcAuqEHAL2VBwC8qQcAv5UHAL6VBwCoAQYAqSUGAKohBgCrIQYArCEGAK0tBgCuJQYAr1UGALaRAICCHQAAgR0AAIAdAAC6kQCAvpEAgMKRAIC+MAEAuDkGALk5BgC6yQYAu8kGALzZBgC92QYAvskGAL/JBgCwLQYAsTEGALI1BgCzCQYAtBkGALUZBgC2CQYAtwkGAKOpBgCEjAIAhigfAIdEAQDKkQCApq0GAKWpBgDOkQCAq/UGAKrpBgDSkQCA1pEAgK/dBgCu3QYArd0GAKzhBgDakQCAsxUGAN6RAIDikQCAtj0GAOaRAIDqkQCAtTUGALrZAQC72QEA7pEAgPKRAIC+fQEAv2UBALx9AQC9dQEAqMUFAKnJBQCq2QUAq9EFAKz5BQCt+QUArikCAK8pAgD2kQCA+pEAgP6RAIACkgCAjAAAAAaSAIAKkgCADpIAgLjtAgC5hQIAuo0CALuBAgC8hQIAvY0CAL69AgC/fQMAsFkCALFZAgCy7QIAs+UCALT9AgC15QIAtuUCALfVAgCjUQUAEpIAgBaSAIAakgCAHpIAgKZ5BQClcQUAIpIAgKudAgCqnQIAJpIAgCqSAICvIQIArjkCAK0xAgCsOQIAghEAAC6SAICAZQAAgQkAADKSAIC+mAMAOpIAgD6SAICEJAMAQpIAgIdoAwCGjBwARpIAgEqSAIBOkgCAUpIAgFaSAIBakgCAs6ECAITAHAC10QIAXpIAgGKSAIC21QIAZpIAgGqSAIC7wQIAuvUCAL0RAQC82QIAvxEBAL4ZAQBukgCAcpIAgHaSAIB6kgCAfpIAgIKSAICGkgCA77gGAIqSAIDhnAQAjpIAgON0BgCSkgCAlpIAgJqSAICekgCAgPkAAIH5AACCBQAAopIAgL5YHACEWB8A71wAAO9ABgDhkAEA4fwGAOM8AADjdAYAqpIAgK6SAICGmBwAh/QcAKNpAgC+DB8AspIAgLaSAIC6kgCAph0CAKUZAgC+kgCAqwkCAKo9AgDCkgCAxpIAgK/ZAQCu0QEArdkBAKwRAgCokR0AqZkdAKqhHQCroR0ArNEdAK3dHQCu1R0Ar8kdADaSAICmkgCAypIAgM6SAIDSkgCA1pIAgNqSAIDekgCAuHkeALl5HgC6zR4Au8UeALzdHgC9xR4AvsUeAL/1HgCwuR0AsY0dALKFHQCzTR4AtFUeALVdHgC2VR4At0keALjNHwC51R8Aut0fALvVHwC88R8Avf0fAL7pHwC/6R8AsKUfALGxHwCysR8As40fALSVHwC19R8Atv0fALf1HwCoGR4AqRkeAKotHgCrPR4ArCUeAK0tHgCuJR4Ar90fAOKSAIDmkgCA6pIAgO6SAIDykgCAxpEAgPaSAID6kgCAs+UfAP6SAIACkwCABpMAgAqTAIC27R8Ate0fAA6TAIC7NR4AuiEeABKTAIAWkwCAv3EeAL4RHgC9GR4AvCUeAIJpAACjoR8AgFkAAIFRAACmqR8AGpMAgB6TAIClqR8AqmUeAKtxHgCGAAQAh+wBAK5VHgCvNR4ArGEeAK1dHgCoMR4AqTEeAKpBHgCrQR4ArEEeAK1JHgCucR4Ar3EeACKTAIAmkwCAKpMAgC6TAIAykwCANpMAgDqTAIA+kwCAuCkBALkpAQC6OQEAuzUBALwtAQC90QAAvtEAAL/RAACwyQEAsckBALLZAQCz2QEAtMkBALXJAQC2GQEAtxkBALPJHQBCkwCARpMAgEqTAIBOkwCAtskdALXJHQBSkwCAuw0CALoNAgBWkwCAWpMAgL8NAgC+DQIAvQ0CALwNAgBekwCAo40dAGKTAIBmkwCApo0dAGqTAIBukwCApY0dAKpJAgCrSQIAcpMAgHaTAICuSQIAr0kCAKxJAgCtSQIAgA0AAIERAACCEQAAepMAgO/MAgB+kwCAgpMAgISQAgDjLAIAvigDAOHYAQCKkwCAhhAEAIfUAwCOkwCAkpMAgLNhAwCWkwCAmpMAgJ6TAICikwCAtnkDALVxAwCmkwCAu10DALpdAwCqkwCArpMAgL/hAAC++QAAvfEAALz5AACjoQIAspMAgLaTAIC6kwCAvpMAgKa5AgClsQIAwpMAgKudAgCqnQIAxpMAgMqTAICvIQEArjkBAK0xAQCsOQEAzpMAgNKTAIDvZB8A1pMAgNqTAIDekwCA4pMAgOaTAICADQAAgREAAIIVAADqkwCA4eAcAO6TAIDjiB8A8pMAgISAAgC+jAUAh0gFAIYsBAD6kwCA/pMAgO+kHgDv9B4A4QAeAOFQHwDjLB4A47AeAAKUAIAGlACACpQAgA6UAIASlACAFpQAgISEBACzcQEAGpQAgLUdAQC2FQEAHpQAgCKUAIAmlACAugEBALsBAQC89QAAvf0AAL71AAC/7QAAqK0GAKm9BgCqtQYAq8kGAKzZBgCt2QYArskGAK/BBgAqlACALpQAgDKUAIA2lACAOpQAgD6UAIBClACARpQAgLhtBwC5BQcAug0HALsBBwC8AQcAvQEHAL4BBwC/AQcAsIkGALGJBgCybQcAs2UHALR9BwC1ZQcAtmUHALdVBwCGkwCAozkGAEqUAID2kwCApl0GAE6UAIBSlACApVUGAKpJBgCrSQYAVpQAgFqUAICuvQcAr6UHAKy9BwCttQcAgG0AAIEJAACCGQAAXpQAgGKUAIC+nAMAZpQAgGqUAICGQAAAh2AAAG6UAIBylACAdpQAgHqUAIB+lACAgpQAgKiRBgCpkQYAqrkGAKu5BgCsqQYArakGAK7ZBgCv2QYAhpQAgIqUAICOlACAkpQAgJaUAICalACAnpQAgKKUAIC4cQEAuXEBALpxAQC7cQEAvNkBAL3BAQC+wQEAv/UBALCxBgCxuQYAsokGALOJBgC0UQEAtVEBALZRAQC3UQEAszEGAKaUAICqlACArpQAgLKUAIC2KQYAtSEGALaUAIC7fQYAunUGALqUAIC+lACAv5UBAL6VAQC9XQYAvF0GAMKUAICjdQYAxpQAgMqUAICmbQYAzpQAgNKUAIClZQYAqjEGAKs5BgCErAEAvqABAK7RAQCv0QEArBkGAK0ZBgCo3QIAqe0CAKrlAgCr/QIArOUCAK3tAgCu5QIArz0DANqUAIDelACA4pQAgL5kDADmlACA6pQAgO6UAIDylACAuMkDALnJAwC62QMAu9EDALz5AwC9+QMAvpkDAL+VAwCwRQMAsU0DALJFAwCzXQMAtEUDALVNAwC2RQMAt/kDAIFVAwCASQMAs2UCAIJVAwC1ZQIA9pQAgPqUAIC2ZQIAhgAMAIfkAwC7gQMAuokDAL2BAwC8mQMAv4EDAL6JAwCjLQIA/pQAgAKVAIAGlQCACpUAgKYtAgClLQIADpUAgKvJAwCqwQMAEpUAgBaVAICvyQMArsEDAK3JAwCs0QMA49gGAOGsBwDhnAYA45wGABqVAICEWA0AHpUAgCKVAIAmlQCAKpUAgC6VAIAylQCA7xwBADaVAIA6lQCA70AGAIB5AACBFQAAghEAAIQADAA+lQCA46wAAEKVAIDhpAEASpUAgO9wAACGyAwAh6QNAE6VAIBSlQCAVpUAgFqVAIC6yQUAu8kFALilBQC5zQUAvvkFAL/5BQC8zQUAvcUFALKlBQCzrQUAsBEGALERBgC2rQUAt50FALS1BQC1rQUAqmEGAKthBgConQYAqZUGAK5hBgCvYQYArHEGAK1xBgBelQCAYpUAgGaVAIBqlQCAbpUAgHKVAIC+sAwAdpUAgKghDgCpIQ4AqiEOAKs9DgCsJQ4ArS0OAK4lDgCviQ4ARpUAgHqVAIB+lQCAgpUAgIaVAICKlQCAjpUAgJKVAIC4UQ8AuV0PALpVDwC7bQ8AvHUPAL19DwC+dQ8Av2kPALD5DgCxoQ4AsqEOALOhDgC0oQ4AtakOALaRDgC3kQ4As6kOAJaVAIDWlACAmpUAgJ6VAIC2rQ4Ata0OAKKVAIC7ZQ4Auj0OAKaVAICqlQCAv20OAL5lDgC9dQ4AvHUOAIIZAACj7Q4AgGUAAIEZAACm6Q4ArpUAgLKVAICl6Q4AqnkOAKshDgC2lQCAupUAgK4hDgCvKQ4ArDEOAK0xDgCoYQ4AqXUOAKp9DgCrdQ4ArG0OAK31DgCu/Q4Ar/UOAIaAAQCHpAEAvpUAgMKVAIDGlQCAypUAgM6VAIDSlQCAuHUBALl9AQC6dQEAu8kBALzdAQC9xQEAvsUBAL/1AQCwjQ4AsZUOALKdDgCzkQ4AtFUBALVdAQC2VQEAt00BALP1DgDWlQCA2pUAgN6VAIDilQCAtnUOALXlDgDmlQCAu1EOALpJDgDqlQCA7pUAgL+ZAQC+kQEAvUUOALxJDgDylQCAo7EOAPaVAID6lQCApjEOAP6VAIAClgCApaEOAKoNDgCrFQ4ABpYAgAqWAICu1QEAr90BAKwNDgCtAQ4AqO0CAKktAwCqJQMAqz0DAKwlAwCtLQMAriUDAK+ZAwAOlgCAEpYAgBaWAIAalgCAHpYAgCKWAIC+dAIAKpYAgLiNAwC5kQMAupEDALulAwC8vQMAvXUAAL59AAC/dQAAsOkDALHpAwCy+QMAs/EDALTZAwC12QMAtrkDALe1AwCArQAAgbUAAIK9AACzoQMALpYAgLWhAwC2oQMAMpYAgITgAgA2lgCAuiEDALshAwC8IQMAvSkDAL4RAwC/EQMAo+0DAIXABACFtG8AOpYAgD6WAICm7QMApe0DAEKWAICrbQMAqm0DAIZIBQCHbAMAr10DAK5dAwCtZQMArG0DAEaWAIDjAA4A71hsAOG0DwBKlgCATpYAgFKWAIBWlgCAoakDAKD9DwCjwQMAog0DAOHgAwDv4A8A4+QDAFqWAIBelgCAYpYAgIQEBAC+BAQAZpYAgO+UAwBqlgCAbpYAgHKWAIDj1AMAdpYAgOFUAAB6lgCAfpYAgIKWAICGlgCAgA0AAIEVAACCHQAAipYAgI6WAICSlgCAj5EbAO+cDgCE4AcA4dQOAJqWAIDj8A4AnpYAgKKWAICGGAcAh5AEAJnlFwCY5RcAm+kLAJo5CwCd/QoAnPELAJ9VDwCeXQ8AkSkfAJDNGwCTJR8Aks0fAJXREwCUKRMAlxkXAJZ1EwCM4RAAjSUQAI4tEACP+QwAJpYAgJaWAICKORQAi5UUAITpGACFBRgAhuUYAIfxFACmlgCAqpYAgIIxHACDFRwAnKkEAK6WAICylgCAtpYAgLqWAIC+lgCAmtEEAJt9BACUTQ0AleUIAJblCACXtQgAwpYAgMaWAICSWQwAk1kMAKGRAADKlgCAowF8AKKZAACluXwApJF8AKeZeACm4X0AqYF5AKiheACriXQAqgF0AK0BcACsWXQAr4VwAK6dcACx4WwAsAFsALMBaACyHWwAtfVoALT1aADOlgCA0pYAgNaWAIDalgCA3pYAgOKWAIDmlgCA6pYAgO6WAIDylgCAqD0HAKmVBwCqlQcAq6kHAKzdBwCtxQcArsUHAK8dBgD2lgCAgh0AAIEdAACAHQAA+pYAgP6WAIAClwCAvmABALgZBgC5GQYAuikGALslBgC8IQYAvSEGAL4hBgC/IQYAsHEGALFxBgCycQYAs3EGALRNBgC1NQYAtj0GALctBgCzHQcACpcAgIYoAACHqAAADpcAgLZFBwC1VQcAEpcAgLu1BgC6tQYAFpcAgBqXAIC/8QYAvokGAL2lBgC8pQYAHpcAgKNZBwAilwCAJpcAgKYBBwAqlwCALpcAgKURBwCq8QYAq/EGADKXAIA2lwCArs0GAK+1BgCs4QYAreEGAKipBQCptQUAqr0FAKs9AgCsJQIArVECAK5RAgCvUQIAOpcAgD6XAIBClwCARpcAgIQ8AwBKlwCATpcAgFKXAIC4pQIAua0CALqlAgC7vQIAvKUCAL2tAgC+pQIAv30DALAxAgCxMQIAshkCALMZAgC09QIAta0CALalAgC3nQIAVpcAgFqXAIBelwCAszkFAGKXAIC1oQIAtt0CAGaXAIBqlwCAbpcAgLr5AgC7+QIAvMECAL3BAgC+PQIAv2UCAHKXAICmgQIApf0CAHqXAICjZQUAvlh8AIbYfACHnHwArzkCAK5hAgCtnQIArJ0CAKulAgCqpQIAfpcAgIKXAICohQIAqZUCAKqVAgCrpQIArL0CAK3VAgCu0QIAr9ECAIGFAQCAhQEAhpcAgILtAQCKlwCAjpcAgJKXAICWlwCAuHUBALl9AQC6dQEAu80BALzVAQC93QEAvskBAL/BAQCwtQIAsb0CALKBAgCzgQIAtFEBALVRAQC2UQEAt1EBAJqXAICelwCAopcAgKaXAIDhMAYA4WQHAOMoBgDjxAYAhCB9AKqXAIDvbAAA7xgGAK6XAICylwCAtpcAgLqXAICzXQIAvkh8AL6XAIDClwCAxpcAgLYVAgC1dQIAypcAgLs5AgC6MQIAzpcAgNKXAIC/1QEAvtUBAL0VAgC8FQIAo519AHaXAIDWlwCA2pcAgN6XAICm1X0ApbV9AOKXAICr+X0AqvF9AOaXAIDqlwCArxV+AK4VfgCt1X0ArNV9AIBNAACBVQAAglUAALOxfgDulwCAtWV/ALZtfwDylwCAhkADAIcEAwC66X8Au+l/ALz5fwC9+X8Avt1/AL/NfwD2lwCA+pcAgAaXAID+lwCAApgAgAaYAIAKmACADpgAgKhtfgCpXX4AqlV+AKuFfwCsgX8ArYF/AK6BfwCvgX8AsEF/ALFBfwCyQX8As0F/ALR1fwC1ZX8Atm1/ALdlfwC4XX8AuS1/ALolfwC7PX8AvC1/AL0dfwC+FX8Av/UAAKP9fwASmACAFpgAgBqYAIAemACApiF+AKUpfgAimACAq6V+AKqlfgAmmACAKpgAgK+BfgCukX4ArbV+AKy1fgAumACAMpgAgDaYAIA6mACAPpgAgEKYAIBGmACASpgAgIA9AACBCQAAghkAAE6YAIBSmACAhLgBAL6wAQBWmACAqK0BAKnVAQCq1QEAqw0BAKwVAQCtGQEArgkBAK8JAQCGAAQAhwQBAFqYAIBemACAYpgAgGaYAIBqmACAbpgAgLjtAAC5hQAAuo0AALuFAAC8nQAAvYUAAL6NAAC/hQAAsHkBALF5AQCy7QAAs+UAALT9AAC15QAAtuUAALfVAACzXQIAcpgAgHaYAIB6mACAfpgAgLaZAgC1nQIAgpgAgLu9AgC6vQIAhpgAgIqYAIC/IQMAvjkDAL0xAwC8OQMAvigDAKMZAgCOmACAkpgAgKbdAgCWmACAmpgAgKXZAgCq+QIAq/kCAJ6YAICimACArn0DAK9lAwCsfQMArXUDAL7IBACmmACAqpgAgL7EBQCumACAspgAgLaYAIC6mACAgD0AAIEJAACCGQAAvpgAgMKYAICEOAMAypgAgM6YAIDveAIA0pgAgIZIBACHVAMA1pgAgNqYAIDemACA4pgAgOaYAIDqmACA7pgAgPKYAIDjVAIA9pgAgOFAAQD6mACA/pgAgOMkfwACmQCA4Zx8AAaZAIAKmQCADpkAgBKZAICEbAUAFpkAgBqZAIAemQCAIpkAgO8YfwAmmQCAKpkAgLPxAgAumQCAMpkAgDqZAIA+mQCAtukCALXhAgBCmQCAu3EBALppAQCHoAUAhswEAL85AQC+WQEAvVEBALxhAQDhQH8ARpkAgOM4fgCEwAQAgtkAAO8UAACApQAAgdkAAEqZAIDjwAAATpkAgOHUAQBSmQCAVpkAgO+EfgBamQCAqs0BAKvVAQBemQCAYpkAgK79AQCvnQEArMUBAK31AQBmmQCAo1UCAGqZAIBumQCApk0CAHKZAIB2mQCApUUCAMaYAIA2mQCAepkAgH6ZAICCmQCAhpkAgIqZAICOmQCAqJkGAKmZBgCq7QYAq/0GAKzlBgCt7QYAruUGAK/dBgCwpQYAsa0GALKlBgCzuQYAtK0GALVVBwC2UQcAt00HALh1BwC5fQcAunUHALtJBwC8WQcAvVkHAL5JBwC/RQcAs0UGAJKZAICWmQCAmpkAgJ6ZAIC2TQYAtU0GAKKZAIC7SQYAukEGAIYIAACHjAAAv7EHAL5JBgC9TQYAvFEGAIJdAACjAQYAgEUAAIFdAACmCQYAqpkAgK6ZAIClCQYAqgUGAKsNBgCymQCAtpkAgK4NBgCv9QcArBUGAK0JBgCoTQYAqVUGAKpVBgCriQYArLEGAK29BgCuqQYAr6kGAKaZAIC6mQCAvpkAgMKZAIDGmQCAypkAgM6ZAIDSmQCAuEkBALlJAQC6WQEAu1kBALxJAQC9SQEAvt0BAL/VAQCw3QYAsa0GALKlBgCzjQYAtJkGALWZBgC2jQYAt4UGALPdBgDWmQCA2pkAgN6ZAIDimQCAtj0GALU5BgDmmQCAu2kGALoZBgDqmQCA7pkAgL9dBgC+XQYAvVkGALxxBgDymQCAo5kGAPaZAID6mQCApnkGAP6ZAIACmgCApX0GAKpdBgCrLQYABpoAgAqaAICuGQYArxkGAKw1BgCtHQYAqNUCAKndAgCq4QIAq+ECAKw1AwCtPQMArjUDAK8tAwCAzQMAgQkAAIIZAAAOmgCAEpoAgIQYAgC+dAMAGpoAgLjpAwC56QMAuokDALuFAwC8nQMAvYEDAL6BAwC/tQMAsFUDALFdAwCyVQMAs+kDALT5AwC1+QMAtukDALfhAwCGIAwAhxADAB6aAIAimgCAJpoAgCqaAIAumgCA71wCADKaAIDhFAAANpoAgOOIAgC++AwAOpoAgD6aAIBCmgCAu/kDALrxAwC+gA0ARpoAgL9dAwC+XQMAvV0DALzhAwCzCQIASpoAgE6aAIBSmgCAVpoAgLbdAwC13QMAWpoAgKipBgCpqQYAqrkGAKu5BgCsqQYArakGAK4dBQCvFQUAXpoAgGKaAIBmmgCAapoAgG6aAIBymgCAdpoAgHqaAIC4GQUAuS0FALolBQC7yQUAvNkFAL3FBQC+zQUAv8UFALBtBQCxdQUAsnUFALNFBQC0XQUAtT0FALY1BQC3KQUA4fQGAOFUBwDjFAYA47wGAIEJAACAqQAAfpoAgII5AACE7A0AgpoAgIeIDACGDAwAipoAgI6aAIDvzAcA78QHAKMpAwCSmgCAlpoAgJqaAICemgCApv0CAKX9AgCimgCAq9kCAKrRAgCmmgCAqpoAgK99AgCufQIArX0CAKzBAgCoPQ4AqY0OAKqFDgCrnQ4ArIUOAK2NDgCuuQ4Ar7UOAIaaAICumgCAspoAgLaaAIC6mgCAvpoAgMKaAIDGmgCAuL0OALllDwC6bQ8Au2UPALx9DwC9ZQ8Avm0PAL9lDwCw1Q4Asd0OALLVDgCzoQ4AtJUOALWdDgC2lQ4At40OALMNDgDKmgCAzpoAgNKaAIDWmgCAtg0OALUNDgDamgCAuxkOALoRDgDemgCAFpoAgL9ZDgC+UQ4AvXUOALwBDgDimgCAo0kOAOaaAIDqmgCApkkOAO6aAIDymgCApUkOAKpVDgCrXQ4AhKQDAPaaAICuFQ4Arx0OAKxFDgCtMQ4AqLEOAKmxDgCqzQ4Aq8UOAKzdDgCtxQ4ArsUOAK/1DgCA7QEAgfEBAILxAQD6mgCAhpABAIe0AQD+mgCAApsAgLjFAQC5zQEAusUBALvdAQC8zQEAvf0BAL6ZAQC/lQEAsI0OALFBAQCyQQEAs0EBALRBAQC1QQEAtkEBALdBAQCzRQ4ABpsAgAqbAIAOmwCAEpsAgLZFDgC1VQ4AFpsAgLuFAQC6SQ4AGpsAgB6bAIC/hQEAvoUBAL2VAQC8lQEAIpsAgKMBDgAmmwCAKpsAgKYBDgAumwCAMpsAgKURDgCqDQ4Aq8EBADabAIA6mwCArsEBAK/BAQCs0QEArdEBAKgtAwCpPQMAqjUDAKuJAwCsmQMArZkDAK6JAwCvgQMAPpsAgEKbAIBGmwCASpsAgE6bAIBSmwCAVpsAgFqbAIC4rQMAuWUAALptAAC7ZQAAvH0AAL1lAAC+bQAAv2UAALDJAwCxyQMAsqkDALOlAwC0vQMAtaEDALahAwC3lQMAgL0AAIEJAACCGQAAXpsAgGKbAIC+2AMAapsAgG6bAICErAIAcpsAgIfoAwCGDAQAdpsAgHqbAIB+mwCAgpsAgLP9AwCGmwCAipsAgI6bAICSmwCAtlkDALVRAwCWmwCAu00DALpNAwCamwCAnpsAgL8lAwC+OQMAvTEDALw9AwCimwCAppsAgKqbAICumwCA71gPALKbAIC2mwCAupsAgOOQDgC+mwCA4bAPAMKbAIDGmwCAypsAgM6bAIDSmwCAgHUAAIF9AACCdQAAhBgFAO88AwDamwCAvhQFAN6bAIDj0AMA4psAgOFAAADmmwCAhtAEAIdYBQDqmwCA7psAgPKbAID2mwCA+psAgP6bAIACnACABpwAgAqcAIDvrA8AhOwEAOEQDgAOnACA41QBABKcAIAWnACAGpwAgB6cAICj/QIAIpwAgCacAIAqnACALpwAgKZZAgClUQIAMpwAgKtNAgCqTQIANpwAgDqcAICvJQIArjkCAK0xAgCsPQIAqJkGAKmZBgCqrQYAq70GAKylBgCtrQYArqUGAK/ZBgDWmwCAghEAAIEZAACAwQcAPpwAgEKcAIC+cAMARpwAgLhJBwC5SQcAul0HALtVBwC8TQcAvXEHAL51BwC/bQcAsKkGALGpBgCyuQYAs7EGALSZBgC1mQYAtnkHALd5BwC1NQYASpwAgE6cAIC2NQYAhjAAAIdcAwCzPQYAUpwAgL19BgC8dQYAv0UGAL5FBgBmmwCAVpwAgLt1BgC6dQYAo2UGAFqcAIBenACAYpwAgGacAICmbQYApW0GAGqcAICrLQYAqi0GAG6cAIBynACArx0GAK4dBgCtJQYArC0GAKhVBgCpWQYAqm0GAKthBgCsaQYArWkGAK6ZBgCvmQYAdpwAgHqcAIB+nACAgpwAgIacAICKnACAjpwAgJKcAIC4+QYAufkGALqNBgC7hQYAvJ0GAL2FBgC+hQYAv7UGALDpBgCx6QYAsvkGALP5BgC06QYAtd0GALbJBgC3yQYAs+UGAJacAICanACAnpwAgKKcAIC26QYAteEGAKacAIC7LQYAui0GAKqcAICunACAvxkGAL4tBgC9LQYAvC0GAIIVAACjoQYAgGEAAIFhAACmrQYAspwAgL6QAQClpQYAqmkGAKtpBgCEpAEAupwAgK5pBgCvXQYArGkGAK1pBgCohQIAqY0CAKqVAgCruQIArNUCAK3dAgCu1QIAr80CAIaAHACHZAMAvpwAgL5gAwDCnACAxpwAgMqcAIDOnACAuHUDALl9AwC6dQMAu8kDALzZAwC92QMAvskDAL/BAwCwvQIAsY0CALKFAgCzTQMAtFUDALVdAwC2VQMAt00DALMdAgDSnACAhAgDANacAIDanACAtl0CALVdAgDenACAu0kCALp5AgDinACA5pwAgL+ZAwC+kQMAvZkDALxRAgCwAAAAo1kCAOqcAIDunACAphkCAPKcAID2nACApRkCAKo9AgCrDQIA+pwAgP6cAICu1QMAr90DAKwVAgCt3QMAAp0AgAadAIAKnQCA76wGAA6dAIASnQCAFp0AgBqdAIC+6BwAHp0AgCKdAIAqnQCALp0AgOGABwAynQCA42AGAIBdAACBYQAAgmEAALN9AQA2nQCAtW0BALZlAQA6nQCAhiAdAIdYHQC6+QEAu/EBALzZAQC92QEAvrEBAL+xAQDvoAAAPp0AgEKdAIBGnQCASp0AgE6dAIBSnQCA71wBAIRsHADhzAYAVp0AgOMcBgDjSAAAWp0AgOEwAQBenQCAo/EBAGKdAICFABQAZp0AgGqdAICm6QEApeEBAG6dAICrfQEAqnUBAHKdAIB2nQCArz0BAK49AQCtVQEArFUBAKjtHQCpLR4AqjkeAKs5HgCsKR4ArSkeAK6dHgCvkR4AJp0AgHqdAIB+nQCAgp0AgIadAICC+QAAgfEAAID9AAC4qR4AuakeALpJHwC7SR8AvFkfAL1FHwC+TR8Av0UfALDxHgCx+R4AssEeALPBHgC0uR4AtbkeALatHgC3pR4AsBEfALERHwCyER8AsyUfALQlHwC1KR8Atl0fALdRHwC4cR8AuXkfALpBHwC7QR8AvJUAAL2dAAC+lQAAv40AAIqdAIC2nACAjp0AgJKdAICWnQCAmp0AgIb4AwCH0AAAqM0fAKnVHwCq0R8Aq70fAKytHwCtcR8ArnEfAK9xHwCzOR4Anp0AgKKdAICmnQCAqp0AgLaRHgC1RR4Arp0AgLu1HgC6tR4Asp0AgLadAIC/jR4AvoEeAL2RHgC8pR4Aup0AgKN9HgC+nQCAwp0AgKbVHgDGnQCAyp0AgKUBHgCq8R4Aq/EeAM6dAIDSnQCArsUeAK/JHgCs4R4ArdUeAKhVAQCpgQAAqoEAAKuBAACsgQAArYkAAK6xAACvsQAA1p0AgNqdAIDenQCA4p0AgOadAIDqnQCA7p0AgPKdAIC4ZQAAuW0AALplAAC7fQAAvGUAAL1tAAC+ZQAAv90DALChAACxrQAAsqUAALO5AAC0qQAAtZ0AALaVAAC3XQAA9p0AgIIdAACBHQAAgB0AAPqdAID+nQCAAp4AgL4UAgAKngCAhKgCAA6eAIASngCAFp4AgBqeAIAengCAjwAAALNJAwAingCAhugEAIesAgAmngCAtkkDALVJAwAqngCAuykDALolAwAungCAMp4AgL8ZAwC+LQMAvS0DALwxAwA2ngCAo40DADqeAIA+ngCApo0DAEKeAIBGngCApY0DAKrhAwCr7QMASp4AgE6eAICu6QMAr90DAKz1AwCt6QMAvoQDAFKeAIBWngCAWp4AgF6eAIBingCAZp4AgGqeAICAPQAAgQkAAIIZAABungCAcp4AgHqeAICENAMAfp4AgLMtAQCCngCAh8wCAIZMBQCGngCAti0BALUtAQCKngCAu0kBALp5AQCOngCAkp4AgL+9AQC+vQEAvbkBALxRAQDheB8Alp4AgOPQHwCangCAnp4AgOGUAQCingCA42gDAKaeAICqngCArp4AgO+IAwCyngCAtp4AgO+sHwC6ngCAvp4AgMKeAIDGngCAyp4AgM6eAIDSngCA1p4AgO9EHgDangCA4dweAN6eAIDjHB4A4p4AgOqeAIDungCA8p4AgIFpAACAZQAAo+UBAIJ9AACl5QEA9p4AgIQUBACm5QEAvigEAPqeAICrgQEAqrEBAK1xAQCsmQEAr3UBAK51AQCoIQYAqS0GAKolBgCrPQYArCUGAK0tBgCuXQYAr00GAHaeAIDmngCAhggDAIeMAwD+ngCAAp8AgAafAIAKnwCAuOkGALnpBgC6jQYAu4UGALydBgC9hQYAvo0GAL+FBgCwPQYAsQ0GALIFBgCz7QYAtPkGALX5BgC27QYAt+UGALDNBwCx1QcAstEHALPtBwC09QcAtf0HALbpBwC36QcAuN0HALklBwC6LQcAuyUHALw9BwC9JQcAvi0HAL8lBwAOnwCAEp8AgAaeAIAWnwCAGp8AgB6fAIAinwCAJp8AgKgVBgCpGQYAqu0HAKv9BwCs7QcArd0HAK7VBwCvuQcAswUGACqfAIAunwCAMp8AgDafAIC2PQYAtQUGADqfAIC7cQYAumkGAD6fAIBCnwCAv1kGAL5RBgC9WQYAvGUGAEafAICjQQYASp8AgE6fAICmeQYAUp8AgIS0AQClQQYAqi0GAKs1BgC+gAEAWp8AgK4VBgCvHQYArCEGAK0dBgCoNQYAqT0GAKo1BgCrWQYArHUGAK2lAQCurQEAr6UBAIDpAACB6QAAgv0AAL8kAQCGMA8Ah+QAAF6fAIBinwCAuMUAALnNAAC6xQAAu90AALzNAAC9/QAAvvUAAL+dAACw3QEAsSUBALItAQCzIQEAtCEBALUhAQC2IQEAtyEBALvBAgC6OQIAZp8AgGqfAIC/xQIAvsUCAL3VAgC82QIAs50FAG6fAIBynwCAdp8AgIwAAAC2BQIAtd0FAHqfAICqfQIAq4UCAH6fAICCnwCAroECAK+BAgCsnQIArZECAIafAICj2QUAip8AgI6fAICmQQIAkp8AgJafAIClmQUAgpFqAIORagCanwCAnp8AgIa5FgCH6RcAhBEWAIWZFgCKoRIAi6ESAKKfAICmnwCAjpEeAI9ZHgCMmRMAjREeAJJxGgCT5RoAqp8AgO/oJACW8QYAlwUGAJTlGgCVGQYAmikCAJvFAgCunwCAsp8AgLafAIDhKBsAnN0CAOMgDwCfIQcAnsEHAJ01GwCcLRsAm6EbAJr5HwCZOR8AmLEfAJcBEgCWIRMAlSkTAJRRFgCTGRcAkjEXAJGxFwCQKWsAj1FrAOOsBwCEBA0A4RwHAIANAACBNQAAgj0AALqfAIC+nwCAwp8AgL4gDQDKnwCAzp8AgO9MBwCGWAwAh2ANANKfAIDWnwCA2p8AgN6fAICEXA8A4p8AgO8IAADvhAYA4ZABAOGwBgDj4AAA42QGAOafAIDqnwCA7p8AgPKfAID2nwCA+p8AgL4ADwCEQA4A/p8AgAKgAIAGoACACqAAgA6gAIASoACAFqAAgBqgAICj1QMAotUDAKExAwCgLQcAVp8AgMafAIAeoACAIqAAgCagAICCmQAAgZEAAICZAACoTQ0AqZ0NAKqVDQCrJQ4ArD0OAK0RDgCuEQ4ArxEOALB9DgCxDQ4AsgUOALMtDgC0OQ4AtTkOALYtDgC3JQ4AuOkOALnpDgC6wQ4Au8EOALy5DgC9nQ4AvpUOAL+NDgCzPQ0AKqAAgC6gAIAyoACANqAAgLaxDgC1lQ4AOqAAgLvpDgC6mQ4AhogAAIfkAAC/3Q4Avt0OAL3ZDgC88Q4APqAAgKN5DQC+hAEAhIAGAKb1DgBCoACARqAAgKXRDgCq3Q4Aq60OAEqgAIBOoACArpkOAK+ZDgCstQ4ArZ0OALIFNQCzGTQAsG0wALENNQBSoACAVqAAgLQBKAC1PSkAWqAAgF6gAIBioACAZqAAgGqgAIBuoACAcqAAgHagAICiRQEAo9UBAHqgAIChTQEAps0FAKcBOACkAQQApX0FAKoBPACrRT0AqEk5AKnlOQCudTEAr30xAKxdPQCtATAAqO0OAKn1DgCqCQ4AqwkOAKwZDgCtGQ4Arg0OAK8tDgB+oACAgqAAgIagAICKoACAjqAAgJKgAICWoACAmqAAgLgdDgC5JQ4Aui0OALslDgC8PQ4Avd0BAL7VAQC/zQEAsFUOALFdDgCyVQ4Asy0OALQ1DgC1JQ4Ati0OALclDgCzgQ0AnqAAgKKgAICqoACArqAAgLaZDQC1kQ0AvlQEALuZDQC6kQ0AhogEAIe8AwC/4Q0AvvENAL35DQC8gQ0AgkkAAKPFDQCA9QMAgUkAAKbdDQCyoACAtqAAgKXVDQCq1Q0Aq90NALqgAIC+oACArrUNAK+lDQCsxQ0Arb0NAKgdAgCpRQIAql0CAKtVAgCseQIArXkCAK6JAwCviQMAwqAAgMagAIDKoACAzqAAgIT8BQDSoACA1qAAgNqgAIC4iQMAuWUDALptAwC7ZQMAvH0DAL1lAwC+bQMAv2UDALDBAwCxwQMAssEDALPBAwC0wQMAtcEDALbBAwC3wQMA3qAAgOKgAIDmoACA6qAAgO6gAIDhpAEA8qAAgOPADgC+aAQA9qAAgPqgAIDvHAEA/qAAgAKhAIAGoQCACqEAgLOVAwAOoQCAEqEAgBqhAIAeoQCAtrkDALWxAwAioQCAu0UCALpFAgCGqAQAh6QFAL9FAgC+RQIAvVUCALxVAgDh4A4A4SwMAOMIDgDj1A4AgK0AAIHRAACC0QAAJqEAgCqhAIAuoQCAMqEAgDahAIA6oQCAPqEAgO+IDgDvLA4AoxUDAEKhAICFxCsARqEAgEqhAICmOQMApTEDAE6hAICrxQIAqsUCAFKhAIBWoQCAr8UCAK7FAgCt1QIArNUCAKgNBgCpFQYAql0GAKtVBgCseQYArXkGAK65BgCvuQYAFqEAgFqhAIBeoQCAYqEAgGahAIBqoQCAbqEAgHKhAIC4TQcAuVUHALpRBwC7aQcAvHkHAL1lBwC+bQcAv2UHALDJBgCxyQYAst0GALPVBgC0zQYAtXUHALZ9BwC3dQcAs9UGAHahAIB6oQCAfqEAgIKhAIC2+QYAtfEGAIahAIC7DQYAug0GAIYIAACHLAAAv7EHAL4JBgC9AQYAvAkGAIJRAACjkQYAgEEAAIFBAACmvQYAiqEAgI6hAICltQYAqkkGAKtJBgCSoQCAlqEAgK5NBgCv9QcArE0GAK1FBgCwsQYAsbEGALLNBgCzwQYAtMEGALXJBgC28QYAt/EGALgFAQC5DQEAugUBALsdAQC8BQEAvQ0BAL4FAQC/uQEAmqEAgJ6hAICioQCApqEAgKqhAICuoQCApqAAgLKhAICoLQYAqTUGAKo1BgCr8QYArNEGAK3RBgCu0QYAr9EGALPdBgC2oQCAuqEAgL6hAIDCoQCAtjEGALU5BgDGoQCAuxUGALoVBgDKoQCAzqEAgL9tBgC+ZQYAvXUGALx5BgDSoQCAo5kGANahAIDaoQCApnUGAN6hAIDioQCApX0GAKpRBgCrUQYA5qEAgOqhAICuIQYArykGAKw9BgCtMQYAqNUCAKndAgCq4QIAq+ECAKxRAwCtUQMArlEDAK9RAwDuoQCA8qEAgL7sAwD6oQCA/qEAgAKiAIAGogCACqIAgLjpAwC56QMAuokDALuFAwC8nQMAvYEDAL6BAwC/tQMAsDEDALExAwCyNQMAs+kDALT5AwC1+QMAtukDALfhAwCAbQMAgaUAAIKtAACzZQIADqIAgLXVAwC23QMAEqIAgITgAgAWogCAuvkDALv5AwC87QMAvTEDAL4xAwC/MQMAh+wDAIZkPACyAAAAGqIAgB6iAIDjCAQAIqIAgOHsBgAmogCA7wAGACqiAIAuogCAMqIAgDaiAIA6ogCAPqIAgEKiAIBGogCASqIAgE6iAIDjoAMAUqIAgOGoAQBWogCA7/ADAIIdAACBHQAAgB0AAFqiAIBeogCAYqIAgGqiAIC+TD0AbqIAgKOhAwC+QDwApRECAHKiAIB2ogCAphkCAIRsAgB6ogCAqz0CAKo9AgCt9QIArCkCAK/1AgCu9QIAhkA8AIe0PQB+ogCAgqIAgIaiAICKogCAjqIAgO9EBgCSogCA4dQGAJaiAIDjDAcAmqIAgJ6iAICiogCApqIAgLP1AQCqogCArqIAgLKiAIC2ogCAtkUBALXlAQC6ogCAuzEBALopAQC+ogCAwqIAgL8dAQC+HQEAvRkBALwlAQCoLT4AqTU+AKo9PgCrNT4ArC0+AK2FPgCuhT4Ar7k+AGaiAIDGogCAyqIAgM6iAICAGQAAgRkAAIIFAADSogCAuLk+ALm5PgC6ST8Au0k/ALxZPwC9WT8Avk0/AL9BPwCwrT4AsbU+ALKxPgCzjT4AtJk+ALWZPgC2iT4At4k+AKO1PgCEjAIA1qIAgNqiAIDeogCApgU+AKWlPgDiogCAq3E+AKppPgCGCAAAh2gDAK9dPgCuXT4ArVk+AKxlPgDmogCAs5E/AOqiAIDuogCAtlk/APKiAID2ogCAtbk/ALp1PwC7fT8A+qIAgP6iAIC+QT8Av0E/ALxZPwC9VT8AsJU+ALGdPgCyqT4As6U+ALShPgC1oT4AtqE+ALehPgC45T4Aue0+ALrlPgC7/T4AvO0+AL3dPgC+1T4AvxkBAAKjAIAGowCACqMAgA6jAIASowCA9qEAgBajAIAaowCAqF0+AKkhPgCqPT4AqzU+AKwVPgCt/T4ArvU+AK/tPgCj1T4AHqMAgCKjAIAmowCAKqMAgKYdPgCl/T4ALqMAgKs5PgCqMT4AMqMAgDajAICvBT4ArgU+AK0RPgCsHT4AgREAAIANAAA6owCAghkAAD6jAIBCowCAhJQBAL4QAACGQAcAhwABAEqjAIBOowCAUqMAgFajAIBaowCAXqMAgKiNAgCplQIAqpUCAKvNAgCs2QIArdkCAK7NAgCvxQIAYqMAgGajAIBqowCAbqMAgIwAAAByowCAdqMAgHqjAIC4HQMAucEDALrBAwC7wQMAvMEDAL3JAwC+8QMAv/EDALCJAgCxiQIAsikDALMpAwC0OQMAtTkDALYpAwC3JQMAsx0CAH6jAICCowCAhqMAgIqjAIC2WQIAtVECAI6jAIC7TQIAuk0CAJKjAICWowCAv/0DAL79AwC9/QMAvP0DAJqjAICeowCAoqMAgKajAIDhDD4AqqMAgOOoPwCuowCAgT0AAIAxAADvUD8Agh0AALKjAIC++AQAhhgFAIdMAwCEDAIA48wAALqjAIDhvAEAvqMAgMKjAIDGowCAyqMAgM6jAICELAUA0qMAgNajAIDaowCA7xAAAN6jAIDiowCAo90DAOajAIDqowCA7qMAgPKjAICmmQMApZEDAPajAICrjQMAqo0DAPqjAID+owCArz0CAK49AgCtPQIArD0CAAKkAIAGpACACqQAgA6kAIASpACAFqQAgBqkAIDvKD4AHqQAgOE8PgAipACA4zgBAIApAACBFQAAghEAACqkAICzMQIAvsgEAITABAAupACAMqQAgLYpAgC1IQIANqQAgLvNAQC6zQEAOqQAgD6kAIC/dQEAvskBAL3BAQC8yQEAqOkFAKnpBQCq+QUAq/kFAKzpBQCt6QUArjkGAK85BgC2owCAJqQAgIaIAACHQAMAQqQAgEakAIBKpACATqQAgLjRBgC52QYAuuEGALvhBgC8kQYAvZEGAL6RBgC/kQYAsEkGALFJBgCyXQYAs1UGALRNBgC18QYAtvEGALfxBgCjcQUAUqQAgFakAIBapACAXqQAgKZpBQClYQUAYqQAgKuNBgCqjQYAZqQAgGqkAICvNQYArokGAK2BBgCsiQYAbqQAgLPRBwBypACAdqQAgLbxBwB6pACAfqQAgLXBBwC60QcAu90HAIKkAICGpACAvrkHAL+5BwC8xQcAvbkHALhpBgC5aQYAuokGALuJBgC8mQYAvZkGAL6JBgC/iQYAsBEGALEdBgCyFQYAs2kGALR5BgC1eQYAtmkGALdhBgCoSQYAqVUGAKpdBgCrVQYArE0GAK11BgCucQYAr3EGAEajAICCHQAAgR0AAIAdAACKpACAjqQAgJKkAIC+cAEAo5UGAJqkAICGKAAAh0gBAJ6kAICmtQYApYUGAKKkAICrmQYAqpUGAKakAICqpACAr/0GAK79BgCt/QYArIEGAK6kAICzFQYAsqQAgLakAIC2PQYAuqQAgL6kAIC1NQYAutkBALvZAQDCpACAxqQAgL59AQC/ZQEAvH0BAL11AQCovQUAqckFAKrZBQCr0QUArPkFAK35BQCuKQIArykCAMqkAIDOpACA0qQAgNakAICMAAAA2qQAgN6kAIDipACAuO0CALmFAgC6gQIAu4ECALyFAgC9jQIAvrECAL+xAgCwWQIAsVkCALLtAgCz5QIAtP0CALXlAgC25QIAt9UCAKNRBQDmpACA6qQAgO6kAIDypACApnkFAKVxBQD2pACAq50CAKqdAgD6pACA/qQAgK8hAgCuOQIArTECAKw5AgCBbQAAgG0AAAKlAICCBQAAvlwMAAqlAIAOpQCA79AGAITsAwDhHAUAEqUAgOP8BwAWpQCAGqUAgIbYDACHvAwAqIUCAKmVAgCqlQIAq6UCAKy9AgCt1QIArtECAK/RAgAepQCAIqUAgCalAIAqpQCALqUAgDKlAIA2pQCAOqUAgLh1AQC5fQEAunUBALvJAQC82QEAvdkBAL7JAQC/wQEAsLUCALG9AgCygQIAs4ECALRRAQC1UQEAtlEBALdRAQA+pQCAhAQNAEKlAIBGpQCAvhwMAEqlAIDvHAAA76AGAOGQAQDhRAcA43AGAOOYBgBOpQCAUqUAgFalAIBapQCAs10CAF6lAIBipQCAZqUAgGqlAIC2FQIAtXUCAG6lAIC7OQIAujECAHKlAIB6pQCAv9UBAL7VAQC9FQIAvBUCAKOdDQAGpQCAdqUAgH6lAICCpQCAptUNAKW1DQCGpQCAq/kNAKrxDQCGCAMAh2ADAK8VDgCuFQ4ArdUNAKzVDQCAkQ8AgZkPAIKhDwCzpQ4AiqUAgLWhDgC2eQ8AjqUAgJKlAICWpQCAukUPALtdDwC8RQ8AvU0PAL5FDwC//Q8AqFUOAKldDgCqYQ4Aq30OAKxlDgCttQ8Arr0PAK+1DwCapQCAnqUAgKKlAICmpQCAqqUAgK6lAICypQCAtqUAgLhVDwC5dQ8Aun0PALt1DwC8bQ8AvREPAL4RDwC/EQ8AsM0PALHVDwCy3Q8As9UPALTNDwC1dQ8AtnEPALdxDwCj6Q8AuqUAgL6lAIDCpQCAxqUAgKY1DgCl7Q8AyqUAgKsRDgCqCQ4AzqUAgNKlAICvsQ4ArgkOAK0BDgCsCQ4A1qUAgIIdAACBHQAAgB0AANqlAIDepQCA4qUAgL6UAQCErAEA5qUAgIfgAQCGzAAA6qUAgO6lAIDypQCAlqQAgKhtDgCpiQEAqpkBAKuRAQCswQEArckBAK75AQCv+QEAhKAAAPalAID6pQCA/qUAgAKmAIAGpgCACqYAgA6mAIC4xQAAuc0AALrFAAC73QAAvM0AAL39AAC+9QAAv50AALBBAQCxQQEAskEBALNBAQC0QQEAtUEBALZBAQC3QQEAsxECABKmAIAWpgCAGqYAgB6mAIC2SQIAtUkCACKmAIC7hQIAuoUCACamAIAqpgCAv4UCAL6FAgC9lQIAvJUCAIU8GgCjVQIALqYAgDKmAICmDQIANqYAgDqmAIClDQIAqsECAKvBAgA+pgCAQqYAgK7BAgCvwQIArNECAK3RAgCCGQAARqYAgIAZAACBGQAASqYAgE6mAIBSpgCAWqYAgL4ABABepgCAYqYAgGamAIBqpgCAbqYAgHKmAIB2pgCA7+gOAHqmAICG6AQAh1ADAH6mAICCpgCA74ACAIamAIDhlAEAiqYAgONYAQCOpgCA4wAOAJKmAIDhaA0AlqYAgKhxAgCpcQIAqnECAKupAgCsuQIArbkCAK6pAgCvqQIAhKwFAJqmAICepgCAoqYAgKamAICqpgCArqYAgLKmAIC4bQEAuQ0BALoFAQC7GQEAvAkBAL09AQC+NQEAv9kBALDZAgCx2QIAsm0BALNlAQC0fQEAtWUBALZlAQC3VQEA4WAPAOP0AADjHA4A4bwBALamAICCOQAAgTEAAIA9AAC6pgCAvigEAL6mAIDCpgCAvjwHAO8QAADv0A4AyqYAgIbgBACHyAQAzqYAgLO1AgDSpgCAtX0CALZ1AgDWpgCA2qYAgN6mAIC6UQIAu1ECALz1AQC9/QEAvvUBAL/tAQBWpgCAxqYAgKqxBQCrsQUArBUGAK0dBgCuFQYArw0GAOKmAIDmpgCA6qYAgKNVBQDupgCApZ0FAKaVBQDypgCAs+kGAPamAID6pgCA/qYAgAKnAIC24QYAtekGAAanAIC7sQYAuqEGAAqnAIAOpwCAv50GAL6RBgC9pQYAvKkGAKgdBgCpIQYAqiEGAKshBgCsIQYArSEGAK4hBgCvIQYAEqcAgBanAIAapwCAHqcAgCKnAIAmpwCAKqcAgC6nAIC45QcAue0HALrlBwC7/QcAvOUHAL3tBwC+5QcAv00HALAlBgCxNQYAsj0GALMxBgC0FQYAtRkGALYNBgC3AQYAo6kHAIIVAACBtQEAgLUBADKnAICmoQcApakHADanAICr8QcAquEHAISgAgA6pwCAr90HAK7RBwCt5QcArOkHAD6nAICzlQYAhugAAIcYAQC2tQYAQqcAgEanAIC1vQYAukkBALtVAQBKpwCATqcAgL45AQC/OQEAvEUBAL05AQCoPQYAqU0GAKpZBgCrUQYArHEGAK1xBgCuuQEAr7kBAISsAQBSpwCAVqcAgFqnAIBepwCAYqcAgGanAIBqpwCAuKkBALmpAQC6aQEAu2kBALx5AQC9eQEAvmkBAL9pAQCwyQEAsdUBALLVAQCzqQEAtLkBALW5AQC2qQEAt6EBAKPRBQBupwCAcqcAgHanAIB6pwCApvEFAKX5BQB+pwCAqxECAKoNAgCCpwCAhqcAgK99AgCufQIArX0CAKwBAgCKpwCAjqcAgJKnAICWpwCAgTEAAIANAACapwCAgjkAAJ6nAICipwCAviQDAKqnAICupwCAsqcAgIbYHACHTAMAtqcAgLqnAIC+pwCAhMAcAOMgAQDCpwCA4cgBAManAIDvMAIAyqcAgM6nAIDSpwCA1qcAgNqnAIDepwCA4qcAgLOVAwDmpwCA6qcAgO6nAIDypwCAtrkDALWxAwD2pwCAu1EDALpJAwD6pwCA/qcAgL/1AAC+SQMAvUEDALxJAwCoLQIAqUUCAKpdAgCrVQIArHkCAK15AgCuvQIAr7UCAL5oHQACqACABqgAgAqoAICAHQAAgQkAAIKpAAAOqACAuFEBALlZAQC6YQEAu2EBALwRAQC9EQEAvhEBAL8RAQCwzQIAsdUCALLdAgCz1QIAtM0CALVxAQC2cQEAt3EBAOFYBgDhVAcA47AAAOO8BgASqACAGqgAgIYYHACHVB0AHqgAgCKoAIAmqACAKqgAgL74HAAuqACA7/AGAO/gBgCjlQIAMqgAgDaoAIA6qACAPqgAgKa5AgClsQIAQqgAgKtRAgCqSQIARqgAgEqoAICv9QEArkkCAK1BAgCsSQIAqG0eAKl1HgCqfR4Aq40eAKyVHgCtnR4Aro0eAK+BHgAWqACATqgAgFKoAIBWqACAWqgAgF6oAIBiqACAZqgAgLiJHgC5iR4AupkeALuRHgC8uR4AvbkeAL59HwC/dR8AsMUeALHNHgCyxR4As90eALTFHgC1zR4AtsUeALe5HgCz9R4AaqgAgG6oAIByqACAdqgAgLYdHgC1HR4AeqgAgLsJHgC6AR4AfqgAgIKoAIC/CR4AvgEeAL0JHgC8ER4Agm0AAKOxHgCAVQAAgWUAAKZZHgCEmAMAv9ABAKVZHgCqRR4Aq00eAIYABACHmAEArkUeAK9NHgCsVR4ArU0eAIqoAICOqACAhCQAAJKoAICWqACAmqgAgKanAICGqACAqLUeAKmFHgCqjR4Aq4UeAKydHgCtgR4Arv0eAK/1HgCwjR4AsZUeALKVHgCzpR4AtL0eALVxAQC2cQEAt3EBALhRAQC5UQEAulEBALtRAQC89QEAvf0BAL71AQC/7QEAsyUeAL4IBwCeqACAoqgAgKaoAIC2IR4AtTUeAKqoAIC7cR4AumkeAK6oAICyqACAv5UBAL5ZHgC9UR4AvGEeALaoAICjYR4AuqgAgL6oAICmZR4AwqgAgMaoAIClcR4Aqi0eAKs1HgDKqACAzqgAgK4dHgCv0QEArCUeAK0VHgDhVBoA0qgAgONcCgDWqACA2qgAgN6oAIDiqACA5qgAgOqoAIC+qAUA7qgAgPKoAICPMSoA+qgAgO/E+wD+qACAk2EuAJIdLwCR2SoAkEkqAJfZEgCWdRIAlQ0TAJTBLgCbHRsAmkEWAJlJFgCYDRcAn3EeAJ4RGwCdcRoAnHkaAKOhAgCinQMAoZUfAKCJHgDjiAEA4wgeAOFoAADh/B4A79wBAO98HwC1if4AtAH8ALMB+gCylfoAsQH4ALAR9gCv4fYArgH0AK0l8gCs7fIAqwHwAKrpDwCp1Q4AqN0OAKcBDACmyQoApe0KAKQBCACj4QYAovEGAKHlAwACqQCAggErAIMBKwAGqQCACqkAgIYxLwCHiS8AhIkrAIVFLgCKdRIAiwUTAIYIBQCHbAUAjhEXAI8RFwCMsRMAjV0WAJI9GgCTQRsAhMgFAIQABwCWUR8Al1EfAJRRGwCVORoAmn0eAJt9AgAOqQCAEqkAgIFZAQCAVQEAnFkDAIJRAQC+yAcAFqkAgBqpAIAeqQCAIqkAgCapAIAqqQCA79QeAC6pAIDhJB4AMqkAgONoAQA2qQCAOqkAgD6pAIBCqQCAu2kCALpZAgBGqQCASqkAgL8dAgC+HQIAvRkCALxxAgCz7QIATqkAgFKpAIBWqQCAWqkAgLZ9AgC17QIAXqkAgKMNBQD2qACAYqkAgGqpAIBmqQCApp0FAKUNBQBuqQCAq4kFAKq5BQCGCAMAh3wDAK/9BQCu/QUArfkFAKyRBQCAsQcAgbkHAIJBAACzsQYAcqkAgLVZBwC2MQcAdqkAgHqpAIB+qQCAuuEHALvhBwC84QcAveEHAL7hBwC/3QcAqLUGAKm5BgCqdQYAq4UHAKydBwCt/QcArvUHAK8ZBwCCqQCAhqkAgIqpAICOqQCAkqkAgJapAICaqQCAnqkAgLh1BwC5fQcAunUHALsFBwC8HQcAvTEHAL4xBwC/MQcAsGkHALFpBwCyeQcAs3kHALRpBwC1VQcAtlEHALdNBwCj/QcAoqkAgKapAICqqQCArqkAgKZ9BgClFQYAsqkAgKutBgCqrQYAtqkAgLqpAICvkQYArq0GAK2tBgCsrQYAvqkAgMKpAIDGqQCAyqkAgIAdAACBCQAAgjkAAM6pAIDSqQCA2qkAgIbIAACHpAEA3qkAgOKpAIDmqQCA6qkAgKiNAQCpmQEAqtkBAKvRAQCs8QEArfEBAK45AQCvOQEAhKAAAO6pAIDyqQCA9qkAgPqpAID+qQCAAqoAgAaqAIC4zQAAudUAALrVAAC75QAAvP0AAL2VAAC+nQAAv5UAALBJAQCxSQEAslkBALNZAQC0SQEAtUkBALb9AAC39QAAugUEALsJBAC44QcAueEHAL4JBAC/CQQAvAkEAL0JBACyjQcAs+UHALC1BwCxhQcAtuUHALftBwC08QcAtfEHAKpNBwCrVQcAqEkHAKlJBwCu3QcAr8UHAKxNBwCt1QcACqoAgA6qAIASqgCAFqoAgBqqAIAeqgCAIqoAgCaqAICz0QIAKqoAgC6qAIC+AAwAMqoAgLbxAgC1+QIANqoAgLsNAgC6DQIAOqoAgD6qAIC/DQIAvg0CAL0NAgC8DQIAghUAAKOVAgCAYQAAgWEAAKa1AgBCqgCASqoAgKW9AgCqSQIAq0kCAIbIDACHrAwArkkCAK9JAgCsSQIArUkCAKhlAgCpdQIAqn0CAKt1AgCsbQIArbECAK6xAgCvsQIAhKANAE6qAIBSqgCAVqoAgFqqAIBeqgCAYqoAgGaqAIC4MQEAuTEBALoxAQC7MQEAvNUBAL3dAQC+yQEAv8EBALDRAgCx0QIAstECALPRAgC0EQEAtREBALYRAQC3EQEA4bAGAGqqAIDj0AYAhEAPAG6qAIDhpAEAcqoAgOPABgB2qgCAeqoAgH6qAIDv1AYA7AAAAIKqAIDvZAcAhqoAgIqqAICOqgCAkqoAgLO5AgCWqgCAtakCALZ9AgCaqgCAnqoAgKKqAIC6WQIAu1kCALxJAgC9SQIAvpkBAL+ZAQCjdQ0ARqoAgKaqAICqqgCArqoAgKaxDQClZQ0AsqoAgKuVDQCqlQ0AvqQDALaqAICvVQ4ArlUOAK2FDQCshQ0AgE0AAIFVAACCVQAAs2UPALqqAIC1ZQ8Atm0PAL6qAICGQAMAhxQDALrtDwC7/Q8AvOkPAL3VDwC+3Q8Av9UPAKhZDgCpoQ8AqqEPAKuhDwCsoQ8AraEPAK6hDwCvoQ8AwqoAgMaqAIDKqgCAzqoAgNKqAIDWqgCA2qoAgN6qAIC4AQ8AuQEPALoBDwC7HQ8AvA0PAL01DwC+PQ8Av9UAALBlDwCxdQ8AsnEPALNNDwC0VQ8AtV0PALZNDwC3QQ8AoykOAOKqAIDmqgCA6qoAgO6qAICmIQ4ApSkOAPKqAICrsQ4AqqEOAPaqAID6qgCAr5kOAK6RDgCtmQ4ArKUOAP6qAIACqwCABqsAgAqrAIDvJA0ADqsAgBKrAIAWqwCA49AOABqrAIDhGA4AHqsAgIAVAACBGQAAggUAACKrAICo0QEAqdkBAKopAQCrKQEArDkBAK05AQCuKQEArykBAL5oAQAqqwCAhsgBAIesAAAuqwCAMqsAgDarAIA6qwCAuO0AALmFAAC6jQAAu4UAALydAAC9gQAAvoEAAL+BAACwWQEAsVkBALLtAACz5QAAtP0AALXlAAC25QAAt9UAALOhAgA+qwCAQqsAgEarAIBKqwCAtrkCALWxAgBOqwCAu50CALqdAgBSqwCAVqsAgL8hAwC+OQMAvTEDALw5AwCF+PUAo+UCAFqrAIBeqwCApv0CAGKrAIBmqwCApfUCAKrZAgCr2QIAaqsAgG6rAICufQMAr2UDAKx9AwCtdQMAuOkAALnpAAC6aQAAu2kAALx5AAC9ZQAAvm0AAL9lAACwsQAAsbkAALKBAACzgQAAtPkAALX5AAC27QAAt+UAAKhlAwCpdQMAqn0DAKt1AwCsbQMArdEAAK7RAACv0QAAcqsAgHarAIB6qwCA1qkAgH6rAICCqwCAhqsAgIqrAICA/QEAgQkAAIIZAACOqwCAkqsAgL5EAgCaqwCAnqsAgISsAgCiqwCAh/gCAIasBQCmqwCAqqsAgK6rAICyqwCAs/UCALarAIC6qwCAvqsAgMKrAIC2UQEAteUCAMarAIC7fQEAunUBAMqrAIDOqwCAvz0BAL49AQC9VQEAvFUBAOFwDwDSqwCA47gOAITABQDvyAAA1qsAgNqrAIDeqwCA4zwOAOKrAIDh0AEA5qsAgIR0BwDqqwCA72gBAO6rAIDyqwCApXkCAKbNAQD2qwCAgCEAAIEhAACC3QcAo2kCAKzJAQCtyQEArqEBAK+hAQD6qwCA/qsAgKrpAQCr4QEAlqsAgAKsAIC+QAIABqwAgIYwAwCHMAMACqwAgA6sAICoOQcAqTkHAKoNBwCrHQcArAUHAK0NBwCuBQcAr3kHALAJBwCxCQcAshkHALMRBwC0OQcAtTkHALbdBwC3yQcAuPkHALn5BwC6zQcAu8EHALzFBwC9yQcAvrkHAL+xBwCzpQcAEqwAgBasAIAarACAHqwAgLatBwC1rQcAIqwAgLvtBwC67QcAJqwAgCqsAIC/3QcAvt0HAL3lBwC87QcALqwAgKPhBwAyrACANqwAgKbpBwA6rACAPqwAgKXpBwCqqQcAq6kHAEKsAIBGrACArpkHAK+ZBwCsqQcAraEHAEqsAIBOrACAUqwAgFasAIBarACAXqwAgGKsAIBmrACAgREAAIANAABqrACAghkAAG6sAIByrACAvuQBAHasAICG4AAAhxgBAHqsAIB+rACAgqwAgIasAICKrACA77AEAI6sAIDh1AYAkqwAgONcBACWrACAmqwAgJ6sAICirACAqJkBAKmZAQCqDQEAqwUBAKwdAQCtBQEArgUBAK81AQCEiAEApqwAgKqsAICurACAsqwAgLasAIC6rACAvqwAgLjBAAC5wQAAusEAALvBAAC8wQAAvcEAAL7BAAC/wQAAsE0BALElAQCyIQEAsyEBALQlAQC1LQEAthEBALcRAQDCrACAxqwAgLONAgDKrACAtZ0CAM6sAIDSrACAto0CANasAIDarACAu+kCALqBAgC9/QIAvP0CAL/hAgC+6QIA3qwAgKbVAgClxQIAvggDAKPVAgCCLQAAgRkAAIB5AACvuQIArrECAK2lAgCspQIAq7ECAKrZAgDirACA6qwAgO80AgDurACAhxgDAIYs/ADyrACA9qwAgPqsAID+rACAAq0AgAatAIAKrQCADq0AgOMAAQASrQCA4eABABatAIC6tQMAu70DABqtAIAerQCAvnkDAL95AwC8pQMAvXkDACarAICztQMAIq0AgCatAIC2kQMAKq0AgC6tAIC1pQMAqEkCAKlJAgCqWQIAq1kCAKxJAgCtdQIArnECAK9tAgC+aP0AvqT/ADKtAIA2rQCAOq0AgD6tAIBCrQCARq0AgLj5AgC5+QIAukkBALtJAQC8XQEAvUEBAL5BAQC/fQEAsBUCALEdAgCyFQIAs8kCALTZAgC12QIAtskCALfJAgDjIAYA4bAGAOGAAQDjEAYAgA0AAIE1AACCPQAASq0AgE6tAIBSrQCAWq0AgF6tAIDvcAAAYq0AgGatAIDvTAEAhIz9AGqtAICjmQIAbq0AgKWJAgByrQCAdq0AgKa9AgCGwPwAh+T8AKuRAgCqmQIArVUCAKyJAgCvVQIArlUCAKh9/gCpgf4Aqpn+AKuZ/gCsif4ArYn+AK65/gCvuf4AVq0AgHqtAIB+rQCAgq0AgIatAICKrQCAjq0AgJKtAIC4tf4Aub3+ALph/wC7Yf8AvGH/AL1h/wC+Yf8Av2H/ALDJ/gCxyf4Ast3+ALPR/gC0uf4Atbn+ALaR/gC3kf4AsxH+AJatAICarQCAnq0AgKKtAIC2Cf4AtQH+AKatAIC7Df4Aug3+AKqtAICurQCAv33+AL59/gC9Bf4AvAn+ALKtAICjVf4Atq0AgLqtAICmTf4Avq0AgMKtAIClRf4Aqkn+AKtJ/gCEKAMAxq0AgK45/gCvOf4ArE3+AK1B/gCAzQEAgdEBAILRAQCzuf4Ayq0AgLXR/gC21f4Azq0AgIZgAQCHYAEAug0BALsFAQC8HQEAvQUBAL4NAQC/BQEA0q0AgNatAIDarQCA3q0AgOKtAIDhwP0A5q0AgOOM/ADqrQCA7q0AgPKtAIDvtPwA9q0AgPqtAID+rQCAAq4AgKgp/gCpKf4Aqj3+AKs1/gCsVf4ArVn+AK5N/gCvRf4ABq4AgAquAIAOrgCAEq4AgBauAIAargCAHq4AgCKuAIC4SQEAuUkBALpZAQC7UQEAvHkBAL15AQC+GQEAvxUBALDFAQCxzQEAssUBALPdAQC0xQEAtc0BALbFAQC3eQEAJq4AgCquAIAurgCAo7n9ADKuAICl0f0AptX9AITQAwBBrgCAvuACAKoNAgCrBQIArB0CAK0FAgCuDQIArwUCAIFJAACAQQAAowkDAIJdAAClGQMARa4AgEmuAICmEQMAhsAEAIfkAwCrDQMAqg0DAK0BAwCsHQMArwEDAK4JAwCw4QMAseEDALLhAwCz/QMAtOUDALXtAwC25QMAtz0DALgFAwC5DQMAugUDALsdAwC8BQMAvQ0DAL4FAwC/vQAATa4AgFGuAIBVrgCAWa4AgOasAIBdrgCAYa4AgGWuAICo8QMAqfkDAKqpAwCrqQMArLkDAK25AwCuqQMAr6UDALNBAgBprgCAba4AgHGuAIB1rgCAtlkCALVRAgB5rgCAu0UCALpFAgB9rgCAga4AgL9JAgC+QQIAvUkCALxVAgCFrgCAia4AgI2uAICRrgCA74wDAJWuAICZrgCAna4AgONsAwChrgCA4VAAAKWuAICprgCAvngFALGuAICEcAIAgOUAAIHpAACC+QAAta4AgIawBACHVAUAua4AgO9A/gC9rgCA4Vz+AMGuAIDjVAEAxa4AgMmuAIDNrgCA0a4AgLOZAQDVrgCA2a4AgN2uAIDhrgCAth0BALUdAQDlrgCAuz0BALo9AQDprgCA7a4AgL/hAAC++QAAvfEAALz5AACoIQYAqVEGAKpRBgCrzQYArNUGAK3dBgCu1QYAr8kGAK2uAIDxrgCA9a4AgPmuAID9rgCAAa8AgAWvAIAJrwCAuG0HALkFBwC6DQcAuwUHALwdBwC9AQcAvgEHAL8BBwCwuQYAsbkGALJtBwCzZQcAtH0HALVlBwC2ZQcAt1UHAKPZBgANrwCAEa8AgBWvAIAZrwCApl0GAKVdBgCEnAIAq30GAKp9BgC+JAMAHa8AgK+hBwCuuQcArbEHAKy5BwCASQAAgUkAAIJZAACzVQcAIa8AgLV9BwC2aQcAJa8AgIZAAACHVAMAulUHALspBwC8OQcAvTkHAL4pBwC/IQcAo5kGACmvAIAtrwCAMa8AgDWvAICmpQYApbEGADmvAICr5QYAqpkGAD2vAIBBrwCAr+0GAK7lBgCt9QYArPUGAOE4BQBFrwCA4yQEAEmvAIBNrwCAUa8AgFWvAIBZrwCAXa8AgGGvAIBlrwCAaa8AgG2vAIBxrwCA7/QEAHWvAICo+QYAqQkGAKoRBgCrLQYArDkGAK0lBgCuLQYAryUGAHmvAIB9rwCAga8AgIWvAICAGQAAgRkAAIIFAACJrwCAuOUBALntAQC65QEAu/0BALzlAQC97QEAvuUBAL9ZAQCwXQYAsSEGALIhBgCzIQYAtCEGALUpBgC2EQYAtxEGAKjRAgCp2QIAqg0DAKsFAwCsHQMArQUDAK4FAwCvNQMAvmQCAJGvAICVrwCAma8AgJ2vAIChrwCApa8AgKmvAIC4JQMAuS0DALolAwC7PQMAvCUDAL0pAwC++QMAv/kDALBNAwCxIQMAsiUDALM9AwC0JQMAtS0DALYlAwC3HQMAs4UDAITIAgCtrwCAhAgDALGvAIC2hQMAtZUDALWvAIC75QMAuokDAIYIDACHnAMAv+kDAL7hAwC96QMAvPEDAIXsCgA2rgCAo80DALmvAICl3QMAva8AgMGvAICmzQMAxa8AgMmvAICrrQMAqsEDAK2hAwCsuQMAr6EDAK6pAwDNrwCA0a8AgNWvAIDZrwCA78gDAN2vAIDhrwCA5a8AgOO0AwDprwCA4dABAO2vAICADQAAgXUAAIJ9AADxrwCA9a8AgPmvAICzZQEAvgQCALVlAQABsACABbAAgLZlAQCGQA0Ah1gNALv1AQC6/QEAvaUBALy5AQC/mQEAvqUBAAmwAIANsACAEbAAgIQADAAVsACAGbAAgB2wAIDvzAEAIbAAgOEsBgAlsACA4yABAOwAAAApsACALbAAgDGwAIA1sACAo+kBADmwAIA9sACApukBAEGwAIBFsACApekBAKpxAQCreQEASbAAgE2wAICuKQEArxUBAKw1AQCtKQEAqCUOAKktDgCqJQ4Aqz0OAKwlDgCtLQ4AriUOAK+VDgD9rwCAUbAAgFWwAIBZsACAXbAAgIKdAACBnQAAgJ0AALhFDwC5TQ8AukUPALtZDwC8SQ8AvUkPAL59DwC/cQ8AsPEOALH5DgCypQ4As7kOALSpDgC1lQ4Atp0OALd9DwCo1Q8Aqd0PAKoJDwCrCQ8ArBkPAK0FDwCuDQ8ArwUPAGGwAIBlsACAabAAgL6gAwBtsACAcbAAgId4AwCGEAAAuBUPALkdDwC6IQ8AuyEPALz1AAC9/QAAvvUAAL/tAACwQQ8AsU0PALJdDwCzVQ8AtE0PALU1DwC2MQ8AtzEPAHWwAIDvsAwAebAAgH2wAICBsACAhbAAgImwAICNsACAkbAAgJWwAICZsACAnbAAgKGwAIDjqA0ApbAAgOGMDQCzwQ4AqbAAgK2wAICxsACAtbAAgLbFDgC10Q4AubAAgLvJDgC6xQ4AvbAAgMGwAIC/sQ4AvskOAL3BDgC8yQ4AowEOAMWwAIDJsACAzbAAgNGwAICmBQ4ApREOANWwAICrCQ4AqgUOANmwAICErAIAr3EOAK4JDgCtAQ4ArAkOAIBRAACBWQAAgmEAALPFAAC+zAEAtcUAALbNAADhsACAhkAHAIcUAQC6yQAAu8kAALzZAAC92QAAvskAAL/FAACrDQMAqg0DAKkJAwCouQIArw0DAK4NAwCtDQMArA0DAL5gAwDlsACA6bAAgO2wAIDxsACA9bAAgPmwAIC+MAUAuykDALoZAwC5GQMAuAEDAL/dAwC+3QMAvd0DALwxAwCzTQMAsk0DALFNAwCwTQMAtzkDALYxAwC1QQMAtE0DAP2wAICmkQMApZkDAAGxAICjmQMABbEAgAmxAIANsQCAr5kDAK6VAwCthQMArIUDAKuVAwCqlQMAja8AgBGxAIAVsQCAGbEAgB2xAIAhsQCAJbEAgCmxAIAtsQCAMbEAgDWxAIA5sQCAPbEAgEGxAICAHQAAgQkAAIL9AQBFsQCAvwgHAEmxAIBRsQCA7yQAAFWxAICElAIAWbEAgF2xAICH4AIAhgQFAL4AGABhsQCAZbEAgOGQAQBpsQCA44AAAG2xAIBxsQCAdbEAgLNlAQB5sQCAtWUBALZtAQB9sQCAgbEAgIWxAIC65QEAu/kBALzpAQC96QEAvsUBAL+9AQCJsQCAjbEAgJGxAIC+xBkAlbEAgJmxAICdsQCA78gBAKGxAIDh3A4ApbEAgOMwDgCpsQCArbEAgLGxAICEMAQAgHkAAIEVAACCFQAAo+UBALWxAICl5QEApu0BALmxAICGQAYAh5AHAKplAQCreQEArGkBAK1pAQCuRQEArz0BAKjdBQCpIQYAqiEGAKshBgCsIQYArSEGAK4hBgCvnQYATbEAgL2xAIDBsQCAhDABAMWxAIDJsQCAzbEAgNGxAIC4jQYAuZUGALqdBgC7lQYAvI0GAL21BgC+vQYAv7UGALDtBgCx8QYAsvEGALPxBgC0zQYAtbUGALa9BgC3tQYAqIkHAKmVBwCqkQcAq5EHAKy9BwCtpQcArqEHAK/dBwDVsQCA2bEAgN2xAIDhsQCA5bEAgOmxAIDtsQCA8bEAgLhJBwC5VQcAul0HALtVBwC8cQcAvX0HAL5pBwC/aQcAsKUHALGtBwCyuQcAs7EHALSRBwC1kQcAtnkHALd5BwD1sQCA+bEAgP2xAIABsgCA78gFAOHACQAFsgCA48AZAOMkBAAJsgCA4dAGAO/cKACinQMAoxUBAKAZBQChjQUAs1kGAA2yAIARsgCAFbIAgBmyAIC2ZQYAtXUGAB2yAIC7KQYAuiEGACGyAIAlsgCAvxUGAL4VBgC9JQYAvC0GAKOZBgCPmfwAKbIAgDGyAIA1sgCApqUGAKW1BgA5sgCAq+kGAKrhBgCGKB8Ah5wAAK/VBgCu1QYAreUGAKztBgCebQkAn30HAJwNCwCd7QkAmvENAJs5DQCY5fAAmQ0PAJbh8QCX6fEAlMX1AJUN8wCSHfcAk/H1AJD9+QCR7fkAgh3/AIMB+gA9sgCAQbIAgIYV9gCHOfYAhAn6AIXx9ACKwfAAiyXyAEWyAIBJsgCAjuEMAI8VDgCMNfIAjQHzAJKtDgCTgQgATbIAgFGyAICW6QQAl3UGAJR5CgCV8QoAmtEGAJvJAABVsgCAWbIAgIEdAwCAHQMAnFkCAIL1AwCrARAAqpUWAKmNFgCojRYAr5UuAK4BLACt/RIArJkSAKOlHgCipR4AoY0CAN2wAICnGRoAppUaAKUBGACknR8AXbIAgGGyAIBlsgCAabIAgG2yAIBxsgCAdbIAgHmyAICz5SoAsuUqALGtLwCw5S4AfbIAgIGyAIC1ASQAtBEqAKgpAwCpNQMAqj0DAKs1AwCsLQMArbUDAK69AwCvtQMAhbIAgImyAICNsgCAkbIAgIAdAACBCQAAgrkAAJWyAIC4TQIAuV0CALptAgC7CQIAvBkCAL0ZAgC+CQIAvwECALDNAwCx1QMAst0DALPVAwC0zQMAtXUCALZ9AgC3dQIAmbIAgITIHQChsgCAvgwfAKWyAICpsgCA70gGAO9YBwDhWAYA4ZgGAOOUAQDjAAYAhhAcAId8HQC+9B4ArbIAgLGyAIC2ZQMAtfUDALWyAICz5QMAubIAgL2yAIDBsgCAv+ECAL5ZAwC9UQMAvFkDALtBAwC6WQMAxbIAgMmyAIAtsgCAnbIAgM2yAIDRsgCA1bIAgNmyAIDdsgCA4bIAgKitHQCptR0AqrUdAKslHgCsPR4ArR0eAK4VHgCvdR4AsA0eALEtHgCyJR4As40eALSVHgC1nR4AtpUeALeNHgC4tR4Aub0eALq1HgC7nR4AvIUeAL1VHwC+XR8Av1UfALMdHQDlsgCA6bIAgO2yAIDxsgCAtr0eALWVHgD1sgCAu8keALrpHgD5sgCA/bIAgL95HgC+cR4AvXkeALzRHgCCKQAAo1kdAIAdAACBFQAApvkeAAGzAIAFswCApdEeAKqtHgCrjR4ACbMAgITgAwCuNR4Arz0eAKyVHgCtPR4AqIkeAKmVHgCqnR4Aq7EeAKzRHgCt2R4Ars0eAK/FHgANswCAEbMAgIaIAACHbAEAFbMAgBmzAIAdswCAIbMAgLhdAQC5wQEAusEBALvBAQC8wQEAvckBAL7xAQC/8QEAsL0eALGdHgCylR4As2UBALR9AQC1ZQEAtm0BALdlAQCqLR0AqzUdACWzAIApswCAri0dAK+VHACsLR0ArSUdAISMAQCjkR0ALbMAgDGzAICmER0ANbMAgDmzAIClgR0As1UeAD2zAIBBswCARbMAgEmzAIC2GR4AtRkeAE2zAIC7GR4AujkeAFGzAIBVswCAv+EBAL75AQC98QEAvAEeAFmzAIBdswCAYbMAgKOZHQBlswCApdUdAKbVHQBpswCAbbMAgHGzAICq9R0Aq9UdAKzNHQCtPQIArjUCAK8tAgCAZQAAgRUAAIIdAACEAAQAdbMAgHmzAICHcAMAhvwEAIGzAICFswCAibMAgI2zAICRswCAlbMAgJmzAICdswCAvsgEAKGzAIClswCAqbMAgK2zAICxswCAtbMAgO/cHwC5swCA4ZQBAL2zAIDjHAEAwbMAgMWzAIDJswCAzbMAgLt1AwC6aQMAvkgGANGzAIC/HQMAvh0DAL0dAwC8ZQMAs9UDANWzAIDZswCA3bMAgOGzAIC2fQMAtcUDAIRwBQCoJQIAqTUCAKo9AgCrNQIArC0CAK2dAgCulQIAr7UCAIIVAADlswCAgNkBAIEJAADEAAAA6bMAgPGzAID1swCAuKkCALmpAgC6SQEAu0kBALxZAQC9RQEAvkUBAL99AQCwzQIAsdECALLRAgCzqQIAtLkCALW5AgC2qQIAt6ECAOEoHgDhNBwA43QBAOMYHgD5swCA/bMAgIa4BACHVAUAhDgHAAG0AIAFtACACbQAgL6sBwANtACA78weAO/IGgCj9QIAEbQAgBW0AIAZtACAHbQAgKZdAgCl5QIAIbQAgKtVAgCqSQIAJbQAgCm0AICvPQIArj0CAK09AgCsRQIAqGEGAKlhBgCqYQYAq2EGAKxhBgCtYQYArmEGAK9hBgDtswCALbQAgDG0AIA1tACAObQAgD20AIBBtACARbQAgLjxBgC58QYAuvEGALvxBgC8nQYAvbEGAL6xBgC/sQYAsOUGALHtBgCy5QYAs/0GALTlBgC17QYAttkGALfVBgCz6QYASbQAgE20AIBRtACAVbQAgLbhBgC16QYAWbQAgLspBgC6IQYAXbQAgGG0AIC/KQYAviEGAL0pBgC8MQYAgl0AAKOtBgCARQAAgV0AAKalBgBltACAabQAgKWtBgCqZQYAq20GAIYADACHQAMArmUGAK9tBgCsdQYArW0GAG20AIDvfAUAcbQAgHW0AIB5tACAfbQAgIG0AICFtACAibQAgI20AICRtACAlbQAgJm0AIDjaAUAnbQAgOF4BQCz0QYAobQAgKW0AICptACArbQAgLb9BgC1/QYAsbQAgLupBgC6oQYAtbQAgLm0AIC/mQYAvqkGAL2pBgC8sQYAqLkGAKm5BgCqGQYAqxkGAKw1BgCtPQYArjUGAK8pBgC9tACAgh0AAIEdAACAHQAAwbQAgMW0AIDJtACA0bQAgLjpAQC56QEAuvkBALv5AQC86QEAvekBAL5dAQC/VQEAsCUGALEtBgCyJQYAsz0GALQtBgC1HQYAthUGALfZAQCGgAwAh+QCANW0AICjnQUA2bQAgKWxBQCmsQUA3bQAgOG0AIDltACAqu0FAKvlBQCs/QUAreUFAK7lBQCv1QUAtk0DAOm0AICExAMAtUUDAO20AICzjQIA8bQAgPW0AIC+SQMAv0kDALxJAwC9SQMAumkDALtpAwD5tACA/bQAgAG1AICmiQMApYEDAAW1AICjSQIACbUAgA21AIARtQCAr40DAK6NAwCtjQMArI0DAKutAwCqrQMAfbMAgBW1AIAZtQCAHbUAgIW0PQAhtQCAJbUAgCm1AIAttQCAMbUAgIA9AACBCQAAgh0AADW1AIC+sAMAObUAgIc4AwCG3AwAQbUAgEW1AIBJtQCATbUAgFG1AIDvXAYAVbUAgFm1AIC+6AwA45QGAF21AIDh3AEAYbUAgGW1AIBptQCAbbUAgLNRAQBxtQCAdbUAgHm1AIB9tQCAtnEBALV5AQCBtQCAuz0BALo9AQCFtQCAibUAgL/9AQC+9QEAvQUBALwFAQCNtQCAkbUAgJW1AICEQAwAmbUAgJ21AIChtQCA76wHAKW1AIDhJAYAqbUAgONABwCGkAwAh/wMALG1AIC1tQCAgFkAAIFlAACCYQAAo90BALm1AICl9QEApv0BAL21AIDBtQCAxbUAgKqxAQCrsQEArIkBAK2JAQCueQEAr3EBAM20AIA9tQCAybUAgM21AICttQCA0bUAgNW1AIDZtQCAqJ0NAKktDgCqOQ4AqzEOAKwRDgCtEQ4Arn0OAK9tDgCwGQ4AsRkOALIxDgCzMQ4AtNEOALXZDgC2zQ4At8UOALj9DgC52Q4AuqkOALupDgC8vQ4AvaUOAL6tDgC/pQ4AqIEPAKmBDwCqgQ8Aq4EPAKyBDwCtjQ8AroUPAK+1DwDdtQCA4bUAgOW1AIDptQCA7bUAgPG1AID1tQCA+bUAgLidDwC5rQ8AuqUPALtNDwC8VQ8AvV0PAL5JDwC/SQ8AsNEPALHRDwCy0Q8As9EPALS1DwC1vQ8AtrUPALetDwCzCQ4A/bUAgAG2AIAFtgCACbYAgLYNDgC1CQ4ADbYAgLsVDgC6FQ4AEbYAgBW2AIC/eQ4AvnEOAL0FDgC8BQ4AghUAAKNNDgCAYQAAgWEAAKZJDgAZtgCAvhABAKVNDgCqUQ4Aq1EOAIQkAQAhtgCArjUOAK89DgCsQQ4ArUEOAKg5DgCpOQ4AqlkOAKtRDgCscQ4ArXEOAK6RAQCvkQEAhgAAAIeEAAAltgCAKbYAgC22AIAxtgCANbYAgDm2AIC4dQEAuX0BALp1AQC7yQAAvNkAAL3ZAAC+yQAAv8EAALD1AQCx/QEAsvUBALNNAQC0VQEAtV0BALZVAQC3TQEAuk0PALtVDwC4TQ8AuUUPAL59DwC/tQ8AvEUPAL11DwCyAQ8AswEPALAxDwCxMQ8AtgEPALcNDwC0EQ8AtREPAKqZDgCrRQ8AqOUOAKmZDgCuQQ8Ar0EPAKxRDwCtUQ8APbYAgEG2AIBFtgCASbYAgE22AIBRtgCAVbYAgFm2AICzUQ0AXbYAgGG2AIBltgCAabYAgLZxDQC1eQ0AbbYAgLu5AgC6sQIAcbYAgHW2AIC/GQIAvhECAL0ZAgC8oQIAebYAgKMVDQB9tgCAgbYAgKY1DQCFtgCAibYAgKU9DQCq9QIAq/0CAIToAwCRtgCArlUCAK9dAgCs5QIArV0CAKhtAgCprQIAqqUCAKu9AgCspQIAra0CAK6lAgCvfQEAgO0BAIHxAQCC8QEAvqAFAJW2AICZtgCAh2gFAIYcBQC4yQEAuckBALrZAQC70QEAvPkBAL35AQC+mQEAv5UBALAFAQCxDQEAsgUBALMdAQC0BQEAtQ0BALYFAQC3+QEA4WQPAOGcDwDjFA4A49QPAJ22AIDhPA4AobYAgOPkAAC+rAQApbYAgKm2AIDvDAAArbYAgLG2AIDvYA4A77QPALW2AIC5tgCAhEQEALNhAgC9tgCAtWECALZhAgDBtgCAxbYAgMm2AIC6jQEAu4UBALydAQC9hQEAvo0BAL+FAQCjrQUAjbYAgM22AIDRtgCA1bYAgKatBQClrQUA2bYAgKtJBgCqQQYA3bYAgOG2AICvSQYArkEGAK1JBgCsUQYA5bYAgOm2AIDttgCA8bYAgIAdAACBCQAAgjkAAPW2AID5tgCA/bYAgIbIAACHIAMAAbcAgAW3AIAJtwCADbcAgKhtBgCptQcAqr0HAKsdBwCsCQcArTEHAK4xBwCvLQcAhKgDABG3AIAVtwCAGbcAgB23AIAhtwCAJbcAgCm3AIC4zQAAudUAALrVAAC75QAAvP0AAL2VAAC+nQAAv5UAALBVBwCxJQcAsi0HALM9BwC0LQcAtRUHALYdBwC39QAALbcAgOG8BgAxtwCA4/QFADW3AIA5twCAPbcAgEG3AIBFtwCASbcAgE23AIBRtwCAVbcAgFm3AIBdtwCA7+gEALN1BgCCLQAAgRUAAIAdAABhtwCAtvEGALXBBgBltwCAu6EGALrRBgBptwCAvmwBAL+RBgC+qQYAvakGALy5BgCjtQYAcbcAgIYoAACHTAEAdbcAgKYxBgClAQYAebcAgKthBgCqEQYAfbcAgIG3AICvUQYArmkGAK1pBgCseQYAhbcAgLO9AQCJtwCAjbcAgLZ5AQCRtwCAlbcAgLV5AQC6VQEAu10BAJm3AICdtwCAvvkAAL/lAAC8RQEAvf0AAKhxAgCpcQIAqnECAKtxAgCstQIArb0CAK61AgCvrQIAhOw8AKG3AICltwCAqbcAgK23AICxtwCAtbcAgLm3AIC4XQMAuWUDALptAwC7ZQMAvH0DAL1lAwC+bQMAv2UDALDVAgCx3QIAstUCALNtAwC0eQMAtWUDALZtAwC3ZQMAHbYAgL23AIDBtwCAo/UCAMW3AIClMQIApjECAMm3AIDNtwCA0bcAgKodAgCrFQIArA0CAK21AwCusQMAr60DAIBlAACBCQAAghkAANW3AIDZtwCA4bcAgL4QPADltwCAhsA8AIcgAwDptwCA7bcAgPG3AID1twCA+bcAgP23AICohQIAqZUCAKqVAgCrpQIArL0CAK3VAgCu0QIAr9ECAAG4AIAFuACACbgAgA24AIARuACAFbgAgBm4AIAduACAuHUBALl9AQC6dQEAu8kBALzZAQC9xQEAvsUBAL/9AQCwtQIAsb0CALKBAgCzgQIAtFUBALVdAQC2VQEAt00BAOGkBgAhuACA41AGAL6APACEHDwAvoA/ACW4AIApuACALbgAgDG4AIA1uACAObgAgD24AIBBuACA7+AGAEW4AICBfQAAgHEAAEm4AICCBQAAUbgAgFW4AIDvTAAAWbgAgOGQAQBduACA41gBAGG4AIBluACAabgAgIZYPwCH/DwAs509AN23AIBNuACAbbgAgHG4AIC21T0AtbU9AHW4AIC7+T0AuvE9AHm4AIB9uACAvxk+AL4RPgC91T0AvNU9AIG4AICj2T0AhbgAgIm4AICmkT0AjbgAgJG4AICl8T0AqrU9AKu9PQCVuACAmbgAgK5VPgCvXT4ArJE9AK2RPQCoVT4AqVk+AKphPgCrYT4ArGE+AK1hPgCuYT4Ar2E+AISoAwCduACAobgAgKW4AICpuACArbgAgLG4AIC1uACAuEU/ALldPwC6VT8Au20/ALx1PwC9fT8AvnU/AL9tPwCwwT8AscE/ALLBPwCzwT8AtME/ALXBPwC2wT8At8E/AIC5AQCBuQEAggUAALm4AIDhgD4AwbgAgOMoPQDFuACAhoAAAIcEAQDvCD0AybgAgM24AIDRuACA1bgAgNm4AICzqT8AvbgAgN24AIDhuACA5bgAgLahPwC1qT8A6bgAgLtFPgC6RT4A7bgAgPG4AIC/RT4AvkU+AL1VPgC8VT4Ao2k/APW4AID5uACA/bgAgAG5AICmYT8ApWk/AAW5AICrhT4AqoU+AAm5AIANuQCAr4U+AK6FPgCtlT4ArJU+ABG5AICzGT4AFbkAgBm5AIC2IT4AHbkAgCG5AIC1MT4AuvEBALv5AQAluQCAKbkAgL6xAQC/vQEAvNEBAL3RAQCo0T0AqdE9AKrVPQCr6T0ArP09AK3lPQCu7T0ArxECAID5AwCBzQMAgsUDAIQkAwC+AAQAMbkAgIesAwCGvAQAuBkCALktAgC6JQIAu+kCALz5AgC9+QIAvukCAL/pAgCwcQIAsXkCALJBAgCzQQIAtDECALU9AgC2NQIAtykCAKVtPQA1uQCAObkAgKZ9PQA9uQCAbbcAgKNFPQBBuQCArY0CAKyNAgCv4QIAru0CAKwAAABFuQCAq6UCAKqtAgDh+AEASbkAgOP0AgCEwAQATbkAgFG5AIBVuQCAWbkAgF25AIBhuQCAZbkAgGm5AIBtuQCAcbkAgO8wAgB1uQCAqBUCAKkZAgCqJQIAqz0CAKwlAgCtLQIAriUCAK9VAgB5uQCAfbkAgIG5AICFuQCAibkAgI25AICEsAQAkbkAgLjRAgC52QIAuuECALvhAgC8kQIAvZ0CAL6VAgC/iQIAsC0CALE1AgCyNQIAswUCALQdAgC18QIAtvECALfxAgDheD8A4zQBAOMIPgDhbD4AgQkAAICpAACVuQCAgj0AAJm5AIChuQCApbkAgL4gBACpuQCA79g+AO/MPgCtuQCAsbkAgLPpAgCG6AQAh8AEALbpAgC1uQCAubkAgLXpAgC6rQIAu7UCAL25AIDBuQCAvp0CAL9xAgC8pQIAvZUCAC25AICduQCAxbkAgMm5AIDNuQCA0bkAgNW5AIDZuQCAqBUGAKmhBgCqoQYAq70GAKytBgCtgQYArv0GAK/tBgCwlQYAsZ0GALKVBgCzrQYAtLUGALW9BgC2tQYAt60GALiVBgC5mQYAukkHALtJBwC8WQcAvVkHAL5JBwC/SQcArN0FAK3tBQCu5QUArwkFAN25AIDhuQCAqtUFAKvNBQDluQCApZEFAKaRBQDpuQCA7bkAgPG5AID1uQCAo5EFALNJBgD5uQCA/bkAgAG6AIAFugCAtmEGALVFBgAJugCAuzkGALoxBgC+ZAAADboAgL8ZBgC+EQYAvRkGALwhBgCjiQcAgtkBAIHZAQCAwQEAEboAgKahBwClhQcAFboAgKv5BwCq8QcAhggBAId8AQCv2QcArtEHAK3ZBwCs4QcAGboAgLP1BgAdugCAIboAgLaFBgAlugCAKboAgLWdBgC6jQYAu20BAC26AIAxugCAvmUBAL9tAQC8dQEAvW0BAKglBgCpLQYAqjkGAKsxBgCsUQYArUEGAK5BBgCvdQYANboAgDm6AIA9ugCAQboAgEW6AIBJugCATboAgFG6AIC4VQEAuWUBALplAQC7fQEAvGUBAL1tAQC+HQEAvxUBALANBgCx7QEAsuUBALP9AQC05QEAte0BALblAQC3bQEAo7EFAFW6AIBZugCAvkgDAL5YDACmwQUApdkFAF26AICrKQIAqskFAGG6AIBlugCArykCAK4hAgCtKQIArDECAGm6AIBtugCAcboAgHW6AICAGQAAgRkAAIIFAAB5ugCAhKwDAIG6AICHGAMAhswMAIW6AICJugCAjboAgJG6AICokQMAqZkDAKrJAwCrxQMArN0DAK3BAwCuwQMAr/UDAJW6AICZugCAnboAgKG6AIClugCAqboAgK26AICxugCAuH0DALnBAAC6wQAAu9EAALz5AAC9+QAAvpkAAL+ZAACwjQMAsUUDALJNAwCzRQMAtF0DALVFAwC2TQMAt0UDALNBAgC1ugCAuboAgL8EDwC9ugCAtkECALVVAgDBugCAu4ECALpJAgDFugCAyboAgL+BAgC+mQIAvZECALyZAgDNugCA0boAgNW6AIDZugCA76QDAN26AIDhugCA5boAgOMQAwDpugCA4VgAAIQgDQCAKQAAgSkAAIIdAADxugCA4VAGAOGgBwDjoAYA41AHAIWUDAD1ugCA70gbAPm6AIDhJAIA/boAgONwGgABuwCABbsAgAm7AIDvqAEA7+gGAIagDwCHDA0Ao4kCAA27AIClnQIAEbsAgBW7AICmiQIAGbsAgB27AICrSQIAqoECAK1ZAgCsUQIAr0kCAK5RAgCoZQ4AqXUOAKp9DgCrdQ4ArG0OAK21DgCuvQ4Ar7UOAO26AIAhuwCAJbsAgCm7AIAtuwCAOLsAgDy7AIBAuwCAuF0PALltDwC6ZQ8Auw0PALwVDwC9HQ8AvhUPAL8JDwCwzQ4AsdUOALLdDgCz1Q4AtM0OALVxDwC2cQ8At20PALP1DgBEuwCASLsAgEy7AIBQuwCAtjUOALXlDgBUuwCAuxEOALoJDgBYuwCAXLsAgL+1DwC+CQ4AvQEOALwJDgCCFQAAo7EOAIBhAACBYQAApnEOAGC7AIC+EAEApaEOAKpNDgCrVQ4AaLsAgIQgAQCuTQ4Ar/EPAKxNDgCtRQ4An0UIAJ4NCQCdDQkAnJkLAJt1NQCaETUAmZk3AJgNMQCXJTEAliUxAJWBPQCUDT0Ak4k/AJIVOACRPTkAkD05AI9lJQDvrA0AhgAEAIegAQBsuwCAcLsAgHS7AIDv6AEAeLsAgOE0AgB8uwCA4zQBAIC7AIDjCAwAhLsAgOEIDQChoQEAiLsAgKMJBQCibQMApc0EAKQRBQCnHRkAph0ZAKmhHQCoORkAq+kcAKqpHQCtkREArAEQAK8BFACuUREAsfkVALDlFQCz6WkAsgFoALUBbAC0eWkAjLsAgJC7AICUuwCAmLsAgJy7AICguwCAowkDAKIZDQCh/Q0AoP0NAIIlJgCDBToApLsAgKi7AICGqTwAhzU+AIQdOgCFPTsAiok+AIslMgCsuwCAsLsAgI6xNACPMTYAjD0yAI0tMgCSJTYAk9EIAIREAwC+wAQAlhULAJdVDgCUXQoAlVUKAJplDgCbiQ4AtLsAgLi7AIC8uwCAwLsAgJyBAADEuwCAuLUCALm9AgC6tQIAuwkCALwZAgC9GQIAvgkCAL8BAgCwdQ0AsX0NALJJDQCzSQ0AtJUCALWdAgC2lQIAt40CAKi9DQCpUQ0AqlUNAKtpDQCsfQ0ArWUNAK5tDQCvEQ0AZLsAgILtAQCBHQAAgB0AAMi7AIDMuwCAfboAgL5wBQCznQwAhIwFANC7AIDYuwCA3LsAgLalDAC1tQwA4LsAgLv5DAC68QwAhigFAIcgBQC/GQMAvhEDAL3dDAC83QwA5LsAgKPZDADouwCA7LsAgKbhDADwuwCA9LsAgKXxDACqtQwAq70MAPi7AID8uwCArlUDAK9dAwCsmQwArZkMAAC8AIAEvACACLwAgAy8AIAQvACAFLwAgBi8AIDvvAEAHLwAgOF8DgAgvACA41ABACS8AIAovACALLwAgDC8AICzlQIANLwAgDi8AIA8vACAQLwAgLa9AgC1uQIASLwAgLs5AgC6YQIAhsgEAIesBAC/GQIAvhECAL0ZAgC8IQIAo1UFAILVBwCBxQcAgMUHAEy8AICmfQUApXkFAFC8AICr+QUAqqEFAFS8AIBYvACAr9kFAK7RBQCt2QUArOEFAFy8AICzWQcAYLwAgGS8AIC2HQcAaLwAgGy8AIC1FQcAugkHALsJBwBwvACAdLwAgL75BwC/+QcAvPkHAL35BwDUuwCARLwAgHi8AIB8vACAgLwAgIS8AICIvACAjLwAgKitBwCptQcAqrUHAKvtBwCs+QcArfkHAK7tBwCv5QcAsKkHALGpBwCySQcAs0kHALRZBwC1WQcAtkkHALdJBwC4eQcAuUUHALpBBwC7XQcAvEUHAL1NBwC+RQcAvzkHAKMdBgCQvACAlLwAgJi8AICcvACAplkGAKVRBgCgvACAq00GAKpNBgCkvACAqLwAgK+9BgCuvQYArb0GAKy9BgCAbQAAgQkAAIIZAACsvACAsLwAgISYAQC+kAEAtLwAgIYAHACHxAEAuLwAgLy8AIDAvACAxLwAgMi8AIDMvACAqF0GAKmVAQCqlQEAq6UBAKy9AQCt1QEArtEBAK/RAQDQvACA1LwAgNi8AIDcvACA4LwAgOS8AIDovACA7LwAgLhZAQC5WQEAus0AALvFAAC83QAAvcUAAL7FAAC/9QAAsLUBALG9AQCygQEAs4EBALR5AQC1eQEAtmkBALdpAQCzHQIA8LwAgPS8AIC+gBwA+LwAgLZVAgC1NQIA/LwAgLt5AgC6cQIAAL0AgAS9AIC/vQIAvr0CAL1VAgC8VQIACL0AgKNZAgAMvQCAEL0AgKYRAgAUvQCAGL0AgKVxAgCqNQIAqz0CABy9AIAgvQCArvkCAK/5AgCsEQIArRECACi9AIAsvQCAvgQdAL4AHgAwvQCANL0AgDi9AIA8vQCAgPkAAIHNAACCxQAAhCADAIawHACHlAMAQL0AgES9AIBIvQCATL0AgFC9AIBUvQCA42wCAFi9AIDhoAEAXL0AgO8UAgBgvQCAZL0AgGi9AIBsvQCAcL0AgHS9AIB4vQCA4fAGAOE0BgDjTAAA4xgGAHy9AICAvQCAhL0AgIi9AICAPQAAgQkAAIIZAACMvQCAkL0AgIS8HQDvmAAA7zgHALMxAgDRAAAAh9gdAIZsHACYvQCAtikCALUhAgCcvQCAu80CALrNAgCgvQCApL0AgL/NAgC+zQIAvc0CALzNAgCyXQYAs2UGALANBgCxVQYAtn0GALedBQC0fQYAtXUGALqNBQC7zQUAuKUFALmFBQC+xQUAv8kFALzVBQC9zQUAqL0AgKy9AICwvQCAtL0AgLi9AIC8vQCAwL0AgMS9AICqtQYAq70GAKgBBwCpvQYAroEGAK+NBgCsmQYArZUGAKNxHQDIvQCAzL0AgNC9AIDUvQCApmkdAKVhHQDYvQCAq40dAKqNHQDcvQCA4L0AgK+NHQCujR0ArY0dAKyNHQDkvQCAs9UeAOi9AIDsvQCAts0eAPC9AID0vQCAtcUeALqhHgC7oR4A+L0AgPy9AIC+pR4Av6keALyxHgC9sR4AJL0AgJS9AIAAvgCAhAQDAID5AACB+QAAghEAAAS+AICoIR4AqSEeAKo5HgCrOR4ArCkeAK0pHgCuAR4ArwEeALABHgCxAR4AsgEeALMBHgC0BR4AtQkeALY9HgC3NR4AuA0eALkVHgC6HR4AuxUeALwNHgC95R8Avu0fAL/lHwCjkR8ACL4AgIYoAQCHSAEADL4AgKaJHwClgR8AEL4AgKvlHwCq5R8AFL4AgBi+AICv7R8AruEfAK31HwCs9R8AHL4AgLMtHgAgvgCAJL4AgLaVHgAovgCALL4AgLWdHgC6sR4Au7EeADC+AIA0vgCAvnUBAL99AQC8oR4AvaEeAKjRHgCp2R4AquEeAKvhHgCsUR4ArVEeAK5RHgCvUR4AOL4AgDy+AIBAvgCARL4AgEi+AIBMvgCAUL4AgFS+AIC43QEAue0BALrlAQC7jQEAvJkBAL2ZAQC+jQEAv4UBALAxHgCxMR4AsjEeALMxHgC09QEAtf0BALb1AQC37QEAo2kdAFi+AIBcvgCAYL4AgGS+AICm0R0ApdkdAGi+AICr9R0AqvUdAGy+AIBwvgCArzkCAK4xAgCt5R0ArOUdAIFpAACAWQAAvgAEAIJhAAB4vgCAfL4AgIC+AICEvgCAhOwDAIi+AICHiAMAhuwEAIy+AICQvgCAlL4AgJi+AICohQMAqZUDAKqVAwCrpQMArL0DAK3VAwCu0QMAr9EDAJy+AICgvgCApL4AgKi+AICsvgCAsL4AgLS+AIC4vgCAuHEDALlxAwC6cQMAu3EDALzVAAC93QAAvtUAAL/NAACwtQMAsb0DALKBAwCzgQMAtFEDALVRAwC2UQMAt1EDAOFUHgDhrB8A45QBAOMoHgDjYAMAvL4AgOEIAADAvgCA75ADAMS+AIDIvgCAzL4AgNC+AIDUvgCA70wfAO9MHwCzXQIA2L4AgNy+AIDgvgCA6L4AgLYVAgC1dQIA7L4AgLs5AgC6MQIAhCQFAL7gBAC/1QIAvtUCAL0VAgC8FQIAuJEdALmZHQC6oR0Au6EdALzRHQC93R0AvtUdAL/JHQCwCR4AsQkeALIZHgCzGR4AtAkeALUJHgC2vR0At7UdAKipHgCpqR4AqrkeAKu5HgCsqR4ArakeAK55HgCveR4AgKUAAIGtAACCpQAA8L4AgIbQBACH+AQA9L4AgPi+AIB0vgCA5L4AgPy+AIAAvwCABL8AgAi/AIAMvwCAEL8AgKhxBgCpcQYAqnEGAKtxBgCsVQYArUUGAK5NBgCvRQYAsD0GALHlBgCy7QYAs+UGALT9BgC15QYAtu0GALflBgC43QYAuXEHALp1BwC7SQcAvFkHAL1ZBwC+SQcAv0kHALPZBgAUvwCAGL8AgBy/AIAgvwCAtuUGALX9BgAkvwCAuwEGALrZBgAovwCALL8AgL8BBgC+GQYAvREGALwZBgAwvwCAo9kFADS/AIA4vwCAppEFADy/AIBAvwCApfEFAKq1BQCrvQUARL8AgEi/AICuUQUAr1EFAKyRBQCtkQUAo1kHAIIZAACBGQAAgOEBAEy/AICmZQcApX0HAFC/AICrgQcAqlkHAISgAgC+rAEAr4EHAK6ZBwCtkQcArJkHAFS/AICzqQYAhugAAIcsAQC2WQEAWL8AgFy/AIC1oQYAunUBALt9AQBgvwCAZL8AgL75AQC/+QEAvGUBAL35AQCo0QYAqdkGAKplBgCrdQYArG0GAK2dAQCulQEAr40BAITsAQBovwCAbL8AgHC/AIB0vwCAeL8AgHy/AICAvwCAuGkBALlpAQC6CQEAuwUBALwdAQC9AQEAvgEBAL81AQCw9QEAsf0BALL1AQCzaQEAtHkBALV5AQC2aQEAt2EBAIS/AICIvwCAjL8AgKPhBQCQvwCApekFAKYRAgCUvwCAmL8AgJy/AICqPQIAqzUCAKwtAgCtsQIArrECAK+xAgCgvwCApL8AgL4EAwCEAAwAqL8AgKy/AICwvwCAtL8AgIANAACBFQAAgh0AALi/AIC8vwCAwL8AgIdEAwCG3AwAs+kDAMi/AIDMvwCA0L8AgNS/AIC2PQMAtT0DANi/AIC7GQMAuhEDANy/AIDgvwCAv7kAAL6xAAC9uQAAvAEDAOS/AIDhlAEA6L8AgON8AQDsvwCA8L8AgPS/AID4vwCA/L8AgADAAIAEwACACMAAgAzAAIAQwACAFMAAgO9MAgCoVQIAqV0CAKphAgCrYQIArLUCAK29AgCutQIAr60CAL5oDQAYwACAHMAAgCDAAIAkwACAgq0AAIGtAACArQAAuGEBALlhAQC6CQEAuwkBALwBAQC9AQEAvgEBAL8BAQCw1QIAsd0CALLVAgCzbQEAtHUBALV9AQC2aQEAt2EBAOFoBgDh8AcA47AAAOP0BgAowACALMAAgDDAAIA4wACAPMAAgEDAAIBEwACASMAAgL78DABMwACA72wAAO8oBgCjqQIAUMAAgIZoDACHBA0AVMAAgKZ9AgClfQIAWMAAgKtZAgCqUQIAXMAAgGDAAICv+QEArvEBAK35AQCsQQIAqIUOAKmNDgCqhQ4Aq50OAKyNDgCtvQ4ArrUOAK/dDgA0wACAZMAAgGjAAIBswACAcMAAgHTAAIB4wACAfMAAgLitDgC5tQ4Aur0OALu1DgC8dQ8AvX0PAL51DwC/bQ8AsKkOALG1DgCyvQ4As7UOALStDgC1lQ4Atp0OALeVDgCzDQ4AgMAAgITAAICIwACAjMAAgLY9DgC1BQ4AkMAAgLtxDgC6bQ4AlMAAgJjAAIC/UQ4AvmkOAL1hDgC8aQ4AghkAAKNJDgCAZQAAgRkAAKZ5DgCcwACAoMAAgKVBDgCqKQ4AqzUOAIS8AwCkwACAri0OAK8VDgCsLQ4ArSUOAKidDgCppQ4Aqq0OAKulDgCsvQ4AraEOAK7dDgCvzQ4AhiABAIdkAQCowACArMAAgLDAAIC0wACAuMAAgLzAAIC4eQEAuXkBALrNAQC7xQEAvN0BAL3FAQC+xQEAv/UBALC9DgCxjQ4AsoUOALNJAQC0WQEAtVkBALZJAQC3SQEAtS0OAMDAAIDEwACAtjkOAMjAAIDMwACAsz0OANDAAIC9hQEAvEkOAL+FAQC+hQEA1MAAgMS/AIC7UQ4AumEOAKNlDgDYwACA3MAAgODAAIDkwACApmEOAKV1DgDowACAqwkOAKo5DgDswACA8MAAgK/dAQCu3QEArd0BAKwRDgD0wACA+MAAgO/QDwD8wACAAMEAgATBAIAIwQCADMEAgBDBAIC+aAMAGMEAgBzBAIDhVA4AIMEAgONkDgAkwQCAgFkAAIFZAACCaQAAhIwDAIbwBACHFAMAKMEAgCzBAIAwwQCANMEAgDjBAIA8wQCAQMEAgETBAIBIwQCATMEAgFDBAIBUwQCAWMEAgFzBAIBgwQCAZMEAgGjBAIBswQCAqIkDAKmJAwCqmQMAq5kDAKyJAwCtiQMArj0DAK81AwCwUQMAsVEDALJVAwCzfQMAtBUDALUdAwC2FQMAtw0DALg9AwC5DQMAugUDALvtAAC89QAAvfkAAL7pAAC/6QAAcMEAgHTBAIB4wQCAsz0CAHzBAIC1LQIAtiUCAIDBAIC+aAUAiMEAgLq5AgC7uQIAvK0CAL2FAgC+/QIAv/UCAIBJAACBVQAAglUAAIQABQDvjAMAvhgEAId0BQCG/AQA4zwDAIzBAIDhUAAAkMEAgJTBAICYwQCAnMEAgKDBAICkwQCAqMEAgKzBAICwwQCAtMEAgLjBAIC8wQCA79QOAL4oBgDhdA4AwMEAgONUAQDEwQCAyMEAgMzBAIDQwQCAo/ECANTBAIDYwQCA3MEAgODBAICm6QIApeECAOTBAICrdQIAqnUCAOjBAIDswQCArzkCAK4xAgCtSQIArGECAKgpBgCpKQYAqj0GAKsxBgCsSQYArUkGAK55BgCveQYAhMEAgIIVAACBxQcAgMUHAPDBAICEaAMA9MEAgPjBAIC4yQYAuckGALrZBgC72QYAvMkGAL3JBgC+WQcAv1kHALAJBgCxCQYAshkGALMZBgC0CQYAtQkGALb5BgC3+QYAs7UGAPzBAICGrAAAh0ADAADCAIC2yQYAtcEGAATCAIC7zQYAus0GAAjCAIAMwgCAv80GAL7NBgC9zQYAvM0GABDCAICj8QYAFMIAgBjCAICmjQYAHMIAgCDCAIClhQYAqokGAKuJBgAkwgCAKMIAgK6JBgCviQYArIkGAK2JBgCoJQYAqWEGAKplBgCrfQYArGUGAK1tBgCuZQYAr50GACzCAIAwwgCANMIAgDjCAIA8wgCAQMIAgETCAIBIwgCAuPUGALn9BgC69QYAu4kGALyZBgC9mQYAvokGAL+BBgCw5QYAse0GALLlBgCz/QYAtOUGALXtBgC20QYAt80GAEzCAIC2/QYAtf0GAFDCAICz/QYAVMIAgFjCAIBcwgCAvzkGAL4xBgC9OQYAvCEGALs5BgC6MQYAFMEAgGDCAICjrQYAgnkAAIFVAACAVQAAhFwBAKatBgClrQYAaMIAgKtpBgCqYQYAhkh/AIfkAACvaQYArmEGAK1pBgCscQYAbMIAgO/cBwBwwgCAdMIAgHjCAIB8wgCAgMIAgITCAICIwgCAhKADAIzCAIC/JHkAkMIAgONoBwCUwgCA4XQGALPRAgCYwgCAvgQDAISAfQCcwgCAtvkCALXxAgCgwgCAu7UCALqpAgCkwgCAqMIAgL9RAwC+mQIAvZECALylAgCpBQIAqLkCAKsVAgCqHQIArT0CAKw9AgCvUQIArl0CAL5ofQCswgCAsMIAgLTCAIC4wgCAvMIAgMDCAIDEwgCAufEDALjpAwC78QMAuvkDAL1RAwC86QMAv00DAL5RAwCxNQIAsCkCALMBAgCyNQIAtdEDALQZAgC30QMAttkDAIIpAACjlQMAgB0AAIEVAACmvQMAyMIAgMzCAICltQMAqu0DAKvxAwDQwgCA2MIAgK7dAwCvFQIArOEDAK3VAwCGYH0Ah3h9ALNBAQCEAH8AtUEBANzCAIDgwgCAtkkBAOTCAIDowgCAu0EBALpNAQC9SQEAvEUBAL8pAQC+OQEA7MIAgO/cBgDwwgCA9MIAgPjCAID8wgCAAMMAgO8wBgCELH4A4eAGAATDAIDjiAEACMMAgON0AAAMwwCA4SwBAKPJAQAQwwCAFMMAgIVweQAYwwCApsEBAKXJAQAcwwCAq8kBAKrFAQAgwwCAJMMAgK+hAQCusQEArcEBAKzNAQCo3X0AqQV+AKoBfgCrAX4ArAF+AK0BfgCuAX4ArwF+ANTCAIAowwCALMMAgDDDAIA0wwCAgp0AAIGdAACAnQAAuC1+ALnhfgC64X4Au+F+ALzhfgC94X4AvuF+AL/hfgCwQX4AsU1+ALJZfgCzVX4AtDV+ALUlfgC2JX4AtxV+AKitfwCp0X8AqtF/AKvtfwCs9X8ArRV/AK4RfwCvEX8AOMMAgDzDAIBAwwCARMMAgIbwAwCHuAAASMMAgEzDAIC4EX8AuRl/ALohfwC7IX8AvPUAAL39AAC+9QAAv+0AALBxfwCxcX8AsnF/ALNFfwC0QX8AtU1/ALY9fwC3NX8As1l+AFDDAIBUwwCAWMMAgFzDAIC2lX4AtX1+AGDDAIC7tX4AurV+AGTDAIBowwCAv4l+AL6FfgC9kX4AvKV+AGzDAICjHX4AcMMAgHTDAICm0X4AeMMAgHzDAIClOX4AqvF+AKvxfgCAwwCAhMMAgK7BfgCvzX4ArOF+AK3VfgCwrQAAscUAALLBAACzwQAAtMUAALXNAAC28QAAt/EAALhhAAC5YQAAumEAALt9AAC8ZQAAvW0AAL5lAAC/vQMAiMMAgIzDAICQwwCAZMIAgJTDAICYwwCAnMMAgKDDAICoWQEAqVkBAKrtAACr5QAArP0AAK3lAACu5QAAr9UAAKTDAICCHQAAgR0AAIAdAACowwCArMMAgLDDAIC+VAIAhoAEAIfsAgC4wwCAvMMAgMDDAIDEwwCAyMMAgL54AwDjdH4AzMMAgOG4fQDQwwCA1MMAgNjDAIDcwwCA4MMAgOTDAIDowwCA7MMAgPDDAIDvwH4A9MMAgPjDAID8wwCAs4UDAADEAIAExACACMQAgAzEAIC2hQMAtZUDABDEAIC74QMAuokDAL4kBgAUxACAv+kDAL7hAwC99QMAvPUDAIIpAACjwQMAgB0AAIEVAACmwQMAGMQAgBzEAICl0QMAqs0DAKulAwAgxACAheAFAK6lAwCvrQMArLEDAK2xAwDh+AMAKMQAgONcHwAsxACA7/QDADDEAICGPAcAh6wCAON8fgA0xACA4YABADjEAIA8xACAQMQAgO/kEwBExACAs3EBAEjEAIBMxACAUMQAgFTEAIC2EQEAtWEBAFjEAIC7OQEAujEBAFzEAIBgxACAvxkBAL4RAQC9GQEAvCEBAGTEAIBoxACAbMQAgHDEAIB0xACAeMQAgHzEAIDvxH8AgMQAgOH8fgCExACA4/B/AIANAACBdQAAgn0AAIjEAICMxACAkMQAgKP5AQC+AAgApekBAJjEAICcxACAppkBAISoBQCgxACAq7EBAKq5AQCtkQEArKkBAK+RAQCumQEAqCkGAKkpBgCqOQYAqzkGAKwpBgCtUQYArlUGAK9NBgAkxACAhCABAKTEAICUxACAo+EBAKKZBAChGQQAoPEFALg5BgC5OQYAus0GALvFBgC83QYAvcUGAL7FBgC/8QYAsDUGALE9BgCyNQYAsw0GALQVBgC1HQYAthUGALcJBgCPoWwAs5EHAIYoAQCHfAMAtqEHAKjEAICsxACAtbEHALrlBwC77QcAsMQAgLTEAIC+7QcAv90HALz1BwC97QcAn/l4AJ7leACdcXkAnCF8AJvxfACaYX0AmZlxAJjZcACX4XAAlnl0AJVtdACUbXQAk61pAJJxaACReWgAkB1uAIIhbQCD5W8AuMQAgLzEAICGTWgAh5V1AISZaQCFmWkAiqV1AIu5dQDAxACAxMQAgI5xcACPgXwAjDlxAI05cQCSYX0Ak6l9AMjEAIDMxACAlml5AJeZBACU4XgAlX15AJpBBQCbyQUA0MQAgNTEAIDYxACA3MQAgJypAADgxACAo4ENAKKpAQChqQEA5MQAgKexCQCmAQgApU0NAKSZDQCrkRUAqoUVAKkBFACocQkArx0QAK7pEQCtvREArAEQALMBGACy8RwAscEdALDJHQC0wwCA6MQAgLXhGAC0/RkA7MQAgPDEAID0xACA+MQAgIAdAACBCQAAgv0DAPzEAICjFQUAAMUAgIaIDACHPAMACMUAgKYlBQClNQUADMUAgKtpBQCqYQUAEMUAgBTFAICvWQUArmkFAK1pBQCscQUAGMUAgBzFAICEBAwAIMUAgCTFAIDhbAYAKMUAgOPsewAsxQCAMMUAgDTFAIDvqAYAOMUAgDzFAIBAxQCARMUAgKmNBQCogQUAq60FAKqZBQCtoQUArLkFAK+lBQCuqQUAhGgNAEjFAIBMxQCAUMUAgFTFAIBYxQCAXMUAgL70DAC5SQUAuEEFALtZBQC6QQUAvUkFALxBBQC/cQUAvn0FALGpBQCwoQUAs7kFALKhBQC1mQUAtKkFALd5BQC2kQUAqNUEAKndBACq7QQAqyUDAKyFAwCtjQMArrEDAK+xAwBgxQCAZMUAgGjFAIBsxQCAgBkAAIEZAACCBQAAcMUAgLgxAgC5MQIAujUCALvBAgC8hQIAvbUCAL69AgC/tQIAsGkCALFpAgCyQQIAs0ECALQ5AgC1OQIAthECALcRAgCGoAwAh0wNAHjFAIB8xQCA76QGAIDFAICExQCA78wHAOOUAQDhpAYA4TgBAONcBgCIxQCAjMUAgJDFAICUxQCAmMUAgJzFAICzLQQAoMUAgLVFAwCkxQCAqMUAgLZFAwCsxQCAsMUAgLvlAgC65QIAvd0CALzdAgC/tQIAvrUCAATFAIB0xQCAtMUAgLjFAIC8xQCAwMUAgMTFAIDIxQCAqDEOAKk5DgCqAQ4AqwEOAKxxDgCtcQ4ArnUOAK9tDgCwGQ4AsSUOALItDgCzJQ4AtCEOALUhDgC2IQ4AtyEOALjFDgC5zQ4AusUOALvdDgC8xQ4Avc0OAL5ZDwC/WQ8As6kOAMzFAIDQxQCA1MUAgNjFAIC20Q4AtdkOANzFAIC7wQ4Auv0OAODFAIC+LAAAv8UOAL7FDgC90Q4AvNkOAIJpAACj7Q4AgFkAAIFRAACmlQ4A5MUAgOjFAIClnQ4AqrkOAKuFDgCGyAAAh6wAAK6BDgCvgQ4ArJ0OAK2VDgDsxQCAs5EOAPDFAID0xQCAtqUOAPjFAID8xQCAta0OALrhDgC74Q4AAMYAgATGAIC+6Q4Av9UOALz1DgC96Q4Ao6UKAAjGAIAMxgCAEMYAgBTGAICmzQ0Apc0NABjGAICrbQwAqm0MABzGAIAgxgCArz0MAK49DACtVQwArFUMAKgJDgCpCQ4Aqh0OAKsVDgCsIQ4ArSEOAK4hDgCvIQ4AJMYAgCjGAIAsxgCAMMYAgDTGAIA4xgCAPMYAgEDGAIC4zQEAudUBALrdAQC71QEAvM0BAL1RAQC+UQEAv1EBALAhDgCxIQ4AsiUOALM5DgC0KQ4AtRUOALYdDgC39QEARMYAgEjGAIBMxgCAo5kNAFDGAIClpQ0Apq0NAL7cAgCE7AMAWMYAgKrpDQCr6Q0ArP0NAK3hDQCu4Q0Ar90NAIBFAACBTQAAglkAAKNFAwBcxgCApUEDAKZBAwBgxgCAhsAEAIcAAwCqLQMAqyUDAKw9AwCtJQMAriUDAK8VAwCoWQIAqYUDAKqBAwCrgQMArIUDAK2NAwCusQMAr7EDAGTGAIBoxgCAbMYAgHDGAIB0xgCAeMYAgHzGAICAxgCAuGUDALltAwC6ZQMAu30DALxlAwC9bQMAvmUDAL/dAACwpQMAsa0DALKlAwCzvQMAtK0DALWdAwC2lQMAt10DALMJAgCExgCAiMYAgIzGAICQxgCAtg0CALUNAgCUxgCAu2kCALphAgCYxgCAnMYAgL9ZAgC+aQIAvWkCALxxAgCgxgCApMYAgKjGAICsxgCA4aABALDGAIDjaAMAtMYAgIEVAACAFQAA74wDAIIVAAC4xgCAvMYAgMDGAIC+cAUA4RgOAOGUDwDjOA8A49QPAISUAgDIxgCAzMYAgNDGAIDUxgCA2MYAgNzGAIDgxgCA5MYAgOjGAIDv7AEA7/gPAIZgBACHBAUAs5UBAITMBQC1dQEA7MYAgPDGAIC2dQEA9MYAgPjGAIC7UQEAulkBAL31AAC8SQEAv/UAAL71AACoJQYAqVUGAKpVBgCrrQYArLUGAK29BgCutQYAr60GAMTGAID8xgCAAMcAgATHAIAIxwCADMcAgBDHAIAUxwCAuGkHALlpBwC6CQcAuwkHALwZBwC9GQcAvg0HAL8BBwCw1QYAsd0GALLVBgCzaQcAtHkHALV5BwC2aQcAt2EHAKPdBgAYxwCAHMcAgCDHAIAkxwCApj0GAKU9BgAoxwCAqxkGAKoRBgAsxwCAMMcAgK+9BwCuvQcArb0HAKwBBgCAXQAAgW0AAIJlAACzUQcAvtgDALVxBwC2cQcANMcAgIbgAACHFAMAul0HALs5BwC8KQcAvRUHAL4dBwC/2QAAqJUGAKmdBgCqlQYAq60GAKy1BgCtvQYArrUGAK+tBgA4xwCAPMcAgEDHAIBExwCASMcAgEzHAIBQxwCAVMcAgLhxAQC5cQEAunEBALtxAQC81QEAvd0BAL7VAQC/zQEAsNUGALGxBgCysQYAs40GALSVBgC1UQEAtlEBALdRAQBYxwCAoxkGAFzHAIBgxwCApjkGAFTGAIBkxwCApTkGAKoVBgCrcQYAaMcAgGzHAICuVQYAr5EBAKxhBgCtXQYAcMcAgHTHAIB4xwCAfMcAgIDHAICExwCAiMcAgIzHAICQxwCAlMcAgJjHAICcxwCAgBkAAIEZAACCBQAAoMcAgISAAgC+gAMAhwwDAIasHADhaAYAqMcAgOOYBwCsxwCAsMcAgLTHAIDvrAcAuMcAgLzHAIDAxwCAxMcAgMjHAIDMxwCA0McAgNTHAICzZQMA2McAgLVlAwC2bQMA3McAgODHAIDkxwCAuukDALvlAwC8/QMAve0DAL7RAwC/0QMA6McAgOzHAIDwxwCA9McAgPjHAID8xwCAAMgAgATIAICogQMAqYEDAKqBAwCrgQMArIEDAK2BAwCugQMAr4EDALBBAwCxTQMAskUDALNVAwC0eQMAtXkDALYZAwC3GQMAuCkDALkpAwC6OQMAuzkDALwpAwC9KQMAvhkDAL8ZAwCBGQAAgBEAAKMhAgCCLQAApSECAAjIAIAMyACApikCABDIAIAYyACAq6ECAKqtAgCtqQIArLkCAK+VAgCulQIAhEwCAL5IHQCHZB0AhuwcAONAAwAcyACA4aABACDIAIDvnAMAJMgAgCjIAIAsyACAMMgAgDTIAIA4yACAPMgAgEDIAIBEyACASMgAgEzIAIBQyACAVMgAgFjIAIDvtAEAhKgdAOF8BgBcyACA43AGAGDIAIBkyACAaMgAgGzIAICz4QEAcMgAgHTIAIB4yACAfMgAgLblAQC19QEAgMgAgLuhAQC62QEAvuQcAIjIAIC/rQEAvqUBAL2xAQC8uQEAqBUeAKkZHgCqKR4AqykeAKw9HgCtJR4Ari0eAK8lHgAUyACAgvkfAIH5HwCA4R8AhMgAgIzIAICGHAAAh7ADALjBHgC5wR4AusEeALvBHgC8wR4AvcEeAL7BHgC/wR4AsF0eALElHgCyLR4AsyUeALQhHgC1KR4AthkeALcZHgCjoR4AkMgAgJTIAICYyACAnMgAgKalHgCltR4AoMgAgKvhHgCqmR4ApMgAgKjIAICv7R4AruUeAK3xHgCs+R4ArMgAgLOZHwCwyACAtMgAgLa9HwC4yACAvMgAgLW1HwC6mR8Au5kfAMDIAIDEyACAvnkfAL95HwC8eR8AvXkfAKglHgCpUR4AqlUeAKtpHgCseR4ArXkeAK5pHgCvaR4AyMgAgMzIAIDQyACA1MgAgNjIAIDcyACA4MgAgOTIAIC42R4Aue0eALr5HgC7+R4AvOkeAL3pHgC+nR4Av5UeALAZHgCxGR4AsukeALPpHgC0+R4AtfkeALbpHgC36R4Ao90eAIIpAACBFQAAgB0AAOjIAICm+R4ApfEeAOzIAICr3R4Aqt0eAKTHAIDwyACArz0eAK49HgCtPR4ArD0eAITIAgCzQQEAvgwBAPjIAIC2QQEA/MgAgADJAIC1UQEAuk0BALslAQCGSAAAh1ABAL4lAQC/LQEAvDEBAL0xAQAEyQCACMkAgIQEAwC+gAQADMkAgO+oHwAQyQCAFMkAgL8oMQDjdB8AGMkAgOE4HgAcyQCAIMkAgCTJAIAoyQCALMkAgDDJAICjzQIANMkAgKXdAgA4yQCAPMkAgKbNAgBAyQCARMkAgKupAgCqwQIArb0CAKy9AgCvoQIArqkCAKm1AgCoaR0AqwECAKoJAgCtAQIArBkCAK8xAgCuAQIAhGwFAEjJAIBMyQCAUMkAgFTJAICCnQEAgZ0BAICdAQC55QMAuOUDALvlAwC65QMAveUDALzlAwC/5QMAvuUDALEhAgCwSQIAsyUCALIlAgC1KQIAtCECALcVAgC2FQIAqM0CAKnRAgCq0QIAqw0BAKwVAQCtBQEArgEBAK8BAQBYyQCAXMkAgGDJAIBoyQCAvvgEAGzJAIBwyQCAdMkAgLgVAQC5HQEAuikBALspAQC89QEAvf0BAL71AQC/7QEAsEkBALFVAQCyXQEAs1UBALRNAQC1NQEAtj0BALcxAQCGoAUAh8gFAHjJAIDvvAAAfMkAgIDJAICEyQCA74weAIQsBwDh8B4AiMkAgOMcHgCMyQCA4ZQBAJDJAIDjbAAAsxkCAJTJAICYyQCAnMkAgIQACAC2xQEAtd0BAKDJAIC70QEAus0BAKTJAICoyQCAv7EBAL7JAQC9wQEAvMkBAKPZBQBkyQCArMkAgLDJAIC0yQCApgUGAKUdBgC4yQCAqxEGAKoNBgC8yQCAwMkAgK9xBgCuCQYArQEGAKwJBgDEyQCAgh0AAIEdAACAHQAAyMkAgMzJAIDQyQCA1MkAgIZAAwCHxAMA2MkAgNzJAIDgyQCA5MkAgOjJAIDsyQCAqK0HAKmxBwCqsQcAq7EHAKwZBwCtBQcArg0HAK8FBwDwyQCA9MkAgPjJAID8yQCAAMoAgATKAIAIygCADMoAgLgtBwC5zQAAusUAALvdAAC8zQAAvf0AAL71AAC/nQAAsEkHALFVBwCyUQcAsykHALQ5BwC1OQcAtiUHALcVBwCzOQYAEMoAgBTKAIAYygCAHMoAgLaFBgC1kQYAIMoAgLuRBgC6jQYAJMoAgCjKAIC//QYAvv0GAL39BgC8hQYALMoAgKN9BgAwygCANMoAgKbBBgA4ygCAPMoAgKXVBgCqyQYAq9UGAEDKAIC+bAEArrkGAK+5BgCswQYArbkGAKjpAQCp6QEAqvkBAKv5AQCs6QEArekBAK45AQCvOQEAgPUAAIH9AACCwQAARMoAgIYQAACHdAEASMoAgPTIAIC4zQAAudUAALrVAAC75QAAvP0AAL2VAAC+kQAAv5EAALBJAQCxSQEAslkBALNZAQC0SQEAtUkBALb9AAC39QAA7/QGAEzKAIBQygCAVMoAgO8wAgBYygCAXMoAgGDKAIDj4AcAZMoAgOGAAQBoygCA4ygGAGzKAIDhyAUAcMoAgLMxAgB0ygCAeMoAgJYAAAB8ygCAtikCALUhAgCAygCAu80CALrNAgCEygCAiMoAgL/NAgC+zQIAvc0CALzNAgCMygCAkMoAgJTKAICj/QIAmMoAgKXtAgCm5QIAnMoAgKDKAICkygCAqgECAKsBAgCsAQIArQECAK4BAgCvAQIAgA0AAIEVAACCHQAAqMoAgKzKAICwygCAvlQMALjKAICGwAwAhyQDALzKAIDAygCAxMoAgMjKAIDMygCA0MoAgKi5AgCpAQEAqgEBAKsBAQCsBQEArQ0BAK4FAQCvOQEAhKgNANTKAIDYygCA3MoAgODKAIDkygCA6MoAgOzKAIC4LQEAucUBALrNAQC7xQEAvMEBAL3JAQC++QEAv/kBALBNAQCxUQEAslUBALMpAQC0OQEAtSUBALYlAQC3FQEA4RgGAPDKAIDjOAcA9MoAgPjKAIC+WAwA/MoAgADLAICEbA8ABMsAgL5gDwAIywCADMsAgBDLAIDvcAYAFMsAgIAVAACBGQAAgi0AAITMDwDjYAYAGMsAgOGgAQAcywCA73QAACDLAICGyAwAh/wMACjLAIAsywCAMMsAgDTLAICjCQ4AtMoAgCTLAIA4ywCAPMsAgKYNDgClDQ4AQMsAgKsVDgCqCQ4ARMsAgEjLAICvYQ4Arn0OAK19DgCsAQ4ATMsAgLOpDgBQywCAVMsAgLapDgBYywCAXMsAgLWpDgC6SQ8Au0kPAGDLAIBkywCAvkkPAL9JDwC8SQ8AvUkPAKhdDgCpbQ4AqmUOAKt9DgCsZQ4ArW0OAK5lDgCvuQ8AaMsAgGzLAIBwywCAdMsAgHjLAIB8ywCAgMsAgITLAIC4UQ8AuV0PALpVDwC7aQ8AvH0PAL1lDwC+bQ8Av2EPALDJDwCxyQ8AstkPALPZDwC0yQ8AtckPALZ9DwC3cQ8AiMsAgLURDwC2EQ8AjMsAgIARAACBGQAAgikAALMVDwC8HQ8AvWEPAL5hDwC/fQ8AkMsAgJTLAIC6FQ8AuwkPAKOtDwCYywCAhugAAIfIAQCcywCApq0PAKWtDwCgywCAq00OAKpNDgCkywCAqMsAgK9NDgCuTQ4ArU0OAKxNDgCocQ4AqXEOAKpxDgCrcQ4ArJ0BAK2FAQCuhQEAr7UBAL7sAACsywCAsMsAgLTLAIC4ywCAvMsAgMDLAIDEywCAuGEBALlhAQC6YQEAu2EBALxhAQC9YQEAvmEBAL9hAQCwzQEAsaUBALKhAQCzoQEAtKUBALWtAQC2kQEAt5EBALP5DQDIywCAzMsAgNDLAIDUywCAtgUCALUVAgDYywCAu2ECALoJAgDcywCA4MsAgL9pAgC+YQIAvXUCALx1AgDkywCAo70NAOjLAIDsywCApkECAPDLAID0ywCApVECAKpNAgCrJQIA+MsAgPzLAICuJQIAry0CAKwxAgCtMQIAge0AAIDtAADv0AEAgh0AAADMAIAIzACAhjgEAIdQAwAMzACAEMwAgBTMAIAYzACA4eABABzMAIDjZA8AIMwAgCTMAIAozACALMwAgLORAwAwzACAtbkDALZ9AwA0zACAOMwAgDzMAIC6WQMAu1kDALxJAwC9SQMAvv0AAL/1AACoRQIAqVUCAKpVAgCrZQIArH0CAK2xAgCusQIAr7ECAL5oBQBAzACARMwAgEjMAIBMzACAUMwAgFTMAIBYzACAuF0BALltAQC6ZQEAuw0BALwZAQC9GQEAvg0BAL8FAQCw0QIAsdECALLRAgCz0QIAtHUBALV9AQC2dQEAt20BAOF4DwDjNA4A47gOAOF8DgBczACAYMwAgGTMAIBozACAbMwAgHDMAIB4zACAfMwAgIDMAIDv5A4A79QOAITMAICjnQIAgmEAAIFpAACAUQAAhJwFAKZxAgCltQIAiMwAgKtVAgCqVQIAhkgEAIfMBACv+QEArvEBAK1FAgCsRQIAqJUGAKmlBgCqrQYAq6UGAKy9BgCtoQYArqUGAK/dBgB0zACAjMwAgJDMAICUzACAmMwAgJzMAICgzACApMwAgLhtBwC5dQcAun0HALt1BwC8bQcAvcUHAL7NBwC/xQcAsKUGALGtBgCyuQYAs7EGALSRBgC1kQYAtl0HALdVBwCzJQYAqMwAgKzMAICwzACAtMwAgLYhBgC1NQYAuMwAgLtpBgC6YQYAvMwAgMDMAIC/VQYAvlUGAL1lBgC8bQYAxMwAgKNhBgDIzACAzMwAgKZlBgDQzACA1MwAgKVxBgCqJQYAqy0GANjMAIDczACArhEGAK8RBgCsKQYArSEGAKipBgCpqQYAqrkGAKuxBgCszQYArTEBAK4xAQCvMQEAgMkBAIHJAQCCBQAA4MwAgL54AgCEeAIA5MwAgOjMAIC43QEAue0BALrlAQC7jQEAvJkBAL2ZAQC+jQEAv4UBALBRAQCxUQEAslEBALNRAQC09QEAtf0BALb1AQC37QEAszEGAOzMAICGKAAAh9wBAPDMAIC2sQEAtUUGAPTMAIC7lQEAupUBAPjMAID8zACAvzkBAL4xAQC9hQEAvIUBAATMAICjdQYAAM0AgATNAICm9QEACM0AgAzNAIClAQYAqtEBAKvRAQAQzQCAFM0AgK51AQCvfQEArMEBAK3BAQAYzQCAHM0AgCDNAIAkzQCAKM0AgCzNAIAwzQCANM0AgDjNAIA8zQCAQM0AgETNAIBIzQCATM0AgFDNAIC+cAMAhQA8AOHEBgCERAIA44wHAIBhAACBYQAAgmEAAO9oAwCFRDwA4RACAFjNAIDj2CsAhlA9AIf0AwBczQCA76QHAGDNAIDvQAIAZM0AgGjNAIBszQCAcM0AgHTNAIB4zQCAhDw8AHzNAICAzQCAhM0AgIjNAIDj7AIAjM0AgOEsAQCzUQMAkM0AgJTNAICYzQCAnM0AgLZ5AwC1cQMAoM0AgLs5AwC6MQMApM0AgKjNAIC/9QAAvvUAAL0VAwC8FQMAqD0CAKmBAgCqmQIAq5ECAKy5AgCtuQIArtECAK/RAgCEqD8Avqg/AKzNAICwzQCAtM0AgLjNAIC8zQCAwM0AgLhRAQC5UQEAulEBALtRAQC8cQEAvXEBAL5xAQC/cQEAsLUCALG9AgCygQIAs4ECALRxAQC1cQEAtnEBALdxAQCAtQAAgb0AAIK1AADIzQCAhrA/AIfgPADMzQCA71QAAL4sPgDhVAYA0M0AgOOIAADUzQCA2M0AgNzNAIDgzQCAo1ECAOTNAIC/2CYA6M0AgOzNAICmeQIApXECAPDNAICrOQIAqjECAPTNAID4zQCAr/UBAK71AQCtFQIArBUCAJAtJACRBSgAkg0oAJPZKACUhS0AlTUsAJbFLACXtTEAmAEwAJkVMACalTUAmyk0AJxtNACdmTUAnj04AJ81OABUzQCAttU+ALXFPgDEzQCAs9E+APzNAIAAzgCABM4AgL/ZPgC+1T4AvcU+ALzFPgC71T4Auuk+AAjOAICPXSQAqeUJAKgVCACrBQwAqg0MAK0BEACsAQwAr0EQAK69EACh4QAADM4AgKMBBACi4QAApZ0EAKSVBACnuQgApgEIAKD1OQChBT0Aouk8AKP1PQAQzgCAFM4AgBjOAIAczgCAscEUALABFACzARgAsn0UALXVGAC01RgAIM4AgCTOAICCISUAgyklACjOAIAszgCAhsUpAIeBLACEGSkAhRkpAIoBLQCL+S0AMM4AgDjOAICOATEAj4k0AIyRMACNHTEAkkU1AJMZNQCG6AcAh+wBAJZZOQCXYTgAlPU0AJVZOQCaoTwAm0U9ADzOAIBAzgCAgX0AAIB9AACcQTwAglUAAKjpPwCp/T8Aqgk/AKsFPwCsHT8ArQU/AK4NPwCvBT8ARM4AgEjOAIBMzgCAUM4AgFTOAIBYzgCAXM4AgGDOAIC4DT8AuRU/ALoVPwC7JT8AvD0/AL39PgC+9T4Av+0+ALB9PwCxQT8AskE/ALNBPwC0QT8AtU0/ALY9PwC3NT8Ao4E8AGTOAIBozgCAbM4AgHDOAICmhTwApZU8AHTOAICrhTwAqrk8AHjOAIB8zgCAr4k8AK6FPACtlTwArJU8AITIAwCz7T0AgM4AgITOAIC26T0AiM4AgIzOAIC16T0Auq09ALu1PQCQzgCAlM4AgL6dPQC/IQIAvKU9AL2VPQCoDT0AqR09AKohPQCrPT0ArCU9AK0tPQCuJT0Ar1k9AIANAACBFQAAgh0AAJjOAICczgCAoM4AgKjOAIC+uAMAuLkCALlhAgC6GQIAuxkCALwJAgC9CQIAviECAL8hAgCwLT0AsTU9ALI1PQCzBT0AtB09ALWhAgC2oQIAt6ECAKOpPACszgCAhigFAIfsAgCwzgCApq08AKWtPAC0zgCAq/E8AKrpPAC4zgCAvM4AgK9lAwCu2TwArdE8AKzhPADAzgCAsykCAMTOAIDIzgCAtvkCAMzOAIDQzgCAtfkCALrVAgC73QIA1M4AgNjOAIC+eQEAv3kBALzFAgC9eQEA3M4AgODOAICj5QIA5M4AgKU1AgDozgCA7M4AgKY1AgDwzgCA9M4AgKsRAgCqGQIArbUBAKwJAgCvtQEArrUBAOPwPgDhrD8A4UA+AON8PwD4zgCA/M4AgADPAIAEzwCAgA0AAIERAACCEQAACM8AgO+oPgAMzwCAEM8AgO8gPgCoLQUAqW0FAKplBQCrrQUArLUFAK29BQCutQUAr60FAKTOAICE6AMAvuADABTPAICGEAMAh5gDABjPAIAczwCAuGkGALlpBgC6AQYAuwEGALwFBgC9DQYAvjEGAL8xBgCw1QUAsd0FALLVBQCzaQYAtHkGALV5BgC2aQYAt2EGAKg5BgCpgQcAqpkHAKuRBwCsuQcArbkHAK7ZBwCv1QcAIM8AgCTPAIA0zgCAKM8AgCzPAIAwzwCANM8AgDjPAIC4VQcAuV0HALppBwC7aQcAvAEHAL0BBwC+AQcAvwEHALCtBwCxsQcAsrEHALOFBwC0nQcAtXUHALZ9BwC3cQcAsxEGADzPAIBAzwCARM8AgEjPAIC2OQYAtTEGAEzPAIC7dQYAumkGAFDPAIBUzwCAv7EGAL5ZBgC9UQYAvGUGAFjPAICjVQYAXM8AgGDPAICmfQYAZM8AgGjPAICldQYAqi0GAKsxBgBszwCAcM8AgK4dBgCv9QYArCEGAK0VBgCouQEAqbkBAKopAQCrKQEArD0BAK0lAQCuLQEAryUBAHTPAICCHQAAgR0AAIAdAAB4zwCAfM8AgIDPAIC+cAEAuIEAALmNAAC6hQAAu5kAALyJAAC9vQAAvrUAAL99AACwXQEAseEAALLhAACz4QAAtOEAALXpAAC20QAAt9EAAITIAgCzpQIAhzgDAIYoAgC2oQIAiM8AgIzPAIC1sQIAup0CALshAwC+bAMAkM8AgL4hAwC/KQMAvDEDAL0xAwCj4QIAlM8AgJjPAICczwCAoM8AgKblAgCl9QIApM8AgKtlAwCq2QIAqM8AgKzPAICvbQMArmUDAK11AwCsdQMAqZkAAKiRAACrzQAAqqEAAK3dAACs3QAAr8UAAK7NAAC+LA0AsM8AgLTPAIC4zwCAvM8AgMDPAIDEzwCAyM8AgLnBAQC4eQAAu8EBALrJAQC9wQEAvNkBAL/FAQC+xQEAsY0AALCNAACzQQAAskkAALVBAAC0WQAAt0EAALZJAADMzwCA0M8AgNTPAIDYzwCA3M8AgO9QBwDgzwCA5M8AgL74DwDjdAcA6M8AgOF8BACAGQAAgQkAAIJ5AADszwCA8M8AgLNpAQD4zwCAhMQCALYdAQD8zwCAANAAgLUVAQC6CQEAuwkBAIboDQCH6A0Avt0BAL/FAQC83QEAvdUBAATQAIAI0ACADNAAgBDQAIDv1AAAFNAAgBjQAIDvTAEA47ADAOG0BgDhgAEA45gBABzQAIAg0ACAJNAAgCjQAIAs0ACAMNAAgKPlAQCEwA0ApZkBADTQAIA40ACAppEBADzQAIBA0ACAq4UBAKqFAQCtWQEArFEBAK9JAQCuUQEA9M8AgETQAIBI0ACATNAAgFDQAIBU0ACAWNAAgFzQAICoaQ8AqXEPAKpxDwCrrQ8ArLUPAK29DwCutQ8Ar6kPALDZDwCx9Q8Asv0PALP1DwC07Q8AtZUPALadDwC3iQ8AuLkPALmFDwC6jQ8Au2kAALx5AAC9eQAAvmkAAL9pAACBnQAAgJ0AAGDQAICCBQAAZNAAgGjQAIBs0ACAcNAAgIaAAwCH9AMAdNAAgHjQAIB80ACAgNAAgITQAICEzwCAs5kPAIjQAICM0ACAkNAAgJTQAIC2XQ8AtV0PAJjQAIC7UQ8Aun0PAJzQAICg0ACAvzEPAL5JDwC9QQ8AvEkPAKNZDgCk0ACAqNAAgKzQAICw0ACApp0OAKWdDgC00ACAq5EOAKq9DgC40ACAvNAAgK/xDgCuiQ4ArYEOAKyJDgDA0ACAxNAAgMjQAIDM0ACAgBkAAIEZAACCBQAA0NAAgISgAQDU0ACAh+gBAIYABADY0ACA3NAAgODQAIDk0ACAqBUBAKkdAQCqFQEAqyUBAKw9AQCtJQEAri0BAK8lAQDo0ACA7NAAgPDQAID00ACA+NAAgPzQAIAA0QCABNEAgLjJAAC5yQAAutkAALvRAAC8+QAAvfkAAL6ZAAC/mQAAsCUBALEtAQCyJQEAsz0BALQtAQC1HQEAthUBALf5AAAI0QCADNEAgBDRAICzkQIAFNEAgLW5AgC2qQIAGNEAgBzRAIAg0QCAuu0CALvlAgC8/QIAveUCAL7lAgC/1QIApvECACTRAIAo0QCApeECACzRAICjyQIAMNEAgDTRAICuvQIAr40CAKylAgCtvQIAqrUCAKu9AgA40QCAPNEAgID5AACB+QAAggUAAEDRAIC+yAMAhBgDAEjRAIBM0QCAUNEAgFTRAIBY0QCAXNEAgGDRAIBk0QCAhhgEAIecAwBo0QCAbNEAgHDRAIB00QCAeNEAgHzRAIDvsAIAgNEAgOGUAQCE0QCA42wCAIjRAICM0QCAkNEAgJTRAICY0QCA79APAJzRAICg0QCApNEAgKjRAIDhrAEArNEAgONsAACAMQAAgT0AAIIdAADv9A4A42wOALDRAIDhLA8AvnAFALM5AgCEDAUAhugEAIdgBQDcAAAAtvECALX5AgC40QCAu9UCALrVAgC80QCAwNEAgL91AQC+dQEAvcUCALzFAgDE0QCA4fQOAMjRAIDjUA4AzNEAgNDRAIDU0QCA2NEAgNzRAIDg0QCA5NEAgOjRAIDs0QCA8NEAgPTRAIDv5A8ApmUCAPjRAID80QCApW0CAADSAICjrQIABNIAgAjSAICu4QEAr+EBAKxRAgCtUQIAqkECAKtBAgAM0gCAENIAgKiZBgCpmQYAqqkGAKupBgCsuQYArbkGAK6pBgCvqQYAFNIAgIIdAACBHQAAgB0AABjSAIAc0gCAINIAgL50AwC4rQYAubUGALq9BgC7tQYAvK0GAL1RBwC+UQcAv1EHALChBgCxoQYAsqEGALOhBgC0oQYAtaEGALalBgC3mQYARNEAgLMlBgCExAMAtNEAgLY9BgAk0gCAKNIAgLU1BgC6YQYAu2EGAIYIAACHiAAAvmEGAL9hBgC8cQYAvXEGAKNhBgAs0gCAMNIAgDTSAIA40gCApnkGAKVxBgA80gCAqyUGAKolBgBA0gCARNIAgK8lBgCuJQYArTUGAKw1BgCoXQYAqW0GAKplBgCrjQYArJkGAK2FBgCujQYAr4UGAEjSAIBM0gCAUNIAgFTSAIBY0gCAXNIAgGDSAIBk0gCAuIUGALmNBgC6mQYAu5UGALyNBgC9rQYAvqUGAL99AQCw/QYAscUGALLNBgCzxQYAtN0GALXFBgC2zQYAt8UGALPtBgBo0gCAbNIAgHDSAIB00gCAtgUGALURBgB40gCAuwEGALo5BgB80gCAgNIAgL8BBgC+GQYAvREGALwZBgCE0gCAo6kGAIjSAICM0gCApkEGAJDSAICElAEApVUGAKp9BgCrRQYAvqABAJjSAICuXQYAr0UGAKxdBgCtVQYAqJkCAKnBAgCqwQIAq8ECAKzBAgCtyQIArvECAK/xAgCB7QMAgO0DAJzSAICC+QMAhpAcAId0AwCg0gCApNIAgLjFAwC5zQMAusUDALvdAwC8zQMAvf0DAL71AwC/nQMAsEEDALFBAwCyQQMAs0EDALRBAwC1QQMAtkEDALdBAwCzSQIAqNIAgKzSAICw0gCAtNIAgLZJAgC1SQIAuNIAgLuFAwC6hQMAvNIAgMDSAIC/hQMAvoUDAL2VAwC8lQMAxNIAgKMNAgDI0gCAzNIAgKYNAgDQ0gCA1NIAgKUNAgCqwQMAq8EDANjSAIDc0gCArsEDAK/BAwCs0QMArdEDAOOYAQDhpAcA4VgGAONYBgDhoAEA4NIAgOPQAADk0gCA6NIAgOzSAIDvOAAA8NIAgO/0AQD00gCA+NIAgO/4BgCAeQAAgRUAAIIdAACEAB0A/NIAgADTAIC+EB0ACNMAgIbAHACHrB0ADNMAgBDTAIAU0wCAGNMAgBzTAIAg0wCAu8UFALqhBQC5qQUAuJEFAL/NBQC+zQUAvckFALzVBQCzHQYAsh0GALEdBgCwHQYAt6EFALa9BQC1vQUAtL0FAKu9BgCqvQYAqb0GAKi9BgCvfQYArn0GAK19BgCsfQYAJNMAgCjTAIAs0wCAMNMAgDTTAIA40wCAPNMAgEDTAICo7R0AqS0eAKoxHgCrMR4ArJUeAK2dHgCulR4Ar40eAATTAIBE0wCASNMAgEzTAIBQ0wCAVNMAgFjTAIBc0wCAuKkeALmpHgC6XR8Au1EfALxxHwC9cR8AvnUfAL9pHwCw/R4Asc0eALLFHgCzrR4AtLkeALW5HgC2rR4At6UeALO5HgBg0wCAZNMAgGjTAICU0gCAth0eALUdHgBs0wCAuwkeALo5HgBw0wCAhOADAL99HgC+fR4AvXkeALwRHgCCaQAAo/0eAIBFAACBUQAAplkeAL6cAwB00wCApVkeAKp9HgCrTR4AhkgAAIdsAACuOR4ArzkeAKxVHgCtPR4AqF0eAKltHgCqZR4Aq30eAKxlHgCtbR4ArmUeAK/9HgB40wCAfNMAgIDTAICE0wCAiNMAgIzTAICQ0wCAlNMAgLhpAQC5aQEAunkBALt5AQC8aQEAvWkBAL7dAQC/1QEAsIUeALGNHgCyhR4As50eALSFHgC1jR4AtoUeALdZAQCz7R4AmNMAgJzTAICg0wCApNMAgLbtHgC17R4AqNMAgLtJHgC6QR4ArNMAgLDTAIC/SR4AvkEeAL1JHgC8UR4AtNMAgKOpHgC40wCAvNMAgKapHgDA0wCAxNMAgKWpHgCqBR4Aqw0eAMjTAIDM0wCArgUeAK8NHgCsFR4ArQ0eAKghAwCpIQMAqiEDAKshAwCsIQMArSEDAK4hAwCvIQMA0NMAgNTTAIDY0wCAvmACANzTAIDg0wCA6NMAgOzTAIC4iQMAuYkDALqdAwC7lQMAvLkDAL25AwC+eQAAv3kAALDlAwCx7QMAsuUDALP9AwC07QMAtd0DALbVAwC3vQMAgKkAAIG1AACCvQAAs6UDAPDTAIC1pQMAtq0DAPTTAICE4AIA+NMAgLotAwC7JQMAvD0DAL0lAwC+JQMAvxUDAKPpAwD80wCAhmgEAIeAAwAA1ACApuEDAKXpAwAE1ACAq2kDAKphAwAI1ACADNQAgK9ZAwCuaQMArWkDAKxxAwAQ1ACAFNQAgBjUAIAc1ACAINQAgOE8HwAk1ACA40AeACjUAIAs1ACAMNQAgO+MHgA01ACAONQAgDzUAIBA1ACARNQAgIIlAACBEQAAgB0AAEjUAIDj5AMATNQAgOGsAQBQ1ACA77ADAIRkAgC+YAUAhtAEAIdEBQBY1ACAXNQAgGDUAIBk1ACAaNQAgGzUAIBw1ACAdNQAgHjUAIDvsAEAhKQFAOHcHgB81ACA4xABAIDUAICE1ACAiNQAgIzUAICzUQEAkNQAgJTUAICY1ACAnNQAgLYRAQC1fQEAoNQAgLsNAQC6DQEApNQAgKjUAIC//QAAvv0AAL39AAC8/QAAqDkGAKk5BgCqmQYAq5EGAKy1BgCt0QYArskGAK/BBgBU1ACArNQAgLDUAIC01ACAgA0AAIGxAACCsQAAuNQAgLhhBwC5YQcAumEHALt9BwC8ZQcAvW0HAL5lBwC/HQcAsIkGALGJBgCyaQcAs2kHALR5BwC1eQcAtmkHALdlBwCjEQYAvNQAgMDUAIC+gAMAxNQAgKZRBgClPQYAyNQAgKtNBgCqTQYAhggAAId8AwCvvQcArr0HAK29BwCsvQcAzNQAgNDUAICzSQcA1NQAgLVZBwDY1ACA3NQAgLZRBwDg1ACA5NMAgLtBBwC6dQcAvUUHALxFBwC/RQcAvkUHAKh5BgCpeQYAqokGAKuJBgCsmQYArZkGAK6JBgCviQYA5NQAgOjUAIDs1ACA8NQAgPTUAID41ACA/NQAgADVAIC4jQYAuZUGALqVBgC7pQYAvL0GAL1xAQC+cQEAv3EBALD5BgCxzQYAstkGALPZBgC0yQYAtckGALa9BgC3tQYAowEGAATVAIAI1QCADNUAgBDVAICmGQYApREGABTVAICrCQYAqj0GABjVAIAc1QCArw0GAK4NBgCtDQYArA0GACDVAIAk1QCAKNUAgCzVAICAGQAAgRkAAIIFAAAw1QCAhKwBAL6sAQCH6AAAhkwPADjVAIA81QCAQNUAgETVAIConQIAqcUCAKrNAgCrwQIArMUCAK3NAgCu+QIArz0DAEjVAIBM1QCAUNUAgFTVAIC+PAwAWNUAgFzVAIBg1QCAuMkDALnJAwC62QMAu9EDALz5AwC9+QMAvpkDAL+ZAwCwRQMAsU0DALJFAwCzXQMAtEUDALVNAwC2RQMAt/kDALNFAgBk1QCAaNUAgGzVAIBw1QCAtk0CALVNAgB01QCAu4kDALqBAwB41QCAfNUAgL+JAwC+gQMAvYkDALyRAwCA1QCAowECAITVAICI1QCApgkCAIzVAICQ1QCApQkCAKrFAwCrzQMAlNUAgJjVAICuxQMAr80DAKzVAwCtzQMAgO0BAIEVAACCEQAAhAACAJzVAIDhpAEAoNUAgOPsAACo1QCArNUAgLDVAIDvMAAAtNUAgLjVAIC81QCAwNUAgIbgDACH9AIAxNUAgMjVAIDM1QCA0NUAgO/MBgDU1QCA4bAHANjVAIDjEAYA3NUAgODVAIDk1QCA6NUAgOzVAIDw1QCA9NUAgPjVAID81QCAANYAgATWAIAI1gCA7+gBAIUYDwDhzAYADNYAgOMcBgCAKQAAgR0AAIIFAAAQ1gCAszkCAITMDQCGaA8Ah/wMAOHQ0gO28QEAtfkBABjWAIC72QEAutEBAL7kDAAc1gCAv30BAL59AQC9fQEAvMEBAKjxDQCp8Q0AqvENAKvxDQCsMQ4ArTEOAK4xDgCvMQ4ApNUAgBTWAIAg1gCAJNYAgCjWAIAs1gCAMNYAgDTWAIC46Q4AuekOALqJDgC7hQ4AvJ0OAL2BDgC+gQ4Av7UOALBVDgCxXQ4AslUOALPpDgC0+Q4AtfkOALbpDgC34Q4Ao3kNADjWAIA81gCAQNYAgETWAICmsQ4ApbkOAEjWAICrmQ4AqpEOAEzWAIBQ1gCArz0OAK49DgCtPQ4ArIEOAFTWAICz7Q8AWNYAgFzWAIC26Q8AYNYAgGTWAIC16Q8Auq0PALu1DwA01QCAaNYAgL6VDwC/mQ8AvK0PAL2hDwCoIQ4AqSEOAKohDgCrPQ4ArCUOAK0tDgCuJQ4Ar1UOAGzWAIBw1gCAdNYAgHjWAICAHQAAgQkAAIK9AAB81gCAuDkOALk5DgC6yQ4Au8kOALzZDgC92Q4AvskOAL/JDgCwLQ4AsTUOALI9DgCzMQ4AtBUOALUZDgC2CQ4AtwkOAKOpDgCA1gCAhIACAL6AAQCFAAQApq0OAKWtDgCI1gCAq/EOAKrpDgCGKAcAhxgAAK/dDgCu0Q4AreUOAKzpDgCM1gCAs+0BAJDWAICU1gCAtuUBAJjWAICc1gCAte0BALplAQC7bQEAoNYAgKTWAIC+bQEAv10BALx1AQC9bQEAqN0NAKnpDQCqIQIAqyECAKwhAgCtIQIAriECAK8hAgCo1gCArNYAgLDWAIC01gCAohECAKMRAgCgqQ4AodUCALiJAgC5iQIAup0CALuVAgC8vQIAvXUDAL59AwC/dQMAsOUCALHtAgCy5QIAs/0CALTtAgC13QIAttUCALe9AgCjqQIAj8UaALjWAIC81gCAwNYAgKahAgClqQIAxNYAgKspAgCqIQIAyNYAgMzWAICvGQIArikCAK0pAgCsMQIAniUOAJ/lDgCc6QoAnRUKAJpFFgCbRQoAmFkWAJlRFgCWcRIAl4ETAJRVEgCV7RIAktEeAJPZHgCQtRoAkVUeAISpHwCFJR8AhiUfAIexEwDQ1gCA1NYAgIJZGwCDURsAjEUSAI2lFwCOpRcAj7kXAIA5+wHY1gCAijkTAIutEwCUmQsAlaEPAJZpDwCX3Q8A3NYAgO+cDwCSyQsAk30LAJxFAwDjeA4A4NYAgOGYDADk1gCAhHgCAJqRAwCbXQMA4QQAAL6IBQDj3OoD6NYAgOzWAIDw1gCA7+wAAO+MDgDhcA4A4fwOAOMwAADjeA4AgSEAAIA5AADvtO0DgikAALMJAgD41gCAhmgEAIcsBQD81gCAtg0CALUNAgAA1wCAu8UBALrFAQAE1wCACNcAgL99AQC+fQEAvdUBALzVAQCE1gCA9NYAgAzXAIAQ1wCAFNcAgBjXAIAc1wCAINcAgKi9BQCp5QUAquEFAKvhBQCs5QUAre0FAK7RBQCv0QUAsGEGALFhBgCyYQYAs2EGALTZBgC12QYAtskGALfBBgC4yQYAuckGALp5BwC7eQcAvEUHAL0lBwC+EQcAvw0HAKNJBQAk1wCAKNcAgCzXAIAw1wCApk0FAKVNBQA01wCAq4UGAKqFBgA41wCAPNcAgK89BgCuPQYArZUGAKyVBgBA1wCARNcAgEjXAIBM1wCAUNcAgFTXAIBY1wCAXNcAgIA5AACBOQAAggUAAGDXAIC+uAMAhLgDAGjXAIBs1wCAqMUGAKnVBgCq1QYAq+UGAKz9BgCtHQEArhUBAK8NAQBk1wCAcNcAgIaIAQCHHAEAdNcAgHjXAIB81wCAgNcAgLjpAQC56QEAuokBALuJAQC8mQEAvZkBAL6JAQC/iQEAsHUBALF9AQCydQEAs+kBALT5AQC1+QEAtukBALfhAQCzXQYAhNcAgIjXAICM1wCAhLwBALadAQC1dQYAkNcAgLu5AQC6sQEAlNcAgJjXAIC/PQEAvj0BAL09AQC8oQEAnNcAgKMZBgCg1wCApNcAgKbZAQCo1wCArNcAgKUxBgCq9QEAq/0BALDXAIC01wCArnkBAK95AQCs5QEArXkBAKj5AgCp+QIAqi0DAKs9AwCsJQMArS0DAK4lAwCvmQMAuNcAgLzXAIDA1wCAxNcAgIANAACBsQAAgrEAAMjXAIC4lQMAuZ0DALqhAwC7oQMAvHEAAL1xAAC+cQAAv3EAALDpAwCx6QMAsvUDALPFAwC03QMAtbUDALaxAwC3sQMAvswDAMzXAIDQ1wCA2NcAgNzXAIDg1wCA5NcAgO/kAgDo1wCA4ZQBAOzXAIDjLAEA8NcAgPTXAICHGAMAhhz8A7tNAwC6TQMA+NcAgPzXAIC/EQMAvnkDAL1xAwC8QQMAs8UDAITo/AMA2ACABNgAgAjYAIC2zQMAtc0DAAzYAICkAfwDpSX/A6bZ/wOnAfgDENgAgKEVAwCiHQMAoz0CAKwR9wOtAfADri3zA68B8wOoEfsDqZn7A6oB9AOrHfcDtAHoA7Vl6wO+xPwDhMT8A7AB7AOxVe8Dsk3vA7Nx7gMU2ACAGNgAgBzYAIAg2ACAJNgAgCjYAIAs2ACAMNgAgOFQBgDhNAQA42wBAOPoBgA02ACAONgAgDzYAIBA2ACAgDUAAIE9AACCNQAASNgAgEzYAIBQ2ACA77ABAO/ABgCj5QIAVNgAgIbo/AOHfP0DWNgAgKbtAgCl7QIAXNgAgKttAgCqbQIAYNgAgGTYAICvMQIArlkCAK1RAgCsYQIAqI3+A6mV/gOqnf4Dq5X+A6yx/gOtvf4Drqn+A6+p/gNE2ACAaNgAgGzYAIBw2ACAdNgAgHjYAIB82ACAgNgAgLgl/wO5Lf8DuiX/A7s9/wO8Jf8DvS3/A74l/wO/zf8DsKn+A7Gp/gOygf4Ds4H+A7SB/gO1if4Dtmn/A7cd/wOE2ACA4SD8A4jYAIDjePwDjNgAgJDYAICU2ACAmNgAgJzYAICg2ACApNgAgKjYAICAHQAAgXEAAIJxAADvDP0Ds1X+A6zYAICw2ACAvkAAALTYAIC2ff4DtXn+A7jYAIC7Lf4Dui3+A4boAACHrAAAvw3+A74F/gO9Ff4DvBX+A6OV/wO82ACAwNgAgMTYAIDI2ACApr3/A6W5/wPM2ACAq+3/A6rt/wPQ2ACA1NgAgK/N/wOuxf8DrdX/A6zV/wPY2ACAs/H+A9zYAIDg2ACAto3+A+TYAIDo2ACAtY3+A7pFAQC7TQEA7NgAgPDYAIC+RQEAv00BALxVAQC9TQEAqC3+A6k1/gOqPf4Dq0n+A6xB/gOtSf4DrnH+A69x/gP02ACA+NgAgPzYAIAA2QCABNkAgAjZAIAM2QCAENkAgLhJAQC5VQEAul0BALtVAQC8TQEAvXUBAL59AQC/dQEAsMUBALHNAQCyxQEAs90BALTFAQC1zQEAtsUBALd9AQCjtf0DFNkAgBjZAICExAMAHNkAgKbJ/QOlyf0DINkAgKsJAgCqAQIAKNkAgL7sAgCvCQIArgECAK0JAgCsEQIAgEkAAIFVAACCVQAAo0UDACzZAIClRQMApkUDADDZAICGwAQAhxQDAKopAwCrJQMArD0DAK0hAwCuIQMArxUDADTZAIA42QCAPNkAgEDZAIBE2QCASNkAgEzZAIBQ2QCAqH0CAKmhAwCqoQMAq6EDAKyhAwCtqQMArpEDAK+RAwCwgQMAsY0DALKFAwCzmQMAtIkDALW9AwC2tQMAt30DALhFAwC5TQMAukUDALtdAwC8RQMAvU0DAL5FAwC/+QAA1NcAgLMNAgBU2QCAWNkAgLYNAgBc2QCAYNkAgLUNAgC6YQIAu20CAGTZAIBo2QCAvmkCAL9dAgC8dQIAvWkCAGzZAIBw2QCAdNkAgHjZAIB82QCA4aQBAIDZAIDjQAMAhNkAgIjZAICM2QCA77gDAIAVAACBHQAAggUAAJDZAICEgAIAvsgFAIcYBQCGLAQAmNkAgJzZAICg2QCA76gBAKTZAIDhdP4DqNkAgOPw/gOs2QCAsNkAgLTZAIC42QCAvNkAgMDZAIDE2QCAs5EBAMjZAIC1UQEAtlEBAMzZAIDQ2QCA1NkAgLp9AQC7dQEAvG0BAL39AAC+9QAAv+kAAKgpBgCpVQYAqlUGAKuNBgCslQYArZ0GAK6VBgCvjQYAlNkAgNjZAIDc2QCA4NkAgOTZAIDo2QCA7NkAgPDZAIC4bQcAuQUHALoNBwC7BQcAvB0HAL0FBwC+AQcAvz0HALD1BgCx/QYAsvUGALNlBwC0fQcAtWEHALZhBwC3VQcA4xAFAPTZAIDh8AQA+NkAgIAdAACBCQAAgjkAAPzZAIAA2gCAhOgDAL7gAwAE2gCA78wFAAjaAICHOAAAhhgAAKOdBgAM2gCAENoAgBTaAIAY2gCApl0GAKVdBgAc2gCAq3kGAKpxBgAg2gCAJNoAgK/lBwCu+QcArfEHAKxhBgCokQYAqZEGAKqRBgCrrQYArLkGAK2lBgCurQYAr6UGACjaAIAs2gCAMNoAgDTaAIA42gCAPNoAgEDaAIBE2gCAuGUBALltAQC6ZQEAu30BALxlAQC9bQEAvmUBAL/ZAQCw3QYAsaUGALKtBgCzpQYAtKEGALWpBgC2mQYAt5kGALMZBgBI2gCATNoAgFDaAIBU2gCAtiUGALUxBgBY2gCAu2EGALoZBgBc2gCAYNoAgL9tBgC+ZQYAvXEGALx5BgBk2gCAo10GAGjaAIBs2gCApmEGAHDaAICEmAEApXUGAKpdBgCrJQYAvqQBAHjaAICuIQYArykGAKw9BgCtNQYAqcUCAKixAgCrxQIAqsUCAK3NAgCsxQIAr/UCAK71AgB82gCAgNoAgITaAICI2gCAjNoAgJDaAICU2gCAmNoAgLnJAwC4wQMAu9kDALrBAwC9+QMAvMkDAL+ZAwC+8QMAsUUDALBFAwCzRQMAskUDALVFAwC0RQMAt0UDALZFAwCASQMAgUkDAIJdAwCzRQIAvtwMALVFAgC2RQIAnNoAgIYADACH5AMAuokDALuJAwC8mQMAvZkDAL6JAwC/iQMAowkCAKDaAICk2gCAqNoAgKzaAICmCQIApQkCALDaAICrxQMAqsUDALTaAIC42gCAr8UDAK7FAwCt1QMArNUDALzaAIDA2gCAxNoAgCTZAIDvAAAAyNoAgMzaAIDQ2gCA4+gAANTaAIDhjAEA2NoAgNzaAIDg2gCA6NoAgOzaAICAbQAAgXUAAIJ9AACEQAIAhvAMAId4DQDw2gCA9NoAgPjaAID82gCAANsAgATbAIAI2wCADNsAgBDbAIAU2wCAGNsAgBzbAIAg2wCAJNsAgCjbAIAs2wCAMNsAgO/MAQCE7AwA4TAGADTbAIDjGAEAONsAgDzbAIBA2wCARNsAgLPlAQBI2wCAhIQPAEzbAIBQ2wCAtuUBALX1AQBY2wCAu30BALrZAQC+oAwAXNsAgL8hAQC+OQEAvTEBALw5AQCo7Q0AqSUOAKotDgCrJQ4ArD0OAK0lDgCuLQ4AryUOAOTaAICC9Q8AgeUPAIDpDwBU2wCAYNsAgIaYAACHDAMAuK0OALlFDwC6TQ8Au0UPALxFDwC9TQ8AvkUPAL95DwCwXQ4AsfkOALKtDgCzpQ4AtL0OALWlDgC2pQ4At5UOAGTbAIDv7AwAaNsAgGzbAIBw2wCAdNsAgHjbAIB82wCAvugAAIDbAICE2wCAiNsAgIzbAIDj6A0AkNsAgOEEDACj5Q4AlNsAgJjbAICc2wCAoNsAgKblDgCl9Q4ApNsAgKt9DgCq2Q4AqNsAgKzbAICvIQ4ArjkOAK0xDgCsOQ4AqDkOAKk5DgCqUQ4Aq1EOAKxxDgCtcQ4ArnEOAK9xDgCw2wCAtNsAgLjbAIC82wCAgBkAAIEZAACCBQAAwNsAgLjRDgC50Q4AutEOALvlDgC84Q4AveEOAL7hDgC/4Q4AsBEOALERDgCyEQ4AsxEOALTxDgC18Q4AtvEOALfxDgCz2Q4AyNsAgIYoAACHuAAAzNsAgLbxDgC1+Q4A0NsAgLvVDgC61Q4A1NsAgNjbAIC/NQ4AvjUOAL3FDgC8xQ4A3NsAgKOdDgDg2wCA5NsAgKa1DgDo2wCA7NsAgKW9DgCqkQ4Aq5EOAPDbAID02wCArnEOAK9xDgCsgQ4ArYEOAKjdDQCp6Q0Aqj0CAKuNAgCsmQIArZkCAK6JAgCviQIAvqwEAPjbAID82wCAhCADAADcAIAE3ACACNwAgAzcAIC4iQIAuYkCALqZAgC7kQIAvLkCAL25AgC+eQMAv3kDALD5AgCx+QIAss0CALPFAgC03QIAtcUCALbBAgC3uQIAs7UCABDcAIAU3ACAGNwAgBzcAIC2GQIAtRECACDcAIC7PQIAuj0CACTcAIAo3ACAvwECAL4ZAgC9EQIAvBkCACzcAICj8QIAMNwAgDjcAICmXQIAPNwAgEDcAIClVQIAqnkCAKt5AgCGSAUAh6wEAK5dAgCvRQIArF0CAK1VAgCohQIAqZUCAKqVAgCrpQIArL0CAK3VAgCu0QIAr9ECAETcAIBI3ACATNwAgFDcAICB8QEAgJkBAHTaAICC9QEAuHkBALl5AQC6zQEAu8UBALzdAQC9xQEAvsUBAL/1AQCwtQIAsb0CALKBAgCzgQIAtFUBALVdAQC2SQEAt0kBAFTcAIBY3ACAXNwAgO/UAQCEEAUAYNwAgGTcAIDvjA4AvuwFAOHsDgBo3ACA4xwOAGzcAIDhlAEAcNwAgONkDgCzXQIAdNwAgHjcAIB83ACAgNwAgLYVAgC1dQIAhNwAgLs5AgC6MQIAiNwAgIzcAIC/2QEAvtEBAL0VAgC8FQIAo50FADTcAICQ3ACAlNwAgJjcAICm1QUApbUFAJzcAICr+QUAqvEFAKDcAICk3ACArxkGAK4RBgCt1QUArNUFAIBRAACBWQAAgmEAALOVBgCo3ACAtXEHALZxBwCs3ACAhkADAIdUAwC67QcAu+UHALzlBwC97QcAvtEHAL/NBwCw3ACAtNwAgLjcAIC83ACAwNwAgMTcAIDvQAQAyNwAgOEwBwDM3ACA45QEANDcAIDU3ACA2NwAgNzcAIDg3ACAoxkGAOTcAIDo3ACA7NwAgPDcAICm/QcApf0HAPTcAICraQcAqmEHAPjcAID83ACAr0EHAK5dBwCtYQcArGkHAKjNBwCp0QcAqtEHAKstBgCsNQYArT0GAK41BgCvnQYAAN0AgATdAIAI3QCADN0AgIAZAACBGQAAggUAABDdAIC4iQYAuYkGALqZBgC7kQYAvLkGAL25BgC+UQEAv1EBALDlBgCx7QYAsv0GALP1BgC02QYAtcUGALbBBgC3uQYAqNEBAKnZAQCqCQEAqwkBAKwZAQCtGQEArgkBAK8JAQCEYAEAvnwBAIeoAACGjAEAGN0AgBzdAIAg3QCAJN0AgLgJAQC5CQEAuhkBALsRAQC8OQEAvTkBAL75AAC/+QAAsH0BALFBAQCyRQEAs10BALRFAQC1TQEAtkUBALc5AQAo3QCALN0AgDDdAICzjQIANN0AgLWdAgC2lQIAON0AgDzdAIBA3QCAurUCALuJAgC8nQIAvYUCAL6NAgC/hQIAps0CAETdAIBI3QCApcUCAEzdAICj1QIAUN0AgFTdAICu1QIAr90CAKzFAgCt3QIAqu0CAKvRAgCE9AMAWN0AgKgxAwCpMQMAqjEDAKsxAwCskQAArZEAAK6RAACvjQAAXN0AgGDdAIBk3QCAaN0AgGzdAIBw3QCAdN0AgHjdAIC4vQAAuWUAALptAAC7ZQAAvH0AAL1lAAC+bQAAv2UAALD9AACxxQAAss0AALOpAAC0uQAAtaUAALahAAC3oQAAgL0BAIEJAACCGQAAfN0AgIDdAIC+WAIAhxQdAIacHQCEbB0AxNsAgIjdAICM3QCAvrwcAJDdAICU3QCAmN0AgLP5AgCc3QCAoN0AgKTdAICo3QCAtlEBALVZAQC+3B8Au0EBALp5AQCs3QCAsN0AgL8hAQC+PQEAvT0BALxZAQDhcAcAtN0AgOMIBgC43QCA78wAALzdAIDA3QCAxN0AgOMQAADI3QCA4dABAMzdAICGkBwAh/QcAO/gBgDQ3QCAo3kCANTdAIDY3QCA3N0AgODdAICm0QEApdkBAOTdAICrwQEAqvkBAOjdAIDs3QCAr6EBAK69AQCtvQEArNkBAITdAICCFQAAgeUfAIDlHwDw3QCA9N0AgPjdAID83QCAqAkfAKkJHwCqHR8AqxUfAKwNHwCtcR8ArnEfAK9xHwCwER8AsS0fALIlHwCzyR8AtN0fALXBHwC2wR8At8EfALjFHwC5yR8AutUfALupHwC8uR8AvbkfAL6pHwC/oR8As7UfAADeAIAE3gCACN4AgAzeAIC20R8AtaUfABDeAIC7yR8AuvUfABTeAIAY3gCAvyUfAL45HwC9PR8AvNEfABzeAIAg3gCAJN4AgCjeAIAs3gCA4WAfADDeAIDjtBwANN4AgDjeAIA83gCA7wAdAEDeAIBE3gCASN4AgEzeAICjNR4AUN4AgFTeAIBY3gCAXN4AgKZRHgClJR4AYN4AgKtJHgCqdR4AhKgCAGTeAICvpR4ArrkeAK29HgCsUR4AgE0AAIFVAACCVQAAs8kBAGjeAIC12QEAtskBAGzeAICGoAAAhwQBALrFAQC7rQEAvLUBAL29AQC+tQEAv60BAKiZAQCpmQEAqg0BAKsFAQCsHQEArQUBAK4FAQCvNQEAcN4AgHTeAIB43gCAfN4AgIDeAICE3gCAiN4AgIzeAIC4JQEAuS0BALo5AQC7OQEAvCkBAL0pAQC+3QAAv9UAALBNAQCxJQEAsi0BALMlAQC0PQEAtSUBALYhAQC3HQEAkN4AgJTeAICY3gCAo4kCAJzeAIClmQIApokCAKDeAICk3gCAqN4AgKqFAgCr7QIArPUCAK39AgCu9QIAr+0CAKzeAICw3gCAtN4AgIRAAgC43gCAvN4AgMDeAIDE3gCAgA0AAIEVAACCHQAAyN4AgMzeAIDQ3gCAh7QDAIbcBAC+zAMA2N4AgNzeAIDg3gCA7+gCAOTeAIDo3gCA7N4AgOP8AgDw3gCA4dABAPTeAID43gCA/N4AgADfAIAE3wCAs2EDAAjfAIAM3wCAEN8AgBTfAIC2eQMAtXEDABjfAIC7XQMAul0DABzfAIAg3wCAv+EAAL79AAC9/QAAvP0AALC5AgCxuQIAsgkBALMJAQC0GQEAtQUBALYFAQC3PQEAuAUBALllAQC6bQEAu2UBALxhAQC9YQEAvmEBAL9hAQCFXAcAJN8AgCjfAIAs3wCAFN0AgDDfAIA03wCAON8AgKgxAgCpOQIAqskCAKvJAgCs2QIArdkCAK7JAgCvyQIAhMwFAOGAHgA83wCA47weAOE4HgBA3wCA46AAAL4QBABI3wCATN8AgO8MHgBQ3wCAVN8AgFjfAIBc3wCA73QeAKNhAgCCUQAAgUEAAICRAABg3wCApnkCAKVxAgBk3wCAq10CAKpdAgCGyAQAhzwFAK/hAQCu/QEArf0BAKz9AQCohQYAqY0GAKqFBgCrmQYArIkGAK2JBgCuvQYAr7EGAETfAIBo3wCAbN8AgHDfAIB03wCAeN8AgHzfAICA3wCAuJ0GALmtBgC6pQYAuwkHALwZBwC9GQcAvg0HAL8FBwCw0QYAsdEGALLRBgCz0QYAtLUGALW9BgC2tQYAt60GALMNBgCE3wCAiN8AgIzfAICQ3wCAtgkGALUBBgCU3wCAuxUGALoVBgCY3wCAnN8AgL95BgC+cQYAvQUGALwFBgCg3wCA4aAEAKTfAIDjXAUAgA0AAIE1AACCPQAAqN8AgKzfAICw3wCAhGADAL5sAAC/8AEAhZAAALTfAIDvmAUAo40HAIQIAACGAAwAh4wAALjfAICmiQcApYEHALzfAICrlQcAqpUHAMDfAIDE3wCAr/kHAK7xBwCthQcArIUHAMjfAICz6QYAzN8AgNDfAIC26QYA1N8AgNjfAIC16QYAukUBALtNAQDc3wCA4N8AgL5FAQC/TQEAvFUBAL1NAQCoIQYAqSEGAKolBgCrPQYArCUGAK0tBgCuSQYAr0EGAOTfAIDo3wCA7N8AgPDfAID03wCA+N8AgPzfAIAA4ACAuEkBALlJAQC6WQEAu1EBALx5AQC9eQEAvhkBAL8VAQCwxQEAsc0BALLFAQCz3QEAtMUBALXNAQC2xQEAt3kBAATgAIAI4ACADOAAgKOhBQAQ4ACApaEFAKahBQAU4ACAjyHqAxjgAICqDQIAqwUCAKwdAgCtBQIArg0CAK8FAgCX7RIAlmUSAJVFEQCUnRYAk3EWAJJVFQCReesDkFnqA59hBgCeNQUAnUUaAJxpGgCbVRkAmkUeAJlZHgCYRR0A4WAAABzgAIDjTD4AIOAAgKOxAgCi1QEAobUHAKCJBgCxATgAsAk+ALOVOgCyjToAtbUmALQBJADvaDoAvjAMAKnJNgCowTYAqwEwAKrhNwCtzTMArPUyAK/5PgCuATwAoRkCACjgAICjbQ4Aom0OAKX1CgCkAQgAp4ULAKaZCgCGAA0Ah0QNAIIJ6wODCesDhDHqA4UVFACGORcAh80XAISgDQAs4ACAiiUQAIsNEwCMnRMAjQ0cAI4ZHwCPDR8A1N4AgO8AAwCSbRgAk0kbAJR9GwCVBQQAllkHAJdJBwAw4ACANOAAgJpFBgCbLQAAnFEDAONgAAA44ACA4WwAAIClAQCBAQEAggUBAL4ADAA84ACAQOAAgETgAIDviAEASOAAgOFUBgBM4ACA41QBAFDgAIBU4ACAWOAAgFzgAICz6QIAYOAAgGTgAIBo4ACAbOAAgLadAgC1mQIAcOAAgLuJAgC6vQIAdOAAgHjgAIC/WQIAvlECAL1ZAgC8kQIAoykNAHzgAICA4ACAhOAAgIjgAICmXQ0ApVkNAIzgAICrSQ0Aqn0NAJDgAICY4ACAr5kNAK6RDQCtmQ0ArFENAIBRAACBWQAAgmEAALMtDwCc4ACAtS0PALbJDwCg4ACAhkADAIcIAwC6yQ8Au8UPALzBDwC9wQ8AvsEPAL/BDwAk4ACAlOAAgKTgAICo4ACArOAAgLDgAIC04ACAuOAAgKhFDgCpgQ8AqskPAKvJDwCsyQ8ArSUPAK4tDwCvJQ8AsGEPALFtDwCyeQ8As3kPALRpDwC1aQ8Ath0PALcVDwC4LQ8AuTUPALo1DwC7BQ8AvB0PAL3xAAC+8QAAv/EAAKNhDgC84ACAhMQBAMDgAIDE4ACApoUOAKVhDgDI4ACAq4kOAKqFDgDM4ACA0OAAgK+NDgCujQ4ArY0OAKyNDgDU4ACA2OAAgNzgAIDg4ACA5OAAgOjgAIDs4ACA8OAAgPTgAICCHQAAgR0AAIAdAAD44ACA/OAAgADhAIC+tAEAqK0BAKnVAQCq1QEAqwUBAKwdAQCtBQEArg0BAK8FAQCGgAEAhxgBAAjhAIAM4QCAEOEAgBThAIAY4QCAHOEAgLiFAAC5jQAAuoUAALudAAC8hQAAvY0AAL6FAAC/vQAAsH0BALHhAACy5QAAs/0AALTtAAC13QAAttUAALe9AACzXQIAIOEAgCThAIAo4QCALOEAgLaFAgC1lQIAMOEAgLslAwC6uQIANOEAgDjhAIC/GQMAvikDAL0pAwC8MQMAvswEAKMZAgA84QCAQOEAgKbBAgBE4QCASOEAgKXRAgCq/QIAq2EDAEzhAIBQ4QCArm0DAK9dAwCsdQMArW0DAKgpAwCpKQMAqjkDAKs5AwCsKQMArSkDAK6dAACvlQAAVOEAgFjhAIBc4QCAYOEAgGThAICCqQEAga0BAICtAQC4mQAAua0AALqlAAC7bQAAvHUAAL19AAC+dQAAv20AALDtAACx9QAAsvUAALPFAAC03QAAtb0AALa1AAC3qQAA4XgBAOEcDgDjEAAA4zwOAGjhAIBs4QCAvhQEAHDhAICErAIAeOEAgId4BQCGDAUAfOEAgIDhAIDvvAAA70gOALPxAgCE4QCAiOEAgIzhAICQ4QCAtukCALXhAgCU4QCAu3EBALppAQCY4QCAhKAEAL85AQC+WQEAvVEBALxhAQCc4QCAhIwEAKDhAICEADgApOEAgKjhAICs4QCAsOEAgKqJDgCriQ4AqLkOAKmxDgCu/Q4Ar+EOAKz5DgCt9Q4Asq0OALNlDgCwkQ4AsaUOALZ9DgC3ZQ4AtH0OALV1DgC6XQ4Au+UNALhdDgC5VQ4AvuENAL/pDQC8/Q0AvfUNAKOxBQB04QCAtOEAgLjhAIC84QCApqkFAKWhBQDA4QCAqzEGAKopBgDE4QCAyOEAgK95BgCuGQYArREGAKwhBgDM4QCA0OEAgNThAIDY4QCAgB0AAIEJAACCOQAA3OEAgODhAIDk4QCAhsgAAIcMAwDo4QCA7OEAgPDhAID04QCAqKUHAKm1BwCqvQcAq8kHAKzZBwCt2QcArskHAK/BBwC+oAAA+OEAgPzhAIAA4gCABOIAgAjiAIAM4gCAEOIAgLjNAAC51QAAutUAALvlAAC8/QAAvZUAAL6dAAC/lQAAsIkHALFlBwCyYQcAs30HALRlBwC1bQcAtmUHALf1AACzNQYAFOIAgBjiAIAc4gCAIOIAgLZZBgC1UQYAJOIAgLuhBgC6TQYAKOIAgCziAIC/qQYAvqEGAL2pBgC8tQYAMOIAgDTiAIDv8AUAOOIAgDziAIBA4gCAROIAgEjiAICAPQAAgQkAAIIdAABM4gCA4cgGAFDiAIDjSAQAVOIAgKO1BgBY4gCAhigAAIdAAQBc4gCAptkGAKXRBgBg4gCAqyEGAKrNBgBk4gCAaOIAgK8pBgCuIQYArSkGAKw1BgBs4gCAs70BAHDiAIB04gCAtnkBAHjiAIB84gCAtXkBALpVAQC7XQEAgOIAgITiAIC++QAAv/kAALxFAQC9+QAAqHECAKlxAgCqcQIAq3ECAKy1AgCtvQIArrUCAK+tAgC+rDwAiOIAgIziAICQ4gCAlOIAgJjiAICc4gCAoOIAgLhpAwC5aQMAugkDALsJAwC8HQMAvQUDAL4NAwC/BQMAsNUCALHdAgCy1QIAs2kDALR5AwC1eQMAtmkDALdhAwCk4gCAqOIAgKziAICj9QIAsOIAgKUxAgCmMQIAtOIAgLjiAIC84gCAqh0CAKsVAgCsDQIArbEDAK6xAwCvsQMA7xgCAIIVAACBbQAAgG0AAMDiAIDI4gCAhvg8AIcYAwDM4gCA0OIAgNTiAIDY4gCA42wHAAThAIDhaAEA3OIAgKiFAgCplQIAqpUCAKulAgCsvQIArdUCAK7RAgCv0QIA4OIAgOTiAIDo4gCA7OIAgPDiAID04gCA+OIAgPziAIC4dQEAuX0BALp1AQC7zQEAvNUBAL3dAQC+yQEAv8EBALC1AgCxvQIAsoECALOBAgC0VQEAtV0BALZVAQC3TQEA4bQGAADjAIDj9AYABOMAgIQYPQAI4wCADOMAgBDjAIAU4wCAGOMAgBzjAIAg4wCAJOMAgCjjAIDvWAYALOMAgIF9AACAcQAAMOMAgIIFAAA44wCAPOMAgO+AAQC+VDwA4ZABAEDjAIDjfAYAROMAgEjjAIBM4wCAhtg8AIf0PACjnT0AxOIAgDTjAIBQ4wCAVOMAgKbVPQCltT0AWOMAgKv5PQCq8T0AXOMAgGDjAICvGT4ArhE+AK3VPQCs1T0AZOMAgLOhPgBo4wCAbOMAgLatPgBw4wCAdOMAgLWxPgC6ST8Au0k/AHjjAIB84wCAvkk/AL9JPwC8ST8AvUk/AKhVPgCpZT4Aqm0+AKtlPgCsfT4ArWk+AK65PwCvuT8AgOMAgITjAICI4wCAjOMAgJDjAICU4wCAmOMAgJzjAIC4VT8AuV0/ALpVPwC7bT8AvHU/AL19PwC+dT8Av20/ALDJPwCxyT8Astk/ALPZPwC0yT8Atck/ALZ9PwC3cT8AghUAAKPhPwCAsQEAgbEBAKbtPwCg4wCAvtABAKXxPwCqCT4Aqwk+AITkAQCk4wCArgk+AK8JPgCsCT4ArQk+ALPdPACo4wCAhugAAIfMAQCs4wCAtpU8ALX1PACw4wCAu7k8ALqxPAC04wCAuOMAgL9ZPwC+UT8AvZU8ALyVPACoUT4AqVE+AKptPgCrYT4ArGE+AK1hPgCulQEAr40BAISgAQC84wCAwOMAgMTjAIDI4wCAzOMAgNDjAIDU4wCAuKkBALmpAQC6aQEAu2kBALx5AQC9eQEAvmkBAL9pAQCw/QEAsc0BALLFAQCzrQEAtLkBALW5AQC2rQEAt6UBALPlPQDY4wCA3OMAgODjAIDk4wCAtuE9ALXpPQDo4wCAuwkCALo5AgDs4wCA8OMAgL99AgC+fQIAvXkCALwRAgD04wCAo6E9APjjAID84wCApqU9AADkAIAE5ACApa09AKp9AgCrTQIACOQAgAzkAICuOQIArzkCAKxVAgCtPQIAgOkAAIHpAACCHQAAvsADAO/kAgAQ5ACAh1QDAIY8BADjEAEAGOQAgOH4AQAc5ACAIOQAgCTkAIAo5ACALOQAgDDkAIA05ACAOOQAgLORAwA85ACAtbkDALZ9AwBA5ACAROQAgEjkAIC6WQMAu1kDALxJAwC9SQMAvv0AAL/1AACoRQIAqVUCAKpVAgCrZQIArH0CAK2xAgCusQIAr7ECAIRsBQBM5ACAUOQAgFTkAIBY5ACAXOQAgL5wBQBg5ACAuF0BALltAQC6ZQEAuw0BALwZAQC9GQEAvg0BAL8FAQCw0QIAsdECALLRAgCz0QIAtHUBALV9AQC2dQEAt20BAOFAPwDjvAAA4wg+AOFsPgBk5ACAaOQAgGzkAIBw5ACAdOQAgHjkAIB85ACAgOQAgL5sBwDvVAAA75w+AIjkAICjnQIAgmkAAIFhAACAaQAAjOQAgKZxAgCltQIAkOQAgKtVAgCqVQIAhsgEAIfsBACv+QEArvEBAK1FAgCsRQIAqKUGAKmpBgCquQYAq7kGAKypBgCtqQYArtkGAK/ZBgCE5ACAlOQAgJjkAICc5ACAoOQAgKTkAICo5ACArOQAgLhxBwC5cQcAunUHALvdBwC8xQcAvc0HAL7FBwC//QcAsKkGALG1BgCytQYAs40GALSVBgC1UQcAtlEHALdRBwCzMQYAsOQAgLTkAIC45ACAvOQAgLYpBgC1IQYAwOQAgLtxBgC6bQYAxOQAgMjkAIC/lQcAvlEGAL1ZBgC8YQYAzOQAgKN1BgDQ5ACA1OQAgKZtBgDY5ACA3OQAgKVlBgCqKQYAqzUGAODkAIDk5ACArhUGAK/RBwCsJQYArR0GAIANAACBFQAAgh0AAOjkAIDs5ACA8OQAgITcAQD05ACAhoAAAIcgAQD45ACA/OQAgADlAIAE5QCACOUAgAzlAIAQ5QCA43QEABTlAIDhyAUAGOUAgBzlAIAg5QCAJOUAgCjlAIAs5QCAMOUAgDTlAIA45QCA77QEADzlAIBA5QCAqD0GAKlVBgCqVQYAq6kBAKy5AQCtuQEArqkBAK+pAQCErAEAROUAgEjlAIBM5QCAUOUAgFTlAIBY5QCAXOUAgLhtAQC5BQEAugEBALsBAQC8BQEAvQ0BAL4xAQC/MQEAsNkBALHZAQCybQEAs2UBALR9AQC1ZQEAtmUBALdVAQCBvQMAgL0DALPVBQCCGQAAtTkCAGDlAIC+VAMAtjECAGjlAIBs5QCAuxUCALoVAgC9uQIAvLECAL+pAgC+sQIAcOUAgKZpAgClYQIAhAAMAKONBQB05QCAhvgMAId8AwCv8QIArukCAK3hAgCs6QIAq00CAKpNAgB45QCAfOUAgIDlAICE5QCAiOUAgIzlAIDjIAEAkOUAgOGgAQCU5QCA70ACAJjlAICc5QCAoOUAgKTlAICo5QCArOUAgLDlAICz8QMAtOUAgBTkAIC45QCAvOUAgLbpAwC14QMAwOUAgLu1AwC6tQMAxOUAgMjlAIC/lQMAvpUDAL2lAwC8pQMAqCkCAKkpAgCqOQIAqzkCAKwpAgCtKQIArlkCAK9VAgCAzQEAgQkAAIIZAADM5QCA0OUAgL58DQCHtA0AhhwMALgxAgC5PQIAujUCALvpAgC8+QIAvfkCAL7pAgC/6QIAsDECALExAgCyMQIAszECALQRAgC1EQIAthECALcRAgDY5QCA3OUAgODlAIDk5QCA6OUAgOzlAIDw5QCA79QGAPTlAIDhVAYA+OUAgOOkAACsDBUA/OUAgADmAIAE5gCAo/ECAAjmAIAM5gCAEOYAgBTmAICm6QIApeECABjmAICrtQIAqrUCABzmAIAg5gCAr5UCAK6VAgCtpQIArKUCAKghDgCpIQ4AqkkOAKtZDgCsaQ4ArWkOAK6ZDgCvmQ4A1OUAgCTmAIAo5gCALOYAgDDmAIA05gCAOOYAgDzmAIC49Q4Auf0OALr1DgC7iQ4AvJ0OAL2FDgC+hQ4Av7UOALDpDgCx6Q4Asv0OALPxDgC01Q4Atd0OALbVDgC3zQ4As8EOAIIVAACBtQAAgLUAAEDmAIC26Q4AteEOAL4QAAC7LQ4Aui0OAIRkAwBE5gCAvxkOAL4RDgC9JQ4AvCkOAEjmAICjhQ4AhogAAIdsAwCmrQ4ATOYAgFDmAIClpQ4AqmkOAKtpDgBU5gCAWOYAgK5VDgCvXQ4ArG0OAK1hDgCziQ4AXOYAgGDmAIBk5gCAaOYAgLaBDgC1iQ4AbOYAgLuVDgC6jQ4AcOYAgHTmAIC/+Q4AvvEOAL2FDgC8hQ4AeOYAgHzmAICA5gCAhOYAgOMMDQCI5gCA4RgNAIzmAIDvrAwAkOYAgJTmAICY5gCAnOYAgKDmAICk5gCAqOYAgKgBDgCpAQ4AqgEOAKsBDgCsAQ4ArQEOAK4BDgCvPQ4AgN0AAIEJAACCGQAArOYAgLDmAICEPAEAvnQAALjmAIC4HQ4AuS0OALolDgC76QEAvPkBAL35AQC+6QEAv+kBALBJDgCxUQ4AslEOALNRDgC0NQ4AtT0OALY1DgC3LQ4Ao4kNALzmAICGrAQAhzwDAMDmAICmgQ0ApYkNAMTmAICrlQ0Aqo0NAMjmAIDM5gCAr/kNAK7xDQCthQ0ArIUNANDmAICznQIAhEgDAL5ABAC2VQMA1OYAgNjmAIC1sQIAunEDALt5AwDc5gCA4OYAgL4xAwC/MQMAvFEDAL1RAwCwkQMAsZkDALKhAwCzoQMAtNEDALXRAwC20QMAt9EDALj1AwC5+QMAus0DALvFAwC83QMAvcUDAL7NAwC/xQMA5OYAgOjmAIDs5gCA8OYAgIV8GQD05gCA+OYAgGTlAICoIQIAqTECAKoxAgCrBQIArB0CAK3xAwCu8QMAr/EDAPzmAIAA5wCABOcAgAjnAIDvUAAADOcAgBDnAIAU5wCA44QAABjnAIDh+AEAHOcAgIAVAACBGQAAggUAACDnAICjmQMAKOcAgIZoBACHYAUALOcAgKZRAgCltQMAMOcAgKt9AgCqdQIANOcAgDjnAICvNQIArjUCAK1VAgCsVQIAPOcAgEDnAIBE5wCASOcAgEznAIBQ5wCAVOcAgO/4AQC+bAQA4YAOAFjnAIDjFAEAXOcAgGDnAIBk5wCAaOcAgGznAIBw5wCAdOcAgLPdAQB45wCAtf0BALb1AQB85wCAgOcAgITnAIC6sQEAu4UBALydAQC9NQEAvj0BAL81AQCpBQYAqLkFAKsVBgCqHQYArT0GAKw9BgCvTQYArl0GACTnAICCHQAAgR0AAIAdAACI5wCAjOcAgJDnAICU5wCAuUEHALidBgC7QQcAukkHAL1FBwC8WQcAv0UHAL5FBwCxCQYAsD0GALOpBgCyAQYAtbkGALSxBgC3rQYAtrEGAKORBgCEjAIAhigAAIfAAwCY5wCAprkGAKWxBgCc5wCAq8kGAKr9BgCg5wCApOcAgK95BgCucQYArXkGAKzRBgCo5wCAs5kHAKznAICw5wCAtlEHALTnAIC45wCAtbEHALptBwC7dQcAvOcAgMDnAIC+WQcAv0UHALxtBwC9ZQcAxOcAgMjnAIDM5wCA0OcAgNTnAIDY5wCA3OcAgO+oBQDg5wCA4TQFAOTnAIDjdAUA6OcAgOznAIDw5wCA9OcAgKMdBgCCLQAAgRUAAIAdAAD45wCAptUGAKU1BgD85wCAq/EGAKrpBgAA6ACAhCgBAK/BBgCu3QYAreEGAKzpBgCoxQYAqdUGAKrVBgCr5QYArP0GAK0VBgCuHQYArxUGAL7sAQAI6ACAhggAAIcgAAAM6ACAEOgAgBToAIAY6ACAuH0GALkFBgC6DQYAuwUGALwBBgC9CQYAvjkGAL85BgCwbQYAsXUGALJ9BgCzdQYAtFkGALVFBgC2TQYAt0UGAKiRAgCpmQIAqqECAKuhAgCs0QIArd0CAK7VAgCvyQIAHOgAgCDoAIAk6ACAvyweACjoAIAs6ACAMOgAgDToAIC4VQMAuV0DALppAwC7ZQMAvGEDAL1hAwC+YQMAv2EDALC5AgCxjQIAsoUCALNtAwC0dQMAtX0DALZ1AwC3bQMAOOgAgDzoAICzIQIAQOgAgLVRAgCEiAMAROgAgLZVAgC05gCAvigcALtBAgC6dQIAvbEDALxZAgC/sQMAvrkDAKNpAgBI6ACATOgAgFDoAIBU6ACAph0CAKUZAgBY6ACAqwkCAKo9AgBc6ACAYOgAgK/5AwCu8QMArfkDAKwRAgCopQIAqbUCAKq9AgCrtQIArK0CAK01AQCuPQEArzUBAL4sHABk6ACAaOgAgGzoAIBw6ACAeOgAgIdoHQCGHB0AuIUBALmNAQC6hQEAu50BALyNAQC9vQEAvrUBAL95AACwUQEAsVEBALJRAQCzUQEAtPEBALXxAQC29QEAt+UBAO/YAACCtQAAgaUAAIClAAB86ACAgOgAgIToAIDvxAYAiOgAgOH0BgCM6ACA4zgBAOPMAACQ6ACA4SgBAJToAICY6ACAtuUBALV1AgCEQBwAs2UCAJzoAICg6ACApOgAgL9lAQC+ZQEAvdUBALzVAQC7xQEAusUBAKjoAICs6ACAo7UdAHToAICw6ACAtOgAgLjoAICmNR4ApaUdALzoAICrFR4AqhUeAMDoAIDE6ACAr7UeAK61HgCtBR4ArAUeAMjoAIDM6ACA0OgAgNToAICADQAAgTUAAII9AADY6ACA3OgAgODoAIC1BQAAcRoAgOG0AgCs2AIAtQUAAHUaAICotR8AqRUfAKodHwCrFR8ArDEfAK09HwCuLR8AryEfAOG0AgCs2AIAtQUAAHkaAIDhtAIArNgCALUFAAB9GgCAuNEAALnZAAC64QAAu+EAALyRAAC9kQAAvpEAAL+RAACwIR8AsTEfALIxHwCzMR8AtAkfALUJHwC28QAAt/EAAOG0AgCs3AIA71QdALUdAACBGgCA4bwCAKzQAgC1KQAAoyUBAKKRAwChFR0AoA0dAOGAHgCFGgCA47wdAOHEAgCz1R4AtQkAAKzYAgCJGgCA4bwCALb9HgC1+R4ArOACALu1HgC6pR4AtQUAAI0aAIC/jR4Avo0eAL2lHgC8pR4AoxUeAOG8AgCs0AIAtREAAI9pJQCmPR4ApTkeAJEaAICrdR4AqmUeAOG0AgCseAEAr00eAK5NHgCtZR4ArGUeAJvdFACa5RUAmQEXAJjhEACfcR8AnnkZAJ35GQCcARsAk+UtAJIRLwCRbSkAkG0pAJf5EQCW8REAlYUsAJSZLQC1JQAA4ZQCAILxJgCDjSoAhJUqAIXhLACGHS4Ah3kuAKy0AgCVGgCAilUvAIspEgCMORIAjRkTAI7xFACPHRYAtQUAAJkaAICSVRcAk5EYAJRxGgCV+RoAlvkcAJd9HgCC4AMAkwsAgJpVHgCb2QAAnHUCAIMMAICzDACAuIkKAKwBBACthQYAroEGAMwQAgDMfAMAtgwAgJ0aAIDCDACAxQwAgMgMAIAACwCAgaUyArwMAIAE6ACAmpUGAJtVIwK8kQYAvbEAAL6RBgC/rQYAuOkGALmVBgC6kQYAoRoAgLTBBgC1zQYAts0GALfdBgCw/QYAseUGALKdAACz5QYAhVTHA6UaAICH/AAAuAEKAK0aAIDpDACAsRoAgIyRcwCNpAEAzPACAL4NAIDBDQCAiRQAALgZCgCLDAAAGg4AgFMOAIC5DACAvwwAgBkKAICRwAEAywwAgLhtCgDODACA1AwAgNoMAIDdDACA4AwAgLUaAIAoDQCA5gwAgLkaAIDhpB4AKw0AgONUHgCvIXMAzCgCAO8MAIDsDACA8gwAgPUMAID4DACAzIACAJS4AwD7DACAkhQCAO9gHgCQAAIA/gwAgAoNAIC48QoADQ0AgJ8LAIAQDQCAiSkLABMNAICpGgCAvDABAL/EAQC+7AEAFg0AgMzsAgC4xQoAukQBAK0JAIAZDQCAygYAgN8GAIDyBgCAHA0AgPoGAIAfDQCACgcAgC0HAIAYBwCA9gcAgC8HAICpDQCAOgcAgK8NAIBKBwCAtXkAAGcHAIC3cSoCcgcAgLFhAAB0BwCAsw0pAo0HAIC96QAAoAcAgPoHAICtBwCAuRkrAsMHAIC7WRQCHwgAgFoJAIA8CACALw4AgFsIAIA5AACAgQgAgHEAAIDHCACAKwAAgCAJAIA9AACAXAkAgEMAAIBeCQCARQgAgGoIAIBJAACAAAgAgFMAAIB5CQCAWQAAgCINAIBfAACAuw0iAtANAIDMFDYCHwAAgL9lAAC+EQAAvW0AAOUHAICAaQEAgXUBAIJxAQCD3SEChGkHAIWBBwCGgQcAh3EBAIihAQCJrQEAirUHAIuNBwCMlQcAjaUBAE8AAICPpQEAkOEBAJHtBwCSsSECk/0HAJSNBwCVUQYAlvEBAJfZAQCY0QEAmXUGAJp9BgCb1QEAnGkGAJ2ZFAKeUQYAn1EGAKB1FAKhuQYAokkBAKOFLQKkIQEApS0BAKZ1FAKntQYAqKERAqlRFAKqlQYAsSEAgMy8NQLNPDUCbQAAgKoDAICsAwCArwMAgL0hAIDEIQCA2yEAgOIhAIDJAACADwAAgLihBgC6BgCAtwYAgMwAAIDOIQCAtQMAgN0FAIAYBgCAugUCALvVAgC46QUAuf0FAL7JAgC/5RcCvA0CAL0BAgCy4QUAs+EFALCNBQCxnQUAtuUFALfpBQC09QUAte0FAKo9BQCrwQUAqD0FAKk1BQCuzQUAr/UFAKzNBQCtxQUAoj0FAKMFBQCg1QIAoTkFAKYdBQCnBQUApB0FAKUVBQC/BgCAm8EFAD4GAIBVBgCAnt0FAJ8xBACcUQIAndUFAHIGAICJBgCApAMAgDAiAIDbAACAoAMAgI8HAIDuBwCA8gcAgJAJAIACCACABggAgJYLAICUCQCArwoAgG8HAICLBwCAlwcAgKIHAICqBwCAqgkAgPsOAIASDwCAHw8AgMwEMwLNsDACzCAzAs3gMALMEDACzGgwAsxYMALNjDACzGgxAs0UMQLM1DECzRQ2AsxwIALN0CcCzDA2AswkMQLMDDwCzWg/AswYPwLNND8CzBg9As3AMgLMRDwCzBg5Asw4MgLNqDICzIgyAs34MwLMfDMCzUAzAswoMwLNCDMCzMghAs0kJgLMrCYCzEA4AsyYJQLNyDoCzBwkAs0QJALMhDsCzag7AsysJQLNvDoCzKw4Asz4JwLM4DgCzXQ4AicPAID2BgCAYQ0AgIgNAIDNICoCzBwrAqoGAIAsIgCAzKQgAs2gJwLMOCYCygQAgMw4OgLNPDsCzBA5As1gPgLMoAMAvj0NAL3tLALWBACAu1UjAgQJAIC5PSICzwYAgNkHAIClBACAoA0AgLIEAIBvBQCA9AYAgL4EAIB1BQCAr70MAK6ZLgKtpQwAwgUAgKvFIgIDBgCAxAQAgCMGAIDQBACAyAUAgCkGAIBdBgCAowEYAqAEAIAaBwCAHQcAgJ9dDACeUQwAnUUMACcHAICbWSECrwcAgLEHAIC0BwCAuAcAgCoHAIDOBwCA0AcAgJMtJgLTBwCAbAgAgG8IAICPBQwAjnEMAI1lDAB5CACAi0UgAmAJAICJNS8CYwkAgGcJAIB8CACAcAkAgHMJAIC9AwCAACIAgIFdDACAYQwAgAABAIEYAACCAAQABCIAgIQQBwCFFAYAhuQIAIc8AgCILAUAiaQFAIoAeAAIIgCAjCQAAAwiAIAUIgCAECIAgLgRAACRxHsAkkh6AJNMeQAcIgCAzOgCAJbwCQC4OQAAkMAJACQiAICS8AkAzPgCAJS0CQC4DQAAKCIAgMwcAgC4BQAANCIAgMzkAgC4HQAAOCIAgDwiAIBDIgCAWiIAgKiMCACp5HsAYSIAgKvUBgDM5AIAuA0AAGsiAIDMlAIAbyIAgLGAewC4CQAAuBUAAMz8AgC15AgAcyIAgMzYAgB3IgCAuAUAALqcBQC7XAUAvAB8AL30fwC++H0Av/xyAIAJOgKBDToCggE6AoMFOgKEGToChR06AoYROgKHFToCiCk6AoktOgKKIToCiyU6Aow5OgKNPToCjjE6Ao81OgLM8AIAkekPAIMiAIDMzAIAuBkAAH8iAIDM3AIAl+UPALg1AAC4DQAAjyIAgMz8AgC4BQAAkyIAgMwwAgCXIgCAzNACAJsiAICfIgCAzIgCAKQtDwClVQ8Apl0PAMyUAgCoqToCqa06ArjVAACjIgCAuDUAAKciAIDMUAMAr7U6AswsAwCrIgCAzBgDALMFDwC0HQ8AzyIAgLYJDwC3CQ8Avmh9ALhtAAC4RQAAzDgDALwpDwDTIgCAviUPAMxYAwCH5Q4AzOg6Ari9AQC4yQEAzPA1As2kMwLMgCICzXwlAs2UNgLMBCkCzew7AsxkOgK45QEAuMEBAInVDgCI1Q4Al7EOALgNAACvIgCAsyIAgLciAIC4GQAAuyIAgNciAICfaTsC2yIAgL8iAIC4PQAAzMQCAMz4AgDDIgCAxyIAgLjZAADLIgCA3yIAgLjRAADjIgCAuPEAAMzMMwLnIgCAuMkAAMzoMwLrIgCAuNUAAKllAAC4yQAAzNgCAKq5BgC3TQ0Atk0NALU1DgC0NQ4AuFUAABUjAICxGQ8AsCkOAL/1AwC+UQ0AvVkNALw1DAC7XQ0Aul0NALldDQC4XQ0AgL0KAIHFCgCCFQQAg8kKAMx8BQCF3QoAhtUKAIfNCgDMVAUAifEKAIq5CACLDQgAjBEIAI0VCACOtScCj+UKAJBpCACRbQgAknEIAJNtJALMEAUAlR0IAJaFCgDMEAUAzDQFAJk9CACaiQoAmw0IAJwRCACdFQgAzEgFAMwQAgCgZQoAoW0KAKJlCgC4BQcApLEEAMzoAgCmsQQAuA0HAKiBBADM/AIAqpkIAKtdCgCsuQgArakEALglBwCvNQgAsNEIALHxBADMwAIAs40IALQpKAK1IQoAtiEKALchCgC4IQsAuSUIALhBBwC7KQsAvA0dAr3dDwC+MQsAvzELAIDdCgAZIwCAnKF9ANADAIDpAwCAhRkJAIaZCQCHlQkAiOEJAIklJQICBACAGwQAgC4EAIBBBACAVAQAgGcEAICQrQoAkUkFAJJtBQCTYQUAlGEFAJVtBQCWZQUAlxEFAJg1BQCZPQUAmjUFAJsNBQCcFQUAnR0FAJ4VBQCfCQUAoKkJAKH9BQCi9QUAowEFAKQFBQClDQUApgUFAKc9BQCoBQUAqQ0FAKoFBQCrGQUArIkJAK2pBQCutQkAr/0JALABCQCxfQUAsnUFALMBBQC0aQkAtQEFALYFBQC3PQUAuAUFALnhJQK6AQUAuwEFALzRJQK9PQkAvnkJAL9dCQCDMAUAoXgHAJ+xfgB6BACApHgHAKVIBwCNBACA8wQAgIt8BADdAACAEwEAgIhIBAAcAQCAIAEAgCQBAIAoAQCALAEAgDABAICyAAcAs/wHADQBAIDhAACAtuQHALfwBwDmAACA6wAAgLrgBwC7nAcAvIgHAL2oBwDwAACAs8F+AKPMBAD1AACA+gAAgIMABAD/AACAhXQEAKUgBAAEAQCAiEwEAAkBAIAOAQCAFwEAgK8tBwCNxAcArSEHAKwpBwDNAwCA8AQAgI8FAICwZQcA4gUAgB0GAIBDBgCAWgYAgHcGAICOBgCA0wMAgOwDAIAFBACAHgQAgDEEAIC8fAQAgt0rAoPlKwKA/QoAgfkrAoaZCQCHmQkAhOEKAIXhCgCKiQkAi4kJAIiJCQCJiQkAjoUJAEQEAICM4QgAjY0JAJK5KwKTQScCkJkrApHFCwCWyQsAl3UnApTFDQCV0SQCmskLAJvZKgKYyQsAmXkHAFcEAIBqBACAnP0LAH0EAICQBACA9gQAgKABAICkAQCAqAEAgONkAgCsAQCAsAEAgLQBAIDvvAcAqBEJALgBAIC8AQCAwAEAgMQBAIDIAQCAzAEAgNABAIDUAQCA2AEAgNwBAIDgAQCA5AEAgOgBAIDsAQCA8AEAgPQBAID4AQCA/AEAgAACAICCnH4ABAIAgKD1VAKh2VQCoulUAqP1dQCk7XUApZ12AKaVdgCnvXYAqIV2AKkpfQCqOX0AqwV9AKwdfQCtBX0Arg19AK8FfQCwfX0AsUl+ALJRfgCzUX4AtHV+ALV9fgC2aX4At2l+ALhZfgC5WX4Auil+ALspfgC8IX4AvSF+AL4ZfgC/GX4AkgcAgDkJAIDXBwCATSIAgLQNAAC1NQAAtj0AAKIGAICsBgCArwYAgAMjAIAJIwCAvSV4ALy1WALGMQCALjoAgJkqAIC9KgCAySoAgNkqAIDhKgCA7SoAgPUqAID9KgCACSsAgF0rAIB1KwCAhSsAgJUrAIClKwCAtSsAgNUrAICAeX8AgYF/AIKBfwCDnX8AhI1/AIWxfwCGsX8Ah7F/AIjhfwCJ4X8AiuF/AIv9fwCM5X8Aje1/AI7lfwCP3X8AkKV/AJGtfwCSpX8Ak71/AJSlfwCVrX8Alm1+AJctfgCYFX4AmRl+AJrpfgCb6X4AnPl+AJ35fgCe6X4An+V+AKAdfgChJX4AoiV+AKM9fgCkJX4ApS1+AKYlfgCnXX4AqGV+AKltfgCqZX4Aq31+AKxlfgCtbX4ArmV+AK9dfgCwJX4AsS1+ALIlfgCzPX4AtCV+ALUpfgC2WXcAt9V1ALj9eQC56XUAuvl1ALvZeQC86XUAvdV1AL7RdQC/2XUAgDF2AIE9dgCCSXYAg0V2AIRBdgCFTXYAhvl0AId9dgCIoQIAiU12AIpZdgCLuXoAjEl2AI2degCOsQIAjx16AJCRVgKRKXYAkoF2AJPNdgCU2XYAlel2AJbJdgCX0VkCmKF2AJllWgKa8XYAm01aApzRdgCdYXoAnoFWAp/VdgCgBQIAoY1aAqI1VwKjCXYApCF2AKUtdgCmiVoCp5laAqi5WgKpdXYAql13ANkrAIDdKwCAESwAgDksAIBJLACAUSwAgFUsAIBhLACAfSwAgIEsAICZLACAnSwAgKUsAIC1LACAUS0AgGUtAIClLQCAuS0AgMEtAIDFLQCA1S0AgJl1CgD4LQCAJC4AgDAuAIBQLgCAXC4AgGAuAIBkLgCAgux6AINkewB8LgCAgC4AgIZ0ewCHvHsArC4AgLguAIDALgCAyC4AgNguAIDnLgCA7y4AgBsvAIAfLwCAJy8AgJJwfAArLwCAMy8AgJFMfAA7LwCASy8AgGcvAIDfLwCA8y8AgKvMfACo5HwAqdx8APcvAIB3MACAezAAgI8wAICiwHwAkzAAgJswAICjMACAzEBJAs0ASQLM/EoCzWhLAqswAIC3MACA7TAAgP0wAIARMQCAjjEAgJoxAICqMQCAsqx8ALNAfAC2MQCAwjEAgMoxAIDOMQCAtGx8ALUEfACAlQcAgZ0HAIKVBwCDqQcAhLkHAIW5BwCG2QcAh9kHAIjpBwCJ6QcAivkHAIv5BwCM6QcAjekHAI7RBwCP0QcAkLEHAJGxBwCSSQEAk0kBAJRZAQCVWQEAlkkBAJdJAQCYeQEAmXkBAJpJAQCbSQEAnFkBAJ1ZAQCeSQEAn0kBAKC5AQChuQEAoskBAKPJAQCk2QEApdkBAKbJAQCnyQEAqPkBAKn5AQCqyQEAq8kBAKzZAQCt2QEArskBAK/JAQCwuQEAsbkBALJJAQCzSQEAtFkBALVZAQC2SQEAt0kBALh5AQC5eQEAukkBALtJAQC8WQEAvVkBAL5JAQC/SQEA0jEAgNYxAIDaMQCAkjIAgNoyAIDmMgCA6jIAgO4yAIDyMgCA+jIAgP4yAIASMwCALjMAgDYzAIB2MwCAejMAgIIzAICGMwCAjjMAgJIzAIC2MwCAujMAgNYzAIDaMwCA3jMAgOIzAID2MwCAGjQAgB40AIAiNACARjQAgIY0AICKNACAqjQAgLo0AIDCNACA4jQAgAY1AIBKNQCAUjUAgGY1AIByNQCAejUAgII1AICGNQCAijUAgKI1AICmNQCAwjUAgMo1AIDSNQCA1jUAgOI1AIDqNQCA7jUAgPI1AID6NQCA/jUAgJ42AICyNgCAnoUMAOY2AIDqNgCA8jYAgIC5AwCBuQMAgskDAIPJAwCE2QMAhdkDAIbJAwCHyQMAiPkDAIn5AwCKyQMAi8kDAIzZAwCN2QMAjs0DAI/FAwCQvQMAkQEMAJJJDgCTSQ4AlFkOAJVZDgCWSQ4Al0kOAJh5DgCZeQ4AmkkOAJtJDgCcWQ4AnVkOAJ5JDgCfSQ4AoLkOAKG5DgCiyQ4Ao8kOAKTZDgCl2Q4ApskOAKfJDgCo+Q4AqfkOAKrJDgCryQ4ArNkOAK3ZDgCuyQ4Ar8kOALC5DgCxuQ4AskkOALNJDgC0WQ4AtVkOALZJDgC3SQ4AuHkOALl5DgC6SQ4Au0kOALxZDgC9WQ4AvkkOAL9JDgC8eQQAvXkEAL6JBAC/nQQAuHUEALl9BAC6aQQAu2kEALRxBAC1cQQAtnEEALdxBACwcQQAsXEEALJxBACzcQQArGkEAK1pBACucQQAr3EEAKhBBACpQQQAqkEEAKtBBACknQUApWEEAKZhBACnYQQAoJ0FAKGFBQCijQUAo4UFAJxdBQCdZQUAnm0FAJ9lBQCYXQUAmUUFAJpNBQCbRQUAlB0FAJVlBQCWbQUAl2UFAJAdBQCRBQUAkg0FAJMFBQCMMQcAjTEHAI4xBwCPMQcAiDEHAIkxBwCKMQcAizEHAIQxBwCFMQcAhjEHAIcxBwCAMQcAgTEHAIIxBwCDMQcAJjcAgC43AIA2NwCAcjcAgHY3AIB+NwCAgjcAgIY3AICyNwCAtjcAgL43AIDSNwCA1jcAgPI3AID6NwCA/jcAgCI4AIBCOACAUjgAgFY4AIBeOACAijgAgI44AICeOACAwjgAgM44AIDeOACA9jgAgP44AIACOQCABjkAgAo5AIAWOQCAGjkAgCI5AIA+OQCAQjkAgEY5AIBeOQCAYjkAgGo5AIB+OQCAgjkAgIY5AICOOQCAkjkAgJY5AICaOQCAnjkAgK45AIDGOQCAyjkAgNY5AIDaOQCA3jkAgOI5AIDqOQCA7jkAgPI5AID+OQCABjoAgA46AIASOgCAGjoAgIC5AQCBuQEAgskBAIPJAQCE2QEAhdkBAIbJAQCHyQEAiPkBAIn5AQCKyQEAi8kBAIzZAQCN2QEAjskBAI/JAQCQuQEAkbkBAJIRAACTEQAAlDEAAJUxAAAeOgCAIjoAgCo6AIAyOgCAPSMAgGUsAIBpLACAJSQAgIJgAgCZ4QAAgIAAAIGYAACC5AYAg4gEAITUGwCFlBoAhhgfALMjAICIxB4AiQAQAIqoEwCLrBEAjAAoAI20KwCOuCoAj7wpAOOwAgC+dAIAnlUAAOMUAgCCbAIAtyMAgJkNAAC+RAIAnjUAAIJoAgCZBQAAuyMAgO/MAgC+oAAAgoQAAO/YAgDj7AEA4/QBAL8jAIDjCAMAwyMAgOM4AwDHIwCA44gDAMsjAIDv4AMAzyMAgO+IAwDvPAEA78QDANMjAIDv1AMA4+wDAB43AIDXIwCA4+wDAOPsAwDj5AMA2yMAgOO4AwDvXAMA70wDAN8jAIDvSAMA7/QDAOMjAIDnIwCA7zQDAON8AwDjlAQA6yMAgO8jAIDzIwCA47QEAPcjAID7IwCA/yMAgO9sBAADJACAByQAgO9YBADvUAQACyQAgBYkAIAaJACAvQAAgOP4BADCAACAMSQAgB4kAIBtKQCA45wEAAglAIBrJQCAriUAgO9QBADaJQCABCYAgO88BAApJgCAgAlLAoYcdwC+RAIAgnQCAL5QAgA+JgCAmREBAJkNAQCPrAIAggQCAI1oAQCewQIAi3wBAJ49AQCeKQEAvggCAJfQAgCZXQEAldACAJ5VAQCT0AIAmXUBAJHQAgC+SAIAn7gCAEYmAICdtAIAnk0BAJuwAgCZXQEAmbQCAL6EAgCeqQEApowCAGImAICkgAIAmakBAGomAIChSAIAgqwCAK/kAgCCtAIAglwCAJnlAQC+CAIAgnwCAIIABACopAIAnvkBAL5wAgC1HAQAnoUBAL6oBQCyhAIAtrECAL6sBQC4KQkAuYkCALqZAgCCjAUAu+gEAIKcBQByJgCAuPAEAJ5ZBgCZbQYAnmEGAJl5BgC+fAIAnmEGAIJcAgC+QAIAmVkGAJ5dBgCCYAIAmaUGAL58AgCevQYAghwCAL4UAgCZzQYAvkwCAIJMAgCa3QYAnt0GAJ/FBgDjDAIAgrwCAJn5BgC+ZAIA7/QCAJrxBgCe6QYAn+kGAJ7ZBgCf1QYA4wQCAJklBgCaIQYAgngCAJk9BgDjBAIAgkQCAJolBgC+cAIA75wCAJ4FBgCfFQYA7+gCAJp1BgCZBQYAggQCAL5wAgDjcAIAnnUGAJ8NBgCeAQYAvnwCAOM0AgCZDQYAvmACAIJsAgDv8AIAmTUGAIKQAwDv2AIAniEGAIQmAICbxQcAmeUHAL58AgCe7QcAn8UHAOPsAwCdUAIAnNEHAIJsAgDv1AIAmc0HAIJ8AgC+cAIAmd0HAJ7dBwC+AAIA42gCAJ6tBwCZuQcA42gCAIJ8AgDjDAIAvkgCAJmpBwCCWAIA78QCAJ6ZBwC+bAIA77gCAIKUAgCejQcA77gCALsAAACZeQcAuQwAAJ5xBwC/AAAAglQCAL0EAAC+aAIAs9QDAJmxBgCxcAMAggQCALc4AACeoQYAtTQAAL5wAgCrWAMAnqEGAO9cAgCZqQYArxADAIJQAgCtFAMAmYUHAJlpBgC+WAIAnmEGAL58AgCCaAIApqACAOOQAgCZaQYA43wBAOOYAQDjrAEA49ABAOPoAQC+dAIAno0FAOMwAgDvzAIAgmgCAJnRBQDvlAIA71QBAO9wAQDvJAEA7ygBAL58AgCevQUA4wwCAIJ4AgCZrQIAvnQCAJ6lAgDjNAIAgmACAJkZAAC+YAIA7/wCAJ4NAACClAIA79QCAJAmAIDj/AIAmQkAAL5gAgCYJgCAnh0AAOMAAgCwJSoAglgCAJkNAADv9AIAvmQCAK4mAIDvwAIAnhkAAIIYAgCCOAIA43ACAJkRAACaNQAAmSkBAL50AgDsJgCAnyUAAJ4JAACZ6QEAvrQDAL7gAwCazQEA79gCAJ4RAQCC2AMA/SYAgIHEAgDjsAMAHycAgOP8AwC+/AIAhMQCAIIoAgCGEAIAKicAgIg8AgCeIQAAnw0AAHonAIDvKAMAj3QCAO8sAwCCiAIAmXUAAJoVAACSxAMAldADAJktAACa0QAAjicAgL7IAgCYaAMAm3wDAILEAwCeQQAAnykAALAnAICChAIA45ACAL4IAwC+JwCABigAgJ8ZAACe7QAA49ACAJlxAACaFQAAvhQCAO8wAgCZIQAA71gCABQoAICv7AMAggQCALFMHACwABwAniUAALJMHACeXQAAn2EAAOO8AgCZIQAA+QAAAHEpAIDvlAIAdSkAgL08HACCgB0Av8EfAHkpAIDjtB0AvnQCAJ71HwDj8B0AmQUAAH0pAIC+fAIAngkAAIJgAgCZDQAAiSkAgL5gAgDvzAIAnh0AAOklAIDv3AIA42gCAPkYAIDjPB0AIRoAgP0YAIABGQCAJRoAgCkaAIAtGgCAMRoAgDUaAIA5GgCA76QCAD0aAIDvJB0AQRoAgLHFAAAFGQCAs8UAALLdAAC1yQAAtMEAALcdAAC2wQAAuWUAALhlAAC7zQAAus0AAL3dAAC83QAAv8UAAL7JAAAJGQCADRkAgE0ZAIBhGQCAERkAgBUZAIDvFHgD7wBIA+HYTQPhOKgC41x5A+O0UAOtGQCAsRkAgLUZAIC5GQCAgMkBAIHVAQCC3QEAg20CAITdAQCFcQIAhgEEAIcdBQCIJQUAiTUFAIo9BQCLbQUAjHUFAI1lBQCObQUAj80BAJC1AQCRvQEAkrUBAJNNAwCUVQMAlV0DAJZVAwCXTQMAmHUDAJl9AwCadQMAm00DAJxVAwCdWQMAnkkDAJ9JAwCguQMAobkDAKLBAwCj3QMApMUDAKXNAwCmxQMAp/0DAKjJAwCpyQMAqtEDAKvRAwCsMQMArTEDAK4xAwCvMQMAsFEDALFRAwCyUQMAs1EDALRxAwC1cQMAtnEDALdxAwC4UQMAuVEDALpRAwC7UQMAvDEDAL0xAwC+MQMAvzEDAL0ZAIDBGQCAxRkAgMkZAIDNGQCA0RkAgNUZAIDZGQCA3RkAgOEZAIDwIAIA5RkAgOkZAIDtGQCA8RkAgPUZAICc9TYAnf02APkZAICRkAIA/RkAgKkZAIBFGQCASRkAgEUaAIC6adgASRoAgE0aAIC4sTYAubE2AFEaAIBVGgCAWRoAgF0aAIBRGQCAYRoAgGUaAIBVGQCAWRkAgF0ZAIBlGQCAaRkAgG0ZAIBxGQCAdRkAgHkZAIB9GQCAgRkAgIUZAICJGQCAjRkAgJEZAICVGQCAglgCAJkZAIBpGgCA8FgCAG0aAICdGQCAoRkAgKUZAIABGgCABRoAgJF0AwDhtDsCCRoAgOPYIgINGgCAERoAgBUaAIAZGgCAHRoAgKUqAIBVLQCAqSoAgMEqAICtKgCAljMAgO/IPwK1KgCA4ZTzAuGY0gLjlPcC4xDGAuGUtgLhkJ0C44SiAuMIhwIZGQCAHRkAgO+4swLvOIsCnSoAgOAtAIDvIJcC7+DgAoLkAgBpLQCACAIAgLrF2QAOAgCAFAIAgBoCAIAgAgCAJgIAgCwCAIAyAgCAOAIAgD4CAIBEAgCASgIAgFACAIDhgHgC8OQGAOMUagKCgAgA4aAPAuEIEwLjhA4C4xgeAlYCAIA0AwCA7zQ7Au8wHwI6AwCAQAMAgO8MEgJGAwCAJRkAgCkZAIBMAwCAUgMAgC0ZAIAxGQCAWAMAgF4DAIB2AwCAggMAgIgDAICOAwCAlAMAgJoDAIB8AwCAZAMAgDUZAIA5GQCAbQMAgFwCAIA9GQCAQRkAgHQCAIBoAgCAvAIAgHoCAICYAgCAYgIAgJICAIBuAgCApAIAgNQCAICAUQYAgV0GAIJVBgCDaQYAhHkGAIV5BgCGaQYAh2kGAIhZBgCJoQcAiqUHAIu9BwCMpQcAja0HAI6lBwDyAgCA7AIAgOACAICSCRQAkxUUAJTxBwCV8QcAlvEHAJfxBwCY0QcAmdEHAJo5FACb0QcAnIEHAJ2BBwCefQcAnx0UAJktAQCYLQEAmz0BAJo9AQCdLQEAnC0BACEZAICeVQEAkd0GAJDRBgCTJQEAkiUBAJUtAQCULQEAlx0BAJYdAQCJ8QYAiOkGAIvxBgCK+QYAjbEGAIzpBgCPqQYAjrkGAIHxBgCA7QYAg/EGAIL5BgCF0QYAhOkGAIfRBgCG2QYAua0DALitAwC7vQMAur0DAL2tAwC8rQMAv90DAL7dAwCxrQMAsK0DALO9AwCyvQMAta0DALStAwC3nQMAtp0DAKm5AQCosQEAq3UBAKqxAQCtFQEArBUBAK/dAwCu3QMAobkBAKCpAQCjiQEAorEBAKWZAQCkkQEAp4kBAKaRAQAuAwCAwgIAgM4CAIDmAgCA2gIAgAQDAICwAgCA+AIAgCIDAIAKAwCAngIAgIACAIC2AgCAyAIAgP4CAICGAgCAKAMAgKoCAIAQAwCAjAIAgBYDAIAcAwCACS0AgOsuAIDKNACAhAcAgAYFAIAVBQCAJAUAgDMFAIBCBQCASwUAgPAsOABUBQCAXQUAgGYFAICSBQCA40huA5sFAIDhTG4DpAUAgO/0AQOnBQCAqgUAgK0FAIBGOgCApkwAgNZVAIA2aACAZnEAgJZ6AID2jACAVp8AgIaoAIDtugCAJMQAgFTNAICE1gCAtN8AgDG7AIA6rgCABqUAgPkqAICJKwCAoSoAgOUqAIBBMQCAATEAgE40AIDVLACABjMAgIo3AIBiNACAHSwAgJI0AICeMwCAEjgAgFkrAICFLACA+jEAgCY5AIAdKwCArSsAgJ4xAIC8LgCAySwAgFksAIA4LgCALC4AgJGgBgDuMwCAGSsAgJ43AIB1LACAzS0AgLAFAIDh1D8D4VgaA+PcLwPjUA4D4RTyA+FA0wPjQOoD40DDA7MFAIC2BQCA73jrA+9c8gO5BQCA5QUAgO9E3gPvmCUD4bSLA+E8lwPjfKID45iLA+EwQQDhUKwD4xx/AOOIRgDoBQCA6wUAgO84ewDv4EEA7gUAgPEFAIDvzIoD7yCHA4DBGACB3RgAgikLAIMpCwCE6Q4AhekOAIYZDwCH8RgAiCUPAIntGgCK5RsAiyEdAIw5HQCN5RsAjmkQAI/VGgCQhRsAkU0PAJJFDwCTXQ8AlEUPAJVNDwCWRQ8Al30PAJhFDwCZTQ8AmkUPAJtpGwCcQQ8AnUEPAJ5BDwCfQQ8AoMEPAKHBDwCiwQ8Ao8EPAKS5CwCluQsApqkLAKfNDwCo9Q8Aqf0PAKr1DwCrzQ8ArNkPAK3ZDwCuyQ8Ar8kPALC5DwCxuQ8AsmkPALNpDwC0YQ8AtWEPALY5DwC3OQ8AuBEPALkRDwC66QEAu+kBALz5AQC9+QEAvukBAL/pAQD0BQCA9wUAgPoFAID9BQCAAAYAgCAGAIDhBACAgAUAgNMFAIAOBgCANAYAgEsGAIBoBgCAfwYAgJYGAIDdAwCA9gMAgA8EAIASBwCAQQgAgD4IAIA/BwCAOSQAgHIkAICjJACAyCQAgLkmAIDEJgCAyCYAgMwmAIDQJgCALygAgG4oAICWKACAmigAgL8oAIDHKACA4ygAgPUoAID5KACA/SgAgLrp0wAVKQCAMCkAgEspAIA9JACASiQAgFckAIBkJACAdiQAgIMkAICVJACApyQAgLckAIDMJACA1iQAgOQkAIDuJACA+yQAgAwlAIAWJQCAbyUAgHYlAIAkJQCAgBkDAIEZAwCCKQMAgykDAIQ5AwCFOQMAhikDAIcpAwCIGQMAiRkDAIppAwCLaQMAjHkDAI15AwCOaQMAj2kDAJAZAwCRGQMAkgEEAJMtAwCUNQMAlVUGAJZdBgCXVQYAmG0GAJl1BgCafQYAm3UGAJxtBgCdNQYAnj0GAJ81BgCgzQYAodUGAKLdBgCj1QYApPkDAKX5AwCm6QMAp+kDAKjZAwCp+QYAqikGAKspBgCsOQYArTkGAK7FAwCvPQMAsEUDALFNAwCyRQMAs10DALRFAwC1TQMAtkUDALd9AwC4SQMAuUkDALpZAwC7fQYAvGUGAL1tBgC+ZQYAgCUAgKkVDwCoAQ8Aq00PAKpNDwCtRQ8ArEUPAK+hDQCuqQ0AoXULAKBhCwCj7QsAoqkLAKXlCwCk5QsApzkPAKZZCAC5oQ0AuJkNALuhDQC6qQ0AvaENALy5DQAxJQCAvqkNALGhDQCw2Q0As6ENALKpDQC1oQ0AtLkNALehDQC2qQ0AOCUAgEglAIBbJQCAsiUAgLwlAICRJQCAoSUAgNAlAICB7Q0AgO0NAIP9DQCC/Q0Ahe0NAITtDQCH2Q0AhiEYAJlNDQCYTQ0Am1ENAJpdDQCdeQ0AnHUNAJ9pDQCecQ0AkYkNAJCBDQCTmQ0AkoENAJWJDQCUgQ0Al30NAJaBDQDgJACAICUAgI0lAIDMJQCA3iUAgAgmAIAtJgCAQiYAgPAlAID6JQCADCYAgBkmAIAxJgCATiYAgFgmAIB2JgCASiYAgGYmAIBuJgCAgCYAgIwmAICUJgCAoyYAgN4mAICcJgCAsiYAgKcmAIC9JgCA1CYAgOImAIABJwCAEScAgBsnAIBPJwCAkicAgOcnAIBPKQCAXSkAgGEpAIBlKQCA8CYAgC4nAIA+JwCASCcAgCMnAIBTJwCAYycAgH4nAIBwJwCAlicAgMInAIDJJwCApicAgNMnAIDdJwCAtCcAgBgoAIAKKACA6ycAgCUoAIDyJwCA/CcAgDMoAIBAKACASigAgFQoAIBeKACAcigAgH8oAICGKACAnigAgKUoAICyKACAyygAgNUoAIDnKACAASkAgA4pAIAZKQCAIykAgDQpAIA7KQCAUykAgMMDAIDmBACAhQUAgNgFAIATBgCAOQYAgFAGAIBtBgCAhAYAgJsGAIDjAwCA/AMAgBUEAIAoBACAOwQAgE4EAIBhBACAdAQAgIcEAICaBACAAAUAgA8FAIAeBQCALQUAgDwFAIBjCACAJAgAgMEGAID8BwCAHQkAgOMoEwAzCQCAKggAgC0IAIAxCACAJAcAgNwuAIDKMACA2S0AgLswAIBFMQCAJwkAgO/sEwAGCQCA3A0AgM8IAICDCACAMQcAgEwHAID8BgCACggAgJQIAIAqCQCACQkAgOANAIDsDQCA2wgAgJkIAIAVBwCAhggAgFUHAID/BgCApgcAgJEkAIDwDQCA4ggAgCcIAICcCACAWAgAgBUJAID0DQCA5QgAgBQIAICfCACA6AgAgBcIAIDJCACAoggAgOwIAIAbCACAzAgAgKYIAID3CACA/QgAgIgHAICKCACAWQcAgAMHAIA9CQCAQQkAgEkJAIA2CQCAGAkAgPgNAID0CACALQkAgAwJAIDkDQCA0ggAgI4IAIBdBwCAMAkAgA8JAIDoDQCA1QgAgJEIAIBgBwCArQgAgGMHAIDjSBIA4xQSAOP4EwDjuBMA4+wSAOOgEgDjbBIA43gSAO/ADQDv2A0A73QSAO9QEgDvqBIA79wSAO8oEwDvIBMA6QcAgMwGAIAOCACAEQgAgNgGAIDUBgCAIQgAgAcHAIBnCACADAcAgHYIAIA0BwCANwcAgKoIAIC2CACAuQgAgOPYEADjoBAA46AQAON0EQDjNBAA4wgQAOPkEADj9BAA77wQAO/gEADvzBAA7zgQAO8QEADvcBAA73AQAO9MEADjhBMA4+gTAOMwEADjEBAA42ATAONAEwDjpBMA47QTAO/IEwDvtBMA75gTAO98EwDvXBMA70wTAO8UEwDv6BAAgO08AIH1PACC/TwAg/U8AITtPACFFT0Ahh09AIcVPQCILT0AiTU9AIo9PQCLNT0AjC09AI0VPQCOHT0AjxU9AJBtPQCRdT0Akn09AJN1PQCUbT0AlRU9AJYdPQCXFT0AmC09AJk1PQCaPT0AmzU9AJwtPQCdFT0Anh09AJ8VPQCg7T0AofU9AKL9PQCj9T0ApO09AKUVPQCmHT0ApxU9AKgtPQCpNT0Aqj09AKs1PQCsLT0ArRU9AK4dPQCvFT0AsG09ALF1PQCyfT0As3U9ALRtPQC1FT0AthE9ALcRPQC4MT0AuTE9ALoxPQC7MT0AvBE9AL0RPQC+ET0AvxE9AIDxPACB/TwAgvU8AIMNPwCEFT8AhR0/AIYVPwCHDT8AiDU/AIk9PwCKNT8Aiw0/AIwVPwCNHT8AjhU/AI8NPwCQdT8AkX0/AJJ1PwCTDT8AlBU/AJUZPwCWCT8Alwk/AJg5PwCZOT8Amgk/AJsJPwCcGT8AnRk/AJ4JPwCfCT8AoPk/AKH5PwCiCT8Aowk/AKQZPwClGT8Apgk/AKcJPwCoOT8AqTk/AKoJPwCrCT8ArBk/AK0ZPwCuCT8Arwk/ALB5PwCxeT8Asgk/ALMJPwC0GT8AtRk/ALYJPwC3CT8AuDk/ALk5PwC6CT8Auwk/ALwZPwC9GT8Avgk/AL8JPwCA+TwAgfk8AIJJPQCDST0AhFk9AIVZPQCGST0Ah0k9AIh5PQCJeT0Aikk9AItJPQCMWT0AjVk9AI5JPQCPST0AkDk9AJE5PQCSAQQAk00GAJRVBgCVXQYAllUGAJdNBgCYdQYAmX0GAJp1BgCbTQYAnFUGAJ1dBgCeVQYAn00GAKC1BgChvQYAorUGAKPNBgCk1QYApd0GAKbVBgCnzQYAqPUGAKn9BgCq9QYAq80GAKzVBgCt3QYArtUGAK/NBgCwtQYAsb0GALK1BgCzTQYAtFUGALVdBgC2VQYAt00GALh1BgC5fQYAunUGALtNBgC8VQYAvV0GAL5VBgC/TQYArH0/AK2lPwCurT8Ar6U/AKh9PwCpZT8Aqm0/AKtlPwCkHT8ApUU/AKZNPwCnRT8AoB0/AKEFPwCiDT8AowU/ALydPwC9pT8Avq0/AL+lPwC4nT8AuYU/ALqNPwC7hT8AtN0/ALWlPwC2rT8At6U/ALDdPwCxxT8Ass0/ALPFPwCMZToAjW06AI5lOgCPfToAiEU6AIlNOgCKRToAi306AIRlOgCFbToAhmU6AId9OgCABToAgQ06AIIFOgCDfToAnF04AJ3lPwCe7T8An+U/AJhdOACZRTgAmk04AJtFOACUuTgAlWU4AJZtOACXZTgAkAU6AJENOgCSBToAkwE5AMAIAIDYCACA3ggAgPAIAIB2BwCAIgkAgHkHAICBBwCAVAkAgJ0HAIDLBwCAvQcAgMQGAIDcBACAewUAgM4FAIAJBgCALwYAgEYGAIBjBgCAegYAgJEGAIDXAwCA8AMAgAkEAIAiBACANQQAgEgEAIBbBACAbgQAgIEEAICUBACA+gQAgAkFAIAYBQCAJwUAgDYFAIBFBQCATgUAgFcFAIBgBQCAaQUAgJUFAICeBQCAXQgAgFYOAIBZDgCAOjoAgKwKAIAVCwCANjoAgD46AICcGQAAnRkAAJ45AACfOQAA4wwAgEI6AIB6NwCA8TAAgKI3AIBaMgCAxSoAgLksAICaMDUA7C0AgB0tAIDoLQCA1y8AgJ+ENQDSMwCAnUQpAGI1AICaNgCA1jYAgAo3AIAeOACAdjEAgAIyAICuMgCARjMAgGI2AIBGOACAcjkAgOkqAICNLACAijEAgNIyAICWNgCAwjkAgJQuAIB6MgCAhjYAgBo3AIALMACAvjUAgLSAGgC1hBkAtojmALeM5ACwABwAsZQeALIAGACznBsAvADsAL2k7wC+qO4Av6TtALgA4AC5tOMAurjiALu84QCkwAAApQAMAKbIDgCnAAgA4jYAgAcvAIAFMQCArXwDAKwAEACt5BMArugSAK9gEQCo8AoAqRwJAKr4FgCr/BQAGjIAgB4zAIAqOACAKSsAgMErAIAtLACAczAAgIIxAIDOMgCA8jMAgI42AICmNgCAyjcAgO44AICiOQCAvjkAgC40AIBuNACAvAgAgCY1AIBGNgCAejgAgE43AIChLQCAIy8AgN40AICeNQCAAjMAgDY0AICaNwCA5jgAgJ0tAIBwLgCAejEAgC4yAIBiMgCAFjUAgD41AICmOACAKSwAgJwAAACqNQCAzSsAgMkrAICaNACAKjUAgF42AICuOACAajcAgA8wAIBaNwCA0SoAgEQuAIB7LwCAMjMAgLIzAIBNLACAPjQAgDkrAIBfLwCAsSoAgO4xAICLMACAEjUAgIDpAwCB6QMAgjkvAIP9AwCE5QMAhe0DAIblAwCHfS4AiEEuAIkhAgCKeS8AiyUCAIw9AgCNJQIAjiECAI8dAgCQZQIAkW0CAJJlAgCTfQIAlGUCAJVtAgCWZQIAlx0CAJglAgCZLQIAmiUCAJs9AgCcJQIAnS0CAJ4lAgCfHQIAoOUCAKHtAgCi5QIAo/0CAKTlAgCl7QIApuUCAKdNAgCodQIAqX0CAKqpAQCrqQEArLkBAK25AQCuqQEAr6kBALDZAQCx2QEAsukBALPpAQC0eSIAtf0BALb1AQC37QEAuNUBALndAQC61QEAu60BALy1AQC9uQEAvqkBAL+pAQChLACAjS0AgP4zAIBmNgCAPjcAgLoxAIDmMQCAHzAAgB42AIA/MACArjMAgAUrAICBKwCAxSsAgFYxAID+NACA9jUAgEo3AIBaOACANSwAgOksAIAXLwCApzAAgH4yAIBCNACAljgAgHo5AIDOOQCA5jkAgOkwAICmMQCA7jcAgOMuAIC/LwCA2y8AgGswAIBuMgCAujIAgGozAICONACAMjUAgJY1AIDeNwCAbjYAgAY4AIB+OACA6SsAgBUsAID9LACAqjIAgPY2AIADLwCAcy8AgDcwAICyMQCA2jQAgCYzAIAVKwCAWS0AgKguAIB/LwCAQjMAgF4zAIBuNQCAgFEBAIEBKgCCXQEAg1UBAIRNAQCFdQEAhn0BAId1AQCITQEAiVUBAIqdKwCLWQEAjEkBAI1JAQCOuQEAj7kBAJDJAQCRyQEAktkBAJPZAQCUyQEAlckBAJb5AQCX+QEAmMkBAJnJAQCa2QEAm9kBAJzJAQCdyQEAnrkBAJ+5AQCgSQEAoZUBAKJFAQCjXQEApEUBAKVNAQCmRQEAp30BAKhFAQCpTQEAqnkPAKtBAQCsQQEArUEBAK5BAQCvQQEAsMEDALHBAwCywQMAs8EDALTBAwC1wQMAtsEDALfBAwC4wQMAucEDALrBAwC7wQMAvMEDAL3BAwC+wQMAv8kMAI41AIBiOACA4jgAgPI4AIAuOQCALSsAgII0AIBOOACAyjgAgJcvAIDxKgCAUSsAgEguAIBoLgCAlzAAgMYyAIDOMwCAejYAgBo4AIDZMACAojgAgA0sAIAlMQCAMTEAgBIyAIBKMgCATjMAgKozAIAqNACADjUAgDo5AIDrLwCAsjgAgEErAICMLgCAMjIAgOI3AIBPLwCAny8AgDkxAIC6OACA8SsAgNksAIB4LgCAwjAAgBUxAIBiMQCA9jEAgEozAIC+MwCAWjUAgPo2AIAGNwCA1jgAgF0sAIBOMgCA3SwAgMoyAIBuMwCAijYAgL44AICqOQCA0jkAgC0xAICxOSMAsBEDALMVAwCyFQMAtTUDALQ1AwC3NQMAtjUDALkVAwC4FQMAuxUDALoVAwC9dQMAvHUDAL91AwC+dQMAoZkNAKCRDQCjqQ0AopENAKW5DQCksQ0Ap6kNAKaxDQCpmQ0AqJENAKtpAwCqkQ0ArXkDAKxxAwCvaQMArnEDAJEZDQCQEQ0Aky0NAJIRDQCVPQ0AlD0NAJctDQCWLQ0AmR0NAJgdDQCbbQ0Amm0NAJ15DQCcgQ4An2kNAJ5xDQCBmQ0AgAkjAIOpDQCCkQ0AhbkNAISxDQCHqQ0AhrENAImZDQCIkQ0Ai2kNAIqRDQCNeQ0AjHENAI9pDQCOcQ0AKjIAgMY1AIDGNACA6jQAgBozAICiMgCAZjcAgA0rAIAuNgCA9SsAgOUrAIDzLgCAEzAAgPY0AIA0LgCABjIAgOUwAIDqNwCAqjgAgA8vAIBhKwCANS0AgIktAIDVMACA0SsAgCIzAIDmMwCASjQAgGY0AIBqNACAfjQAgPo4AIDuNACAkjYAgFY3AIAKOACANjgAgE45AIBSOQCAVjkAgLo5AIAuOACAxjgAgDErAIBVKwCAaSsAgCUsAIAxLACAcSwAgCUtAIBBLQCASS0AgIUtAICRLQCAdC4AgIsvAICzLwCAuy8AgJH4EADTLwCAfzAAgK8wAIDdMACAWjEAgIApAQCBKQEAgjkBAIM5AQCEKQEAhSkBAIZZAQCHWQEAiNkoAIltAQCKKSUAi2EBAIxhAQCNYQEAHjIAgDoyAICQGQEAajIAgJIVAQC+MgCA3jIAgJU1AQCWPQEAlzUBAJgNAQCZFQEAmh0BAJsVAQCcDQEAnfUBAJ7dKABSMwCAoAUBADI0AICiAQEAVjQAgFI0AIClGQEApgkBAFo0AIBeNACAdjQAgKo9AQCrNQEArC0BAK0VAQCuHQEArxUBALBtAQCxdQEAsn0BALN1AQC0bQEAtRUBALYdAQC3FQEAuC0BALk1AQC6PQEAuzUBALzZLgC9KQEAvhkBAL8ZAQC6eR4Au3keALjNAgC5eR4AvpUeAL+dHgC8QQIAvZ0eALJ9HgCzRR4AsH0eALF1HgC2XR4At0UeALRdHgC1VR4AqgUeAKsNHgCodR4AqQ0eAHo0AICeNACArBUeAK0NHgCiSR4Ao0keAKBJHgChSR4ApkkeAKf5AgCkSR4ApUkeAJqNHgCblR4AmI0eAJmFHgCeiR4An4keAJyNHgCdhR4AkgUDAJP1AACQCQMAkY05AJaxHgCXFQYAlO0AAJUBHACKvQMAi0EDAIiFAwCJnQMAjkEDAI9JAwCMyTkAjVEDAIIVAgCDHQIAgAUCAIEdAgCGzQMAh7EDAIQFAgCFxQMAs/kFALLxBQCx+QUAsOEFALeZKgC2EQMAtRkDALThBQC7NQMAujUDALklAwC4JQMAvxUDAL4VAwC9JQMAvCUDAKP9BQCi/QUAof0FAKD9BQCnnQUApp0FAKWdBQCknQUAq7kFAKqxBQCpJScAqL0FAK+ZBQCukQUArZkFAKyhBQCTAQUAkvkFAJF1OQCQ9QUAlwEFAJYZBQCVEQUAlBkFAJt5CQCaOQUAmTEFAJg5BQCfHQUAnh0FAJ0dBQCcHQUAg4kFAIKBBQCBiQUAgPEFAIeFBQCGhQUAhZUFAISBJgCLhQUAioUFAIm1BQCItQUAj4UFAI6FBQCNlQUAjJUFAM40AIA6NQCAQjUAgFY1AIB+NQCAzjUAgAI2AIBqNgCAEjcAgCo3AIBeNwCAYjcAgKY3AICqNwCAAjgAgNo4AIAeOQCANjkAgIMvAICQ6gCA5jUAgLkqAIC9KwCAfSsAgCUrAIBlKwCAkSsAgCEsAIA9LACAES0AgCEtAIA9LQCAmS0AgOQtAIDwLQCADC4AgBwuAIALLwCAEy8AgEMvAIBjLwCAky8AgKsvAICbLwCAry8AgO8vAIBHMACAUzAAgFswAICDMACACTEAgB0xAIBeMgCAVjIAgIYyAIAWNACA4jIAgBYzAIBiMwCAfjMAgKIzAIDGMwCAyjMAgOozAICAjQEAgZUBAIKdAQCDlQEAhI0BAIW1AQCGvQEAh7UBAIiNAQCJwR0AipkBAIvBHQCMhQEAjY0BAI6FAQCP/QEAkIUBAJEZHQCSkRQAk4UBAJSdAQCViTIAlk0ZAJc9GwCYsQEAmbEBAJotHACbtQEAnD0cAJ2pAQCemQEAn5kBAKDlHQChbQEAomUBAKN9AQCkZQEApW0BAKbxHQCnYQEAqKEDAKmhAwCqoQMAq6EDAKyhAwCttQEArq0DAK+lAwCwYRkAsdkDALLZAQCz7QMAtPUDALX9AwC29QMAt+0DALjFAQC50QMAumEdALvVAwC82QEAvT0XAL7FAwC/0QEA+jMAgA40AIAKNACAOjQAgLY0AIDmNACAHjUAgE41AIAyNgCAWjYAgM42AIAWNwCAIjcAgEI3AIBGNwCAUjcAgG43AIDmNwCAFjgAgEo4AIBqOACAtjgAgA45AIAqOQCAijkAgCfqAIAi6gCAVOoAgOEpAIAJKgCADSoAgNbqAIAD6wCAe+sAgBY6AIAmOgCARwgAgFIIAIBVCACASggAgE4IAIBXCQCA8Q4AgOIOAIDnDgCA9g4AgOwOAICyNACASw8AgMoPAICBDwCALw8AgFoPAIBnDwCAbw8AgJ0PAIDCDwCAuA8AgL0PAICqDwCAsQ8AgP4OAIADDwCACA8AgIBBAQCBMQMAgk0BAINFAQCEXQEAhUUBAIZNAQCHIQMAiF0fAIl9AQCKaQMAi3EBAIx1AwCNVQEAjlk6AI9ZAQCQKQEAkSkBAJI5AQCTOQEAlCkBAJUpAQCW2QEAl9kBAJjpAQCZ6QEAFQ8AgCIPAIAqDwCAMg8AgDwPAIBBDwCARg8AgFAPAIBVDwCAXQ8AgGoPAIByDwCAdw8AgHwPAICEDwCAiQ8AgJMPAICYDwCAoA8AgKUPAIDFDwCANw8AgBoPAIBiDwCAjg8AgA0PAIDdFgCA5hYAgOkWAIDvFgCA4xYAgOwWAIDgFgCAExcAgBYXAID1FgCA8hYAgPgWAICAmQcAgZkHAPsWAICDrQcAhLUHAAQXAICGsQcAh7EHAIiRBwCJkQcAipEHAIuRBwCM8QcAjfEHAI7xBwCP8QcAkJEHAJGVBwCSnQcAk5kHAJSFBwCVgQcAloEHAJeFBwCYuQcAmb0HAJq1BwCbsQcAnK0HAJ2pBwCemQcAn50HAKBhBwChZQcAom0HAKNpBwCkdQcApXEHAKZxBwCndQcAqEkHAKlNBwCqRQcAq0EHAKxdBwCtWQcArkkHAK9NBwCwMQcAsTUHALI9BwCzOQcAtCUHALUhBwC2IQcAtyUHALgZBwC5HQcAuhUHALsRBwC8DQcAvQkHAL7xAAC/9QAAgAkBAIENAQCCHQEAgxkBAITZAACF3QAAhtUAAIfRAACI8QAAifUAAIr9AACL+QAAjOkAAI3tAACO5QAAj+EAAJCdAACRmQAAkq0AAJOpAACUtQAAlbEAAJaxAACXtQAAmIkAAJmNAACahQAAm4EAAJydAACdmQAAnokAAJ+NAACgdQAAoXEAAKJ9AACjeQAApGlQAqVtUAKmYQAAp2UAAKhZAACpXQAAqlUAAKtRAACsTQAArUkAAK49AwCvOQMAsClQArEtUAIBFwCABxcAgP4WAIANFwCAChcAgBkXAIDZXFICHxcAgCUXAIAiFwCAKBcAgCsXAIA0FwCALhcAgKOhAACipQAAoZEAAKCVAACntQAAprEAAKW9AACkuQAAq40AAKqJAACpgQAAqIUAAK+FAACugQAArYkAAKyNAACz/QAAsvkAALHxAACw9QAAt5kAALadAAC1nQAAtJkAALutAAC6qQAAuaUAALilAAC/ZQEAvmEBAL1tAQC8aQEAHBcAgFcXAIBAFwCAPRcAgEgXAIBOFwCAOhcAgNksUQJLFwCAVBcAgHkWAIDhDwCAMRAAgA4QAIAiEACAHRAAgJNBAAAnEACALBAAgBMQAICXWQAAllUAAJVZAACUXQAAm3EAAJppAACZZQAAmGUAAJ9lAACeYQAAnTFTApxtAAC4gQQAuYEEALqBBAC7gQQAvIEEAFEXAIC+jQQA5g8AgLDdBQCxTQQAskUEALNdBAC0RQQAtU0EALZFBADrDwCAqKEFAKntQQCqrQUAq6UFAKy9BQCtpQUArq0FAK+lBQCgqQUAoZFBAKKpQACjoQUApKEFAKWhBQCmoQUAp6EFAP8PAIAYEACAWBAAgF0QAIBpEACAnVUFAH8QAICfWQUAjhAAgJMQAICeEACAkwUFAJQdBQCVBQUAlg0FAJcFBQC4EACAyxAAgO8QAIAhEQCAJhEAgC4RAIA9EQCATBEAgIBxBQCBcQUAgnEFAINxBQCEUQUAhVEFAIZdBQBREQCAWREAgHwRAICjEQCArxEAgM8RAIDUEQCA2REAgBMSAIAmEgCAMhIAgEoSAIDEEgCAGhMAgDMTAIA4EwCASxMAgFwTAIBuEwCAcxMAgJoTAICiEwCAtxMAgN4TAIDjEwCAPRQAgEIUAIBHFACAUxQAgF8UAIBkFACAbBQAgHgUAICSFACAlxQAgJ8UAICkFACAqRQAgK4UAICzFACAuBQAgMsUAIDQFACA7BQAgAYVAIAgFQCALBUAgEQVAIBJFQCAVhUAgHcVAICaFQCAtBUAgMAVAIDFFQCAzRUAgO4VAIAIFgCAFxYAgDQWAIA5FgCAQRYAgEYWAIBZFgCAXhYAgICtAQCBtQEAgr0BAIO1AQCErQEAhdUBAIbdAQCH1QEAiO0BAIn1AQCK/QEAi/UBAIztAQCN1QEAjt0BAI/VAQCQrQEAkbUBAJK9AQCTtQEAlK0BAJVVAwCWXQMAl1UDAJhtAwCZdQMAmn0DAJt1AwCcbQMAnVUDAJ5dAwCfVQMAoK0DAKG1AwCivQMAo7UDAKStAwCl1QMAphkOAKfZAwCobQ8AqSEOAKrhAwCr4QMArCkOAK3lAwCuGQ4ArxkOALCVAwCxnQMAsgEOALORAwC0HQ4AtQUOALa5AwC3uQMAuDkOALmNAwC6NQ4AuxEOALyBAQC9gQEAvnkBAL95AQCEFgCAkBYAgJwWAICrFgCAyBYAgM0WAIDuEQCA/xEAgHwWAICBAACAiwAAgJUAAICfAACAqQAAgLMAAID1DwCA+g8AgAQQAIB1EACAehAAgIQQAIDlEACA6hAAgBcRAIAzEQCAOBEAgEIRAIBRFQCADRYAgBIWAIAqFgCAoRYAgKYWAIC+FgCA8A8AgAkQAICJEACAHBEAgNcSAIA/FQCALxYAgGMWAIDDFgCARxEAgGQSAICfEgCAshIAgBEUAIAdFACAKRQAgI0TAICSEwCA0RMAgNYTAID9EwCAAhQAgGkSAIBuEgCAtxIAgLwSAIDCEQCAxxEAgJYRAICbEQCApD0DAKVFAwCmTQMAp0UDAKA9AwChJQMAoi0DAKMlAwCsfQMArUUDAK5NAwCvRQMAqH0DAKllAwCqbQMAq2UDALQ9AwC1xQMAts0DALfFAwCwPQMAsSUDALItAwCzJQMAvP0DAL3FAwC+zQMAv8UDALj9AwC55QMAuu0DALvlAwCEBQwAhQ0MAIYFDACHHQwAgI0MAIGpDACCGQwAg1ENAIxhDACNYQwAjmEMAI9hDACIKQwAiRUMAIodDACLFQwAlD0MAJXFAwCWzQMAl8UDAJABDACRAQwAkgEMAJMBDACc/QMAncUDAJ7NAwCfxQMAmP0DAJnlAwCa7QMAm+UDAIBpBACBaQQAgnEEAINxBACEnQQAhYUEAIaNBACHhQQAiL0EAImNBACKhQQAi50EAIyFBACNqQYAjvkEAI/5BACQiQQAkYkEAJKRBACTkQQAlLEEAJWxBACW+QYAl60EAJiVBACZwQYAmmkGAJtpBgCceQYAnXkGAJ7RBgCf/QsAoA0GAKEdCwCiGQYAo0ULAKQFBgClTQsApjUGAKe1BACoEQYAqREGAKoRBgCrNQQArC0EAK0BBACuXQQArx0GALDNBgCxbQYAsnUGALMNBgC0FQYAtR0GALYVBgC3DQYAuDUGALk9BgC6NQYAuw0GALwVBgC9HQYAvhUGAL8NBgCA9QcAgf0HAIL1BwCD9QAAhO0AAIURAwCGEQMAhxEDAIgxAwCJMQMAijEDAIsxAwCMhQcAjRUDAI4dAwCPFQMAkG0DAJGNBwCShQcAk50HAJSFBwCVjQcAloUHAJe9BwCYhQcAmY0HAJqFBwCbnQcAnIUHAJ2NBwCehQcAn4UAAKB9AAChgQMAooEDAKOBAwCkgQMApYEDAKaBAwCngQMAqBUHAKmFAwCqjQMAq4UDAKydAwCtoQMArqEDAK+hAwCwdQcAsXUHALJxBwCzhQUAtM0FALX1BQC2/QUAt8kDALj5AwC5+QMAuqEFALuhBQC8wQMAvcUDAN4RAIDjEQCAhJz7ACYTAIArEwCAYRMAgGYTAIB2EgCAghIAgJUSAICaEgCARRIAgNwSAIBXEwCASxAAgKMQAIC9EACAxBAAgJB1AACRfQAAknEAAJNxAACUAfwAlVX+AJZd/gCXVf4AmG3+AJlp/gCaef4Am3n+AJxp/gCdaf4Anln+AJ9Z/gCgpf4Aoa3+AKKl/gCjof4ApKH+AKWl/gCmrf4Ap6X+AKiZ/gCpmf4Aqun+AKvt/gCs9f4ArfH+AK7x/gCv8f4AsI3+ALGV/gCymf4As5n+ALSJ/gC1if4Atrn+ALe9/gC4hf4AuY3+ALqF/gC7nf4AvIX+AL2B/gC+gf4Av4H+AKbZCACnBQcApMEIAKWZBQCi0QgAo9EIAKCJBQChtQgArgEHAK8BBwCsMQcArTEHAKo9BwCrJQcAqD0HAKk1BwC2fQcAtwUHALR9BwC1dQcAsskFALNlBwCwcQcAsXEHAL4BBwC/AQcAvDEHAL0xBwC6IQcAuyEHALg9BwC5MQcAhjkHAIc5BwCELQcAhTkHAIINBwCDNQcAgBEHAIEFBwCOSQcAj0kHAIxNBwCN1QUAisEFAIvBBQCI1QUAiXEHAJbVBQCX2QgAlE0FAJXdBQCSUQUAk9kFAJD5BQCRoQUAnnEIAJ99CACcYQgAnWEIAJpxCACbeQUAmMUIAJl1BQD0EACA+xAAgAIRAICBEQCAuxEAgLQRAIArEgCAGBIAgB8SAIBWEgCATxIAgF0SAIDJEgCAHxMAgIcSAIB7EgCApBIAgKsSAIA9EwCAUBMAgHgTAIB/EwCAhhMAgKcTAIC8EwCAwxMAgOgTAID2EwCA7xMAgEwUAIB9FACAhBQAgAsVAIAZFQCAEhUAgPEUAIAlFQCAMRUAgHwVAICDFQCAkxUAgFsVAIBpFQCAnxUAgKYVAIBiFQCASxYAgFIWAIDzFQCA+hUAgNkVAIDgFQCAIxYAgBwWAICwFgCAbhAAgLEQAICqEACA3hAAgNcQAIAQEQCACREAgI8RAIBeEQCAgIEBAIGBAQCCgQEAg4EBAISdAQCFhQEAhokBAIeJAQCItQEAib0BAIq1AQCLjQEAjJUBAI2dAQCOlQEAj40BAIgRAIA3EgCAkv0BAJP1AQCU7QEAlZUBAJadAQCXlQEAmKkBAJmpAQCauQEAm7kBAJypAQCdrQEAnqUBAJ+dAQCgZQEAoW0BAKJlAQCjfQEApGUBAKVtAQCmZQEAp90AAKjlAACppQMAqq0DAKulAwCsvQMAraUDAK6tAwCvpQMAsN0DALHlAwCy7QMAs+UDALSpAQC1VQEAtvUDALftAwC41QMAud0DALrVAwC7rQMAvM0DAL3BAwC+vQMAv7UDANASAICOEgCARBMAgP8UAIA4FQCAlRYAgIkWAIC3FgCAuRUAgIsUAIABFgCAyhMAgMQUAIDSFQCArRUAgPgUAIC9FACAZREAgKgRAIBwFQCA0BAAgFgUAIBiEACAPhIAgOcVAIATEwCAcRQAgEIQAIA5EACAihUAgOESAID2EQCArhMAgGsWAIDqEgCA8RIAgGwRAIAEEgCApgMAgA0jAIARIwCAoAYAgMcAAIC1BgCAqyMAgK8jAIC5IQCAtSEAgOMHAIB7CQCAfwkAgEEjAICnIwCANSMAgDkjAIAdIwCAISMAgCUjAIApIwCALSMAgDEjAIDbBwCA3wcAgNEAAICATQEAgVEBAIJRAQCDTQEAhE0DAIUhAwCGRQEAh30BANcAAICiAwCAqAMAgN0HAIDTAACA1QAAgL0GAIB5AACABxQAgH0AAICHAACAkQAAgAwUAICbAACAGBQAgKUAAIAkFACArwAAgDAUAIC5AACANRQAgM8PAIBVEACAmBAAgJsQAIArEQCAVhEAgKARAIDMEQCA6BEAgOsRAIDzEQCADRIAgBASAIBzEgCAwRIAgDATAIBrEwCAlxMAgJ8TAICwpQEAsa0BALKlAQCzvQEAtKUBALWtAQC2pQEAt10BALhlAQC5bQEAumUBALt9AQC8ZQEA2xMAgDoUAIBpFACAgAW5AIHhBgCC4QYAg+EGAIThBgCoBgCAswYAgIfpBgCI2QYAifmxAIr1sQCL8bEAjO2xAI31BgCO+QYAj/0GAJDZBgCR2QYAkvWxAJwUAICUiZIClfEGAJb1BgCX9QYAmNkGAJnVsgCa3bIAm6kGAJy5BgCduQYAnqkGAJ+BBgCgoQcAoaEHAKIhsgCjpQcApIUAAKWNAACmQbMA1RQAgKiNBwCplQcAqp0HAKuVBwBOFQCAyhUAgDYQAIA+FgCAsP0HALGFBwCyjQcAaBYAgLSZBwCBFgCAtpUHALeNBwC4tQcAub0HALq1BwC7jQcAvJUHAL2dBwC+lQcAv40HAIB1BgCBlaACgpmgAoOZoAKEhaAChb2gAoaxoAKHhaACiLmgAomRoAKKnaACi5mgAoyFoAKNjQEAjoEBAI9FBgCQOQYAkT0GAJIxBgCTMQYAlC0GAJXVBgCW2QYAl90GAJjhBgCZ4QYAmu0GAJvpBgCc9QYAnf0GAJ7xBgCf9QYAoAkGAKEJBgCiBQYAowEGAKQdBgClBQYApgkGAKcNBgCoMQYAqTEGAKo9BgCrNQYArCkGAK0pBgCuJQYArx0GALBhBgCxYQYAsm0GALNpBgC0dQYAtX0GALZxBgC3dQYAuEkGALlJBgC6RQYAu0EGALxdBgC9RQYAvkkGAL9NBgCAsQUAgbEFAIK9BQCDuQUAhKUFAIWtBQCGoQUAh6UFAIiZBQCJmQUAipUFAIuRBQCMjQUAjcEFAI7NBQCPyQUAkLUFAJG9BQCSsQUAk7UFAJSpBQCVqQUAlqUFAJehBQCYnQUAmSkCAJolAgCbIQIAnD0CAJ3pAgCe5QIAn+ECAKAdAgChNQIAojkCAKM9AgCkIQIApSECAKYtAgCnKQIAqBUCAKkZAgCqFQIAqxECAKwNAgCteQIArnUCAK8V8ACwafAAsRECALIdAgCzGQIAtAUCALUhAAC2LQAAtyUAALgZAAC54QEAuu0BALvlAQC8+QEA2BQAgN0UAIC/9YYCp2kNAOIUAIDnFACAzwAAgNkAAICzAwCA4QcAgH0JAID7IgCAzNSFAszghQL/IgCAgSkAgDUkAIBuJACAjSQAgLyZBQC9mQUAvqkFAL+ZvAC4mQUAuZkFALqJBQC7iQUAtKEFALXVsQC23bEAt6kFALCxsgCxzQUAssUFALO9BQCfJACAxCQAgMMoAIDfKACA8SgAgIgmAICFKQCAaSkAgCkkAIAtJACA2WSgAoEJAIDZUKAChAkAgI0JAICKCQCAhwkAgOwhAIDvIgCA9CEAgJhlBQCZEbIA/CEAgNkwoAKUOZEClU0FAJZFBQCXXQUAkGkFAJFpBQCSWQUAk1kFAID9vACB1ZwCgmW8AIPFvACEkbwAhZ28AIalvACHjbwAiK2TAonlvACKKZACi7W8AIwRkAKNlbwAji2wAI/FnAKQ6bwAkcHIAJJBkAKT8Z0ClNW8AJXlvACW4bwAl02QAphlkAKZfZACmrm8AJupCgCcbQ8Anb0KAPMiAICfXQ8AoK0PAKElCgCibQoAo2UKAKQNCgClpQ8ApgXUAKepDwComQ8AqZkPAKopDwCrKQ8ArDkPAK05DwCuKQ8ArykPALBZDwCxndEAspXRALOF1gC0sdEAtbHRALbZ1AC32dQAuOnUALnp1AC6+dQAu/nUALzp1AC96dQAvrnUAL+51ACASdUAgUnVAIJZ1QCDWdUAhEnVAIV90ACGddAAh23QAIhV0ACJXdAAinXVAIut1QCMtdUAjb3VAI611QCPQdAAkMHQAJHB0ACSwdAAk8HQAJTB0ACVwdAAlsHQAJfB0ACYwdAAmc3QAJrF0ACb3dAAnOHVAJ3pDgCe2Q4An9kOAKDV2wChwdkAotnZAKPB2QCkxdkApc3ZAKbF2QCnGdkAqGHZAKlh2QCqydkAq8nZAKzZ2QCt2dkArs3ZAK/B2QCwCdkAsRXZALId2QCzrdoAtB3ZALWx2gC2wdwAt93dALjl3QC59d0Auv3dALut3QC8td0AvaXdAL6t3QDwIQCAgvHaAIPx2gD3IgCA5OgAgIYR2ACHEdgAhOHaAIXh2gCKKdgAiynYAK9AEwClKNoAjinYAI8p2ACMKdgAjSnYAJJh2ACTYdgA6egAgO7oAICWZdgAl23YAJR12ACVbdgAml3YAJst2ADz6ACA8FwCALEw3wCR8AIAnCnYALLQAwCiOQ0Ao1GeAqAlDQChOQ0AplUNAIS8AgCkJQ0ApV0NAKptDQCrAQQAqGENAKlRAwCuuQAAp3UAAKxhDQCtxQIA+OgAgIfMAwDwVAIAzFC6AJHYBACb9NsAkRgCAJk02wCddAQAvh0AAJ9gBQCejAUAjOwCAI2sBAD96ACAvfWKAqghvwCpLb8Aqi2/AKs9vwCsKb8ArVW/AK5RvwCvTb8AoBkIAKGlvQCiIb8AozGzAKQ9vwClJb8Apg2zAKclvwC46bMAuc3LALppswC7uQkAvH0IAL2tCQC+QQwAv50JALA5vwCxhb0Asgm/ALPtywC0Gb8AtQW/ALbtswC3Bb8AiDG9AIkxvQCKrQgAiyW9AIwJCQCNvQgAjiW+AI+JDAAC6QCAgQ0JAIKlDACDUQkAhIEIAIWBCACGmQgAh60MAJhhvQCZYb0Amm0JAJsVnQKcxQ8AnQ28AJ7BDwCfcQkAkBW+AJERnwKSNZ8Ckw2fApQJvgCVCb4AlnG9AJdxvQCCuAQAl6UHALnEAwDwWAIAkUwCAJLIAgCErAQAsD0AAAzpAIAH6QCAvQUAABHpAIDwTAIAuhEAAJEkAgCN5AQAkqwCAJasAgC4uAMAudADAJb4AgCvDQAAFukAgPB4AgCRXAIAlrACAK8FAAAb6QCAIOkAgCnpAIAy6QCAP+kAgIX4AwBM6QCAh4ADAIbAAgBZ6QCAZukAgHPpAICW6QCAuzkAAHzpAICf6QCAiekAgL8dAAC+HQAAvR0AALwhAACVwB0AlMQfAJfIGgCWABgAkSAAAJDUAQCT2B4AkgAcAJ3gEgCcABAAn+gRAJ7sEwCZ8BkAmPQbAJv4FwCaABQAnnEBAJ9xAQCABQAArOkAgM0KAICwDACAXg0AgGQNAIBqDQCAdg0AgHkNAIB8DQCAfw0AgIINAICRDQCAlw0AgJoNAICdDQCAICIAgMcNAIDWDQCA/A0AgP8NAIAODgCAEQ4AgB0OAIAYIgCAMg4AgDUOAIDXFgCAEBcAgNoWAIC4ACwAuYwvALqILgC6AwCAhpwXAMx4vACEmC0AhVwXALcDAIDKAwCAiAAoAIksFADtBACAjAUAgN8FAIAaBgCAQAYAgFcGAIB0BgCAiwYAgDgBAIA8AQCAQAEAgEQBAIBIAQCATAEAgKR9AQBQAQCAonUBAKNlAQCggQEAoYEBALxxugC9kbYAvnG6AL+ltgC48bgAuXW6ALqZzgC7dboAtGG6ALVtugC2eboAt3W6ALAZugCxEboAsgm6ALMFugCsUboArXG2AK5RugCvbboAqNG4AKldugCqRbYAq1G6AKRxlgKlYZYCpnGWAqe9ugCgzZsCofG6AKLJugCjxboAnHmaAp0tugCeDc4An4WWApgJugCZtZYCmjm6AJuJtgCUMboA+CEAgJZpugCXrZYCkHm6AJE1ugCSMboAkwG6AIxJzgCN5bYAjhmaAo+hugCIoboAiUG2AIqhugCLdbYAhAG4AIWFugCGac4Ah4W6AICxugCBvboAgqm6AIOlugCAgbkAgQ27AIIVtwCDAbsAhAG7AIUhtwCGAbsAhz27AIgJuwCJAbsAihm7AIsVuwCMcbsAjX27AI5puwCPZbsAkKG5AJEluwCSyc8AkyW7AJQhuwCVwbcAliG7AJf1twCY6c8AmUW3AJq5mwKbAbsAnLm7AJ31uwCe8bsAn8G7AKARuwChCZQCokm7AKONlwKkCbsApbWXAqY5uwCnibcAqFmbAqkNuwCqLc8Aq6WXAqwNmgKtMbsArgm7AK8FuwCw0ZcCscGXArLRlwKzHbsAtFG5ALXduwC2xbcAt9G7ALjxuwC50bcAuvG7ALvNuwC82bsAvdG7AL7JuwC/xbsAgJmkAIEliAKCqaQAgxmoAFsNAICFvaQAhp3QAIcViAKInYUCiaGkAIqZpACLlaQAjCGIAo0xiAKOIYgCj+2kAJDBpgCRTaQAklWoAJNBpACUQaQAlWGoAJZBpACXfaQAmEmkAJlBpACaWaQAm1WkAJwxpACdPaQAnimkAJ8lpACgYaYAoeWkAKIJ0ACj5aQApOGkAKUBqACm4aQApzWoAKgp0ACphagAqnmEAqvBpACseaQArTWkAK4xpACvAaQAsFGkALFJiwKyCaQAs82IArRJpAC19YgCtnmkALfJqAC4GYQCuU2kALpt0AC75YgCvE2FAr1xpAC+SaQAv0WkAIARiQKBAYkCghGJAoPdpQCEkacAhR2lAFQBAICHEaUAiDGlAIkRqQCKMaUAWAEAgFwBAICNEaUAjgmlAI8FpQCQAaUAkQ2lAJIZpQCTFaUAlLGnAGABAICW2dEAlzWlAJgRpQCZ8akAmhGlAJvFqQCc+dEAZAEAgJ6phQKfEaUAoEmlAKEFpQCiAaUAozGlAKQBpQClGYoCplmlAKediQKoOaUAqYWJAqoJpQCruakArEmFAq0dpQCuPdEAr7WJArB9hAKxQaUAsnmlALN1pQC0wYkCtdGJArbBiQK3DaUAuGGnALntpQBoAQCAu+GlALzhpQC9wakAvuGlAGwBAIC3baYAttWGArUpqgC0hdIAs7mqALJtpgCxjaoAsG2mAL8higK+5aYAvaWJAnABAIC7jaYAdAEAgLm5pgC49aYAeAEAgKZ1pgClbaYAfAEAgIABAICiTaYAhAEAgIgBAICvCaYAruXSAIwBAICsjaQAqymmAKolpgCpMaYAkAEAgJc5pgCWNaYAlQ2mAJQxhwKTmYoCkhHSAJExpgCQZYYCn62mAJ65qgCUAQCAnC2kAJthpgCarYoCmb2KApitigKHfaYAhk2mAIVJpgCEBaYAg72mAIIFhgKB+aoAgFXSAI/1qgCORaYAjcmKAox1pgCL8YoCijWmAIl1iQKIbaYAgCmnAIEhpwCCOacAgzWnAIRRpwCYAQCAhkmnAJwBAIDMSIkCzYiJAoqp0wCLRacAjEGnAI2hqwCOQacAj5WrAJDJ0wBFIwCAkpmHApMhpwCUmacAldWnAJbRpwCX4acAmPGnAJnpiAKaqacAm22LApzppwCdVYsCntmnAJ9pqwCgeYcCoS2nAKIN0wCjhYsCpC2GAqURpwCmKacApyWnAKixiwKpoYsCqrGLAqt9pwCsMaUArb2nAK6lqwCvsacAsNGnALHxqwCy0acAs+2nALT5pwC18acAtumnALflpwC4oacAua2nALq5pwC7tacAvBGlAL2VpwC+edMAv5WnAICRoACBiY8CgsmgAIMNjAKEiaAAhTWMAoa5oACHCawAiNmAAomNoACKrdQAiyWMAoyNgQKNsaAAjomgAI+FoACQUYwCkUGMApJRjAKTnaAAlNGiAJVdoACWRawAl1GgAJhxoACZUawAmnGgAJtNoACcWaAAnVGgAJ5JoACfRaAAoMGgAKHNoACi2aAAo9WgAKRxogCl9aAAphnUAKf1oACo0aAAqTGsAKrRoACrBawArDnUAK2VrACuaYACr9GgALAJoACxRaAAskGgALNxoAC0QaAAtVmPArYZoAC33YwCuHmgALnFjAK6SaAAu/msALwJgAK9XaAAvn3UAL/1jAKAvYACgYGhAIK5oQCDtaEAhAGNAoURjQKGAY0Ch82hAIihowCJLaEAijWtAIshoQCMIaEAjQGtAI4hoQCPHaEAkGmhAJFhoQCSeaEAk3WhAJQRoQCVHaEAlgmhAJcFoQCYgaMAmQWhAJrp1QCbBaEAnAGhAJ3hrQCeAaEAn9WtAKAJ1QChpa0AolmBAqPhoQCkWaEApRWhAKYRoQCnIaEAqDGhAKkpjgKqaaEAq62NAqwpoQCtlY0CrhmhAK+prQCwOYECsW2hALJN1QCzxY0CtG2AArVRoQC2aaEAt2WhALjxjQK54Y0CuvGNArs9oQC8caMAvf2hAL7lrQC/8aEAs2miALKF1gCxaaIAsO2gALe5rgC2baIAtY2uALRtogC7TaIAuvWCArkJrgC4pdYAv42iAL69ogC9uaIAvPWiAKNNogCiWa4AoUGiAKDNoACncaIApk2iAKVtrgCkTaIAq1miAKpVogCpTaIAqEWiAK8pogCuJaIArTGiAKw9ogCTla4AkiWiAJGpjgKQFaIAl5mOApYR1gCVMaIAlGWCApsZogCaFaIAmS2iAJgRgwKfYaIAnq2OAp29jgKcrY4Cg2muAIK9ogCBXa4AgL2iAIe9ogCGBYIChfmuAIRV1gCLXaIAim2iAIlpogCIJaIAj/GOAo41ogCNdY0CjG2iAIARowCBMa8AghGjAIMtowCEOaMAhTGjAIYpowCHJaMAiGGjAIltowCKeaMAi3WjAIzRoQCNVaMAjrnXAI9VowCQMaMAkdGvAJIxowCT5a8AlNnXAJV1rwCWiYMClzGjAJipowCZ5aMAmuGjAJvRowCc4aMAnfmMAp65owCffY8CoBmjAKGljwKiKaMAo5mvAKRpgwKlPaMAph3XAKeVjwKoHYICqSGjAKoZowCrFaMArKGPAq2xjwKuoY8Cr22jALBBoQCxzaMAstWvALPBowC0waMAteGvALbBowC3/aMAuMmjALnBowC62aMAu9WjALyxowC9vaMAvqmjAL+lowBnDQCA0QYAgG0NAIDIBwCAcw0AgA8HAICFDQCAlAcAgIsNAICaBwCAuA0AgH0HAIDKDQCAxQcAgAIOAIBPBwCAFA4AgFIHAIAgDgCAkB0AAOEGAIAPJACA4iUAgCguAICtLACAyS0AgKpVAACrKQAAMjcAgAErAIDGMACAsjIAgAEsAIBTLwCAmSsAgJ8wAIDtKwCAGjUAgI43AICtLQCA5SwAgGYyAIADMACALzAAgA44AIAjMACA+y8AgHI0AICAIa4AgaWsAIJJ2ACDpawAhKGsAIVBoACGoawAh3WgAIhp2ACJxaAAiv0AAIsxxgCM7QAAjdEAAI7VAACPyQAAgCmhAIFNFACCIQEAg+G4AoQ5qgCFOaoAhhG9AodRFACIEQEAidW4AorNrQCLLbsCjGEUAI3ZjQKObRQAj2UUAJB5AQCRubgCkkm9ApNFuwKUDRQAlTUUAJYZAQCXqbgCmF2qAJkBFACaIQEAmwUUAJx5vQKdhbgCnnm7Ap+JuAKggb0CoXm4AqKZCQCjlRQApFmuAKWJFACmmQEAp70UAKipAQCpvbsCqrkBAKuJFACsmRQArZkUAK6JFACviRQAsNkBALEJrgCy6QEAs9W7ArTNuwK17RQAtpW8ArfhFAC4oRQAuaEUALrBoQC7pRQAvNkBAL0ZuAK+0aoAv9GqAL9FFwC+RRcAvTUXALxBvwK7KRcAugm4ArkBuAK4PQIAt+2tALY9AgC1HRcAtB0XALMdFwCyHRcAsR0XALAtAgCvWbgCrk0CAK1pFwCsTQIAq00XAKqdrQCpQRcAqE0KAK40AIDRLACApX0XAKR9FwCjoa4Aom2CAqF9ggKgbYICnzmuAJ41rgCdDa4AnDGPApuZggKaEdoAmTGuAJhljgKXtaIAlgWuAJWJggKUNa4Ak7GCApJ1rgCRNYECkC2uAI99rgCOTa4AjUmuAIwFrgCLva4AigWOAon5ogCIVdoAh0miAIadrgCFfaIAhJ2uAIOZrgCCddoAgZmuAIAdrADMqIQCzUyGAswguQLNTLkCzECOAkYyAIDMmIUCzTyEAswQgwLNUIMCzKCDAs2MgwLMMIACzSSAAswYgALNhIACmjMAgAUsAIAxLQCAiSMAgE0jAIBXIwCAayMAgJMjAIB1IwCAnSMAgGEjAIB/IwCAzPC5As2EuQLMULgCzay7AoDNAACB1QAAgt0AAIPVAACEzQAAhfUAAIb9AACH9QAAiM0AAFcvAIDBLACA1SoAgM0qAIDdKgCAuekAgCErAICQZQAAkW0AAKiIKgA1KwCAPSsAgEUrAIBJKwCATSsAgKIAMACjzDMAoOg9AKHsPACm8DYAp/QoAKQANACl/DUAgFERAIHpiAKCXREAg1URAIQpBACF6b0Chhm4AocVvgKIfREAiUURAIppBACL2b0CjA2vAI1REQCOcQQAj1URAJBJuAKRtb0Ckkm+ApO5vQKUUbgClam9ApZJDACXRREAmKmrAJl5EQCaaQQAm00RAJx5BACdbb4CnmkEAJ9ZEQCgqREAoakRAKK5EQCjuREApIkEAKVZqwCmuQQAp4W+Aqi9vgKpnREAquW5AquREQCs8REArfERAK6RpACv9REAsOkEALEpvQKy4a8As+GvALTZuAK1mREAtukEALctvQK4BagAueW+Arq5EQC7AYgCvKURAL2tEQC+wQQAvwG9AoABuQKBDb8CglUQAINtEACEUQUAheG8AoYlrgCHeRAAiGkFAIlNEACKIbkCi928AowxvwKNwbwCjjm5Ao/BvAKQUQ0AkV0QAJKBqgCTURAAlFEFAJV1EACWUQUAl0W/AphxBQCZQRAAmkEQAJtBEACcQRAAnUEQAJ5hBQCfsaoAoKEFAKGdvwKilb8Co7UQAKTduAKlqRAAptkQAKfZEACoiaUAqe0QAKqBBQCrQbwCrJmuAK2ZrgCusbkCr/EQALDxBQCxNbwCsi2pALPNvwK0gRAAtTmJAraNEAC3hRAAuNkFALkZvAK66bkCu+W/ArytEAC9lRAAvrkFAL8JvAK5La0AuC2tALtFEwC6BboCveG/ArwlBgC/GbwCvvmqALEdEwCwabsCs20TALJtEwC1eRMAtB2mALfVvwK2FQYAqXUTAKh1EwCrhakAqlUGAK1JvAKsdQYAr2ETAK5BvAKhQRMAoGUGAKNxvAKiZQYApVUTAKRlBgCnVRMAplUTAJl1vwKYhbwCm3W/ApqNugKdiRMAnIUOAJ+FEwCeVakAkVW/ApDlBgCTzRMAkpGtAJXZEwCU/QYAl0m/Apa1ugKJmRMAiJETAIs1vwKK9QYAjdm8AozVugKPuRMAjoETAIGtEwCA7boCgxm/AoLdBgCF8bwChBGqAIcVigKGrRMAgD2sAIFhEgCCQQcAg2USAIQZuwKF5b4Chhm9AofpvgKIIbsCidm+AopFEgCLXRIAjSkAgM3pAICOzaoAj8mLApCdiwKRpYsCkrGqAJOxqgCU2akAldmpAJb5qQCX+akAmJWqAJmRiwKatYsCm42LApyJqgCdiaoAnvGpAJ/xqQCgIakAoSGpAKJ9qgCjeYsCpE2LAqV1iwKmYaoAp2GqAKgpqQCpKakAqgmpAKsJqQCsRaoArUGLAq5liwKvXYsCsDmqALE5qgCyQakAs0GpALRxqQC1cakAti2qALcpiwK4PYsCuQWLAroRqgC7EaoAvHmpAL15qQC+WakAv1mpAIKJIwBtKwCAcSsAgI0rAIC+6QCAh5kjAJEpAIB5KwCAyOkAgIu5JACpKwCAifkkAI6VIwCPiSMAsSsAgI2JJACSvSMAESsAgLkrAICR4SMAo+sAgJfFIwCU8SMA4SsAgJkpAICbkSMA+SsAgJndIwD9KwCAnwktAAksAICdjdUAogkjAJ0pAIBBLACAofUjAEUsAICnGSMApCUkAG0sAICq7SQAeSwAgKgdIwCpeSQArhUjAK8JIwCsCSQArQkkALI9IwCJLACAsDEjALFhIwC2VSMAt0UjALRxIwC1XSMAulkjALsRIwCRLACAuV0jAL6JLQCVLACAvI0tANzpAICAuSUAgX0iAIKBIgCDmSIAhK0lAIXZJQCGuSIAh5EiAIiVIgCJ8SUAljIAgIuxJQCMgSUAjYElAI6dIgCPgSIAkLkiAJHpIgCStSIAk9EiAJT5IgCV1SIAlt0iAJfNIgCY+SIAmdUiAJrRIgCbmSIAqSwAgLEsAIDh6QCAvSwAgGUAAACh/SIAogEiAKMZIgDFLACApVklAKY5IgCnESIAqBUiAKlxJQDNLACAqzElAKwBJQCtASUArh0iAK8BIgCwOSIAsWkiALI1IgCzUSIAtHkiALVVIgC2XSIAt00iALh5IgC5VSIAulEiALsZIgD1LACA4SwAgO0sAIDxLACAgI0vAIGlLwCCrS8Ag70vAISlLwCFrS8AhqUvAIfdLwCI5S8Aie0vAIrlLwD5LACAAS0AgAUtAIANLQCAFS0AgJCRLwCRkS8AkpEvAJORLwCUsS8AlbEvAJa1LwCXRTMAmE0zAJlVMwCaPTMAmxkzAJyZMwCdiTMAnlUwAJ9JMACgwTAAockwAKLZMACj1TAApM0wAKX9MACm5TAApzUwAKi1MQCpuTEAqu0xAKuxmgCs0ZYArbE6AK61OgAZLQCAsEGUALHNlgCy1ZoAs8GWALTBlgC14ZoAtsGWALf9lgC4yZYAucGWALrZlgC71ZYAvLGWAL29lgC+qZYAv6WWAMUAAAChfSAAooEgACktAICkrScALS0AgDktAICnkSAAXS0AgKnxJwCqZScAq7EnAKyBJwCtgScArp0gAK+BIACwuSAAsekgALK1IABhLQCAtPkgALXVIAC23SAAt80gAEUtAIC51SAATS0AgLuZIACpLQCAcS0AgHUtAIB5LQCAgDknAIH9IACCASAAgxkgAG0tAICFWScAhjkgAIcRIACIFSAAiXEnAIrlJwCLMScAjAEnAI0BJwCOHSAAjwEgAJA5IACRaSAAkjUgAJNRIACUeSAAlVUgAJZdIACXTSAAmHkgAJlVIACaUSAAmxkgAJyFLgCdBdYAnoEuAJ+BLgCArT8AgbU/AIK9PwCDtT8AhK0/AIW5yACG1T8Ah80/AIj1PwCJ/T8AipnIAIvxPwCMATsAjQE7AI6NyACPOQQAkEkEAJFJBACSWQQAk1UEAJRNBACV3TwAlnkEAJd1BACYWQQAmSEEAJohBACbNdQAnCEEAJ3Z5gCeJQQAnx0EAKDpBACh9QQAos0/AKP1BACkFQQApfnUAKYhyACnIcgAqNHUAKktBACqOQQAq03CAKwtBACtdcgArh0EAK95BACwKQQAsTEEALI9BACzOQQAtC0EALX9BQC2qQUAt6kFALiZBQC5mQUAunkFALtFBQC8AQUAvQEFAL4BBQC/AQUAgC0HAIE1BwCCPQcAgzUHAIQtBwCFqQcAhqUHAIdl1QCILQYAiTEGAIoxBgCLDQYAjPnJAI15BgCOWQYAj1UGAJBpyQCRNQYAkj0GAJM1BgCULQYAlcUGAJZdAwCXVQMAmG0DAJl1AwCafQMAm3UDAJxtAwCdET0AnlkDAJ9ZAwCgqQMAoakDAKK5AwCjuQMApKkDAKWpAwCm2QMAp9kDAKjpAwCp6QMAqvkDAKv9AwCs5QMAre0DAK7lAwCvbcMAsKEDALGhAwCyoQMAs6EDALShAwC1zeYAtq0DALelAwC4yeYAuZkDALppAwC7aQMAvHkDAL15AwC+aQMAv2kDAIAAAACBLQCAfS0AgJUtAIDm6QCAsS0AgLUtAIC9LQCA0S0AgPQtAIDr6QCA8OkAgAAuAIAELgCACC4AgPwtAIAQLgCAoSkAgKUpAIAYLgCAIC4AgPXpAIA8LgCAQC4AgEwuAID66QCAVC4AgFguAIA3LwCAqSkAgGwuAICILgCAhC4AgATqAICQLgCACeoAgJwuAICYLgCAoC4AgLAuAIC0LgCArSkAgMQuAIDMLgCA0C4AgNQuAICxKQCADuoAgLUpAID3LgCA+y4AgP8uAIDV6wCAGOoAgNo1AIAvLwCAuSkAgDvqAIAN6wCAPy8AgEcvAIC9KQCAWy8AgGsvAICqIfQAq7U/AKilPwCpzecArkXwAK+hPwCsSfAArTH0AKJl4gCjvT8AoLk/AKG5PwCmlT8Ap50/AKSlPwClnT8Augk8AG8vAIC4CTwAuQk8AHcvAICHLwCAxSkAgMEpAICy3T8AswU9ALBN7wCx1T8Atn3wALe55AC0HT0AtWk8AB3qAICPLwCAoy8AgKcvAIC3LwCAyy8AgMMvAIDHLwCAgrX7AM8vAICA/T8AgfU/AOMvAIDnLwCA/y8AgAcwAICavT8Am/3NAJi9PwCZtT8Anlk/AJ9ZPwCcWT8AnVk/AJKBPwCTaekAkHnkAJGxPwCWgT8Al4H0AJQh5wCVmT8AFzAAgCswAIAs6gCAJzAAgBswAIAzMACAOzAAgE8wAIAx6gCAVzAAgEoAAABLMACAQzAAgMkpAIBfMACAZzAAgG8wAIBjMACAzSkAgIcwAIA26gCAszAAgPUwAIDRMACA2SkAgNUpAIDRKQCAnSsAgKErAID5MACA4TAAgK41AIA9KgCADTEAgCExAIAZMQCAT+oAgN0pAIA1MQCAKTEAgFIxAIBZ6gCAXjEAgD0xAIBmMQCAajEAgG4xAIByMQCAfjEAgF7qAICGMQCA5SkAgJIxAIBj6gCAljEAgOkpAICiMQCArjEAgL4xAIBo6gCA/+kAgG3qAIDeMQCAcuoAgLgJAQC5CQEAuhkBALsZAQC8CQEAvQkBAL45AQC/OQEAsM3FALE1zACymQ4As5kOALSJDgC1iQ4AtjkBALc5AQCo6dkAqckOAKrZDgCrqcUArMUOAK3NDgCuxQ4Ar/kOAKA1DgChPQ4AojUOAKOxxQCk8Q4ApfEOAKbxDgCn8Q4AmGkPAJlpDwCaeQ8Am3kPAJxpDwCdaQ8Ant0OAJ/NDgCQ+eoAkXEPAJJ9DwCTdQ8AlG0PAJVpDwCWWQ8Al1kPAIh5DwCJeQ8AigkPAIsJDwCMGQ8AjRkPAI4NzACPDQ8AgHkPAIF5DwCCSQ8Ag0kPAIRZDwCFWQ8AhkkPAIdJDwCKUQIAi1ECAIj5xgCJQQIAjnECAI/txgCMQQIAjUECAIIVAgCDHQIAgAUCAIEdAgCGdQIAh30CAIQFAgCFfQIAmsUCAJvNAgCYkc8AmYXaAJ7FAgCfzQIAnNUCAJ3NAgCSDQIAkxUCAJANAgCRBQIAlg0CAJf1AgCUDQIAlQUCAKo9AgCrRQIAqD0CAKk1AgCuXQIAr0UCAKxdAgCtVQIAol3GAKMBAgCgNQIAoQ0CAKYBAgCnxdgApBECAKURAgC6OQIAuzkCALg5AgC5OQIAvtkBAL/ZAQC82QEAvdkBALI9AgCzBQIAsD0CALE1AgC2GQIAtxkCALQdAgC16cIA6jEAgPIxAIDiMQCA/jEAgA4yAIAWMgCAIjIAgCYyAIB36gCACjIAgD4yAIBCMgCA7SkAgFIyAIB86gCANjIAgHIyAICB6gCAhuoAgHYyAICKMgCAgjIAgPEpAICOMgCAnjIAgJoyAICmMgCAw+kAgLYyAICL6gCAwjIAgJXqAIDWMgCA9jIAgJrqAIAKMwCADjMAgJ/qAICk6gCAKjMAgDozAID1KQCAPjMAgPkpAIBWMwCAWjMAgGYzAIByMwCA/SkAgIozAICp6gCApjMAgK7qAIAT6gCAwjMAgLPqAIC4AAAAuOoAgL3qAIABKgCABSoAgMfqAIDC6gCAzOoAgIAB3gCB8QcAgvEHAIPxBwCEFQIAhR0CAIYVAgCHEQIAiCXeAIld3gCKOQIAizkCAIwpAgCNKQIAjhkCAI99ygCQTd4AkWECAJJhAgCT7cEAlH0CAJVlAgCWIcAAl2kCAJhZAgCZMcIAmlUCAJstAgCcNQIAnT0CAJ4xAgCfMQIAoNECAKHRAgCi0QIAo9ECAKTxAgCl8QIApvECAKfxAgCo0QIAqdECAKrRAgCr0QIArDECAK0xAgCuMQIArzECALBRAgCxUQIAslECALNRAgC0cQIAtXECALZxAgC3cQIAuFECALlRAgC6+dwAu1UCALxNAgC9NQIAvj0CAL81AgC+7QYAv/UGALztBgC95QYAuskGALvJBgC4xcsAuckGALbtBgC39QYAtO0GALXlBgCyjQYAs/UGALDR3QCxhQYArvEGAK/xBgCs5QYAreEGAKr1BgCr/QYAqMUGAKn9BgCm9QYAp/0GAKTlBgCl/QYAovUGAKP9BgCg+QYAoZ3dAJ75BgCf+QYAnPkGAJ35BgCa+QYAm/kGAJj5BgCZ+QYAlvkGAJf5BgCUcd0AlfkGAJL9BgCT5QYAkP0GAJH1BgCO/QYAj4UGAIz9BgCN9QYAiuEGAIsB3QCI8QYAifEGAIbBBgCHwQYAhPEGAIXxBgCCkccAg+EGAIDpBgCBxcAAgAAAANHqAIACNACABjQAgBI0AIARKgCAFSoAgNvqAIAmNACAGSoAgODqAIDl6gCA6uoAgJY0AIAdKgCAojQAgKY0AIDv6gCA9OoAgL40AIAhKgCA+eoAgNI0AIDWNACAJSoAgP7qAIDyNACAKSoAgAI1AID6NACACjUAgAjrAIAiNQCALSoAgC41AIA2NQCARjUAgDEqAIAS6wCAF+sAgDUqAIAc6wCAXjUAgCHrAIBqNQCAdjUAgCbrAIAr6wCAkjUAgDDrAICaNQCAQOoAgDkqAICyNQCAtjUAgEEqAIC6NQCAFC4AgDXrAIA66wCAReoAgErqAIDeNQCA9jcAgIDNAQCB1QEAgt0BAIPVAQCEzQEAhfUBAIb9AQCH9QEAiM0BAInVAQCK3QEAi/UJAIzJAQCNyQEAjgEcAI89HwCQRR8AkU0fAJJFHwCTXR8AlEUfAJVNHwCWRR8Al30fAJhBxwCZQR8AmkEfAJtBHwCcQR8AnUEfAJ5BHwCfYd8AoL0fAKHFHwCizR8Ao8UfAKTdHwClxR8Aps0fAKfFHwCo/R8AqcUfAKrNHwCrxR8ArN0fAK3FHwCuzR8Ar8UfALC9HwCxRR8Ask0fALNFHwC0/ckAtVkfALZJHwC3SR8AuHkfALl5HwC6SR8Au8XdALxVHwC9XR8AvlUfAL9NHwAKNgCABjYAgA42AIAZLACAEjYAgBY2AIAaNgCAIjYAgD/rAIAmNgCAOjYAgD42AIAqNgCAQjYAgFY2AIA2NgCASjYAgE42AIBSNgCAROsAgE7rAIBJ6wCASSoAgHI2AIB2NgCAfjYAgGLrAICCNgCAU+sAgE0qAIBRKgCAWOsAgF3rAIBVKgCAojYAgKo2AICuNgCAujYAgLY2AIDCNgCAvjYAgMY2AIDKNgCA0jYAgFkqAIDaNgCA3jYAgF0qAIDuNgCAZ+sAgP42AIACNwCAYSoAgA43AICVKQCAbOsAgHHrAIBlKgCAaSoAgDo3AIB26wCAkjcAgJY3AICuNwCAgLUBAIG9AQCCtQEAg80BAITt9ACF0QEAhtEBAIfRAQCI8QEAifEBAIrxAQCL8QEAjNEBAI3RAQCO0QEAj9EBAJB9wwCRBcMAkl35AJO9AQCUpQEAla0BAJalAQCXXQMAmGUDAJltAwCaZQMAm30DAJxlAwCdbQMAnmUDAJ85wwCgoQMAoaEDAKKhAwCjoQMApKEDAKWhAwCmoQMAp6EDAKjhAwCp4QMAquEDAKvhAwCs4QMAreEDAK7hAwCv4QMAsKEDALGhAwCyoQMAs6EDALShAwC1oQMAtqEDALehAwC4YQMAuWEDALphAwC7YQMAvGEDAL1hAwC+pcMAv6HDALo3AICA6wCA0ukAgMY3AIDCNwCAzjcAgNfpAIDaNwCAhesAgIrrAIAmOACAMjgAgDo4AICP6wCAPjgAgGY4AIByOACAdjgAgG44AICCOACAhjgAgJTrAICSOACAbSoAgJo4AICZ6wCAcSoAgNI4AICkLgCA6jgAgJ7rAICo6wCAdSoAgHkqAIASOQCAresAgH0qAICy6wCAMjkAgLfrAIBKOQCAgSoAgFo5AIBmOQCAbjkAgHY5AICFKgCAvOsAgKY5AICyOQCAiSoAgI0qAIC2OQCAwesAgJEqAIDG6wCAy+sAgNDrAICVKgCA9jkAgPo5AIACOgCACjoAgNrrAICQ1QEAkd0BAJLVAQCT7QEAlPUBAJXB+wCW8QEAl/n7AJjNAQCZ1QEAmt0BAJvVAQCcyfsAnckBAEUqAICPAAAAgNkBAIHZAQCC6QEAg+kBAIT5AQCF+QEAhukBAIfpAQCI2QEAidkBAIoJwQCLrQEAjLUBAI29AQCOtQEAj60BAKAAAAChAAAAogAAAKMAAACkAAAApQAAAKYAAACnAAAAqAAAAKkAAACqAAAAqwAAAKwAAACtAAAArgAAAK8AAACwAAAAsQAAALIAAACzAAAAtAAAALUAAAC2AAAAtwAAALgAAAC5AAAAugAAALsAAAC8AAAAvQAAAL4AAAC/AAAAACAAIMyBACDMgwAgzIQAIMyFACDMhgAgzIcAIMyIACDMiMyAACDMiMyBACDMiM2CACDMigAgzIsAIMyTACDMk8yAACDMk8yBACDMk82CACDMlAAgzJTMgAAgzJTMgQAgzJTNggAgzKcAIMyoACDMswAgzYIAIM2FACDZiwAg2YwAINmM2ZEAINmNACDZjdmRACDZjgAg2Y7ZkQAg2Y8AINmP2ZEAINmQACDZkNmRACDZkQAg2ZHZsAAg2ZIAIOOCmQAg44KaACEAISEAIT8AIgAjACQAJQAmACcAKAAoMSkAKDEwKQAoMTEpACgxMikAKDEzKQAoMTQpACgxNSkAKDE2KQAoMTcpACgxOCkAKDE5KQAoMikAKDIwKQAoMykAKDQpACg1KQAoNikAKDcpACg4KQAoOSkAKEEpAChCKQAoQykAKEQpAChFKQAoRikAKEcpAChIKQAoSSkAKEopAChLKQAoTCkAKE0pAChOKQAoTykAKFApAChRKQAoUikAKFMpAChUKQAoVSkAKFYpAChXKQAoWCkAKFkpAChaKQAoYSkAKGIpAChjKQAoZCkAKGUpAChmKQAoZykAKGgpAChpKQAoaikAKGspAChsKQAobSkAKG4pAChvKQAocCkAKHEpAChyKQAocykAKHQpACh1KQAodikAKHcpACh4KQAoeSkAKHopACjhhIApACjhhIIpACjhhIMpACjhhIUpACjhhIYpACjhhIcpACjhhIkpACjhhIspACjhhIwpACjhhI4pACjhhI8pACjhhJApACjhhJEpACjhhJIpACjkuIApACjkuIMpACjkuIkpACjkuZ0pACjkuowpACjkupQpACjku6MpACjkvIEpACjkvJEpACjlhaspACjlha0pACjlirQpACjljYEpACjljZQpACjlkI0pACjlkbwpACjlm5spACjlnJ8pACjlraYpACjml6UpACjmnIgpACjmnIkpACjmnKgpACjmoKopACjmsLQpACjngaspACjnibkpACjnm6MpACjnpL4pACjnpZ0pACjnpa0pACjoh6opACjoh7MpACjosqEpACjos4cpACjph5EpACjqsIApACjrgpgpACjri6QpACjrnbwpACjrp4gpACjrsJQpACjsgqwpACjslYQpACjsmKTsoIQpACjsmKTtm4QpACjsnpApACjso7wpACjssKgpACjsubQpACjtg4ApACjtjIwpACjtlZgpACkAKgArACwALQAuAC4uAC4uLgAvADAAMCwAMC4AMOKBhDMAMOeCuQAxADEsADEuADEwADEwLgAxMOaXpQAxMOaciAAxMOeCuQAxMQAxMS4AMTHml6UAMTHmnIgAMTHngrkAMTIAMTIuADEy5pelADEy5pyIADEy54K5ADEzADEzLgAxM+aXpQAxM+eCuQAxNAAxNC4AMTTml6UAMTTngrkAMTUAMTUuADE15pelADE154K5ADE2ADE2LgAxNuaXpQAxNueCuQAxNwAxNy4AMTfml6UAMTfngrkAMTgAMTguADE45pelADE454K5ADE5ADE5LgAxOeaXpQAxOeeCuQAx4oGEADHigYQxMAAx4oGEMgAx4oGEMwAx4oGENAAx4oGENQAx4oGENgAx4oGENwAx4oGEOAAx4oGEOQAx5pelADHmnIgAMeeCuQAyADIsADIuADIwADIwLgAyMOaXpQAyMOeCuQAyMQAyMeaXpQAyMeeCuQAyMgAyMuaXpQAyMueCuQAyMwAyM+aXpQAyM+eCuQAyNAAyNOaXpQAyNOeCuQAyNQAyNeaXpQAyNgAyNuaXpQAyNwAyN+aXpQAyOAAyOOaXpQAyOQAyOeaXpQAy4oGEMwAy4oGENQAy5pelADLmnIgAMueCuQAzADMsADMuADMwADMw5pelADMxADMx5pelADMyADMzADM0ADM1ADM2ADM3ADM4ADM5ADPigYQ0ADPigYQ1ADPigYQ4ADPml6UAM+aciAAz54K5ADQANCwANC4ANDAANDEANDIANDMANDQANDUANDYANDcANDgANDkANOKBhDUANOaXpQA05pyIADTngrkANQA1LAA1LgA1MAA14oGENgA14oGEOAA15pelADXmnIgANeeCuQA2ADYsADYuADbml6UANuaciAA254K5ADcANywANy4AN+KBhDgAN+aXpQA35pyIADfngrkAOAA4LAA4LgA45pelADjmnIgAOOeCuQA5ADksADkuADnml6UAOeaciAA554K5ADoAOjo9ADsAPAA9AD09AD09PQA+AD8APyEAPz8AQABBAEFVAEHiiJVtAEIAQnEAQwBDRABDby4AQ+KIlWtnAEQAREoARFoARHoARMW9AETFvgBFAEYARkFYAEcAR0IAR0h6AEdQYQBHeQBIAEhQAEhWAEhnAEh6AEkASUkASUlJAElKAElVAElWAElYAEoASwBLQgBLSwBLTQBMAExKAExURABMagBMwrcATQBNQgBNQwBNRABNSHoATVBhAE1WAE1XAE3OqQBOAE5KAE5qAE5vAE8AUABQSABQUE0AUFBWAFBSAFBURQBQYQBRAFIAUnMAUwBTRABTTQBTUwBTdgBUAFRFTABUSHoAVE0AVQBWAFZJAFZJSQBWSUlJAFbiiJVtAFcAV0MAV1oAV2IAWABYSQBYSUkAWQBaAFsAXABdAF4AXwBgAGEAYS5tLgBhL2MAYS9zAGHKvgBiAGJhcgBjAGMvbwBjL3UAY2FsAGNjAGNkAGNtAGNtMgBjbTMAZABkQgBkYQBkbABkbQBkbTIAZG0zAGR6AGTFvgBlAGVWAGVyZwBmAGZmAGZmaQBmZmwAZmkAZmwAZm0AZwBnYWwAaABoUGEAaGEAaQBpaQBpaWkAaWoAaW4AaXYAaXgAagBrAGtBAGtIegBrUGEAa1YAa1cAa2NhbABrZwBrbABrbQBrbTIAa20zAGt0AGvOqQBsAGxqAGxtAGxuAGxvZwBseABswrcAbQBtMgBtMwBtQQBtVgBtVwBtYgBtZwBtaWwAbWwAbW0AbW0yAG1tMwBtb2wAbXMAbeKIlXMAbeKIlXMyAG4AbkEAbkYAblYAblcAbmoAbm0AbnMAbwBvVgBwAHAubS4AcEEAcEYAcFYAcFcAcGMAcHMAcQByAHJhZAByYWTiiJVzAHJhZOKIlXMyAHMAc3IAc3QAdAB1AHYAdmkAdmlpAHZpaWkAdwB4AHhpAHhpaQB5AHoAewB8AH0AwqIAwqMAwqUAwqYAwqwAwrBDAMKwRgDCtwDDgADDgQDDggDDgwDDhADDhQDDhgDDhwDDiADDiQDDigDDiwDDjADDjQDDjgDDjwDDkQDDkgDDkwDDlADDlQDDlgDDmQDDmgDDmwDDnADDnQDDoADDoQDDogDDowDDpADDpQDDpwDDqADDqQDDqgDDqwDDrADDrQDDrgDDrwDDsADDsQDDsgDDswDDtADDtQDDtgDDuQDDugDDuwDDvADDvQDDvwDEgADEgQDEggDEgwDEhADEhQDEhgDEhwDEiADEiQDEigDEiwDEjADEjQDEjgDEjwDEkgDEkwDElADElQDElgDElwDEmADEmQDEmgDEmwDEnADEnQDEngDEnwDEoADEoQDEogDEowDEpADEpQDEpgDEpwDEqADEqQDEqgDEqwDErADErQDErgDErwDEsADEsQDEtADEtQDEtgDEtwDEuQDEugDEuwDEvADEvQDEvgDFgwDFhADFhQDFhgDFhwDFiADFiwDFjADFjQDFjgDFjwDFkADFkQDFkwDFlADFlQDFlgDFlwDFmADFmQDFmgDFmwDFnADFnQDFngDFnwDFoADFoQDFogDFowDFpADFpQDFqADFqQDFqgDFqwDFrADFrQDFrgDFrwDFsADFsQDFsgDFswDFtADFtQDFtgDFtwDFuADFuQDFugDFuwDFvADFvQDFvgDGjgDGkADGoADGoQDGqwDGrwDGsADHjQDHjgDHjwDHkADHkQDHkgDHkwDHlADHlQDHlgDHlwDHmADHmQDHmgDHmwDHnADHngDHnwDHoADHoQDHogDHowDHpgDHpwDHqADHqQDHqgDHqwDHrADHrQDHrgDHrwDHsADHtADHtQDHuADHuQDHugDHuwDHvADHvQDHvgDHvwDIgADIgQDIggDIgwDIhADIhQDIhgDIhwDIiADIiQDIigDIiwDIjADIjQDIjgDIjwDIkADIkQDIkgDIkwDIlADIlQDIlgDIlwDImADImQDImgDImwDIngDInwDIogDIpgDIpwDIqADIqQDIqgDIqwDIrADIrQDIrgDIrwDIsADIsQDIsgDIswDItwDJkADJkQDJkgDJlADJlQDJmQDJmwDJnADJnwDJoQDJowDJpQDJpgDJqADJqQDJqgDJqwDJrQDJrwDJsADJsQDJsgDJswDJtADJtQDJuADJuQDJuwDKgQDKggDKgwDKiQDKigDKiwDKjADKkADKkQDKkgDKlQDKnQDKnwDKuQDKvG4AzIAAzIEAzIjMgQDMkwDOhgDOiADOiQDOigDOjADOjgDOjwDOkADOkQDOkgDOkwDOlADOlQDOlgDOlwDOmADOmQDOmgDOmwDOnADOnQDOngDOnwDOoADOoQDOowDOpADOpQDOpgDOpwDOqADOqQDOqgDOqwDOrADOrQDOrgDOrwDOsADOsQDOsgDOswDOtADOtQDOtgDOtwDOuADOuQDOugDOuwDOvADOvEEAzrxGAM68VgDOvFcAzrxnAM68bADOvG0AzrxzAM69AM6+AM6/AM+AAM+BAM+CAM+DAM+EAM+FAM+GAM+HAM+IAM+JAM+KAM+LAM+MAM+NAM+OAM+cAM+dANCAANCBANCDANCHANCMANCNANCOANCZANC5ANC9ANGKANGMANGQANGRANGTANGXANGcANGdANGeANG2ANG3ANOBANOCANOQANORANOSANOTANOWANOXANOaANObANOcANOdANOeANOfANOiANOjANOkANOlANOmANOnANOqANOrANOsANOtANOuANOvANOwANOxANOyANOzANO0ANO1ANO4ANO5ANWl1oIA1bTVpQDVtNWrANW01a0A1bTVtgDVvtW2ANeQANeQ1rcA15DWuADXkNa8ANeQ15wA15EA15HWvADXkda/ANeSANeS1rwA15MA15PWvADXlADXlNa8ANeV1rkA15XWvADXlta8ANeY1rwA15nWtADXmda8ANea1rwA15sA15vWvADXm9a/ANecANec1rwA150A157WvADXoNa8ANeh1rwA16IA16PWvADXpNa8ANek1r8A16bWvADXp9a8ANeoANeo1rwA16nWvADXqda814EA16nWvNeCANep14EA16nXggDXqgDXqta8ANey1rcA2KEA2KIA2KMA2KQA2KUA2KYA2KbYpwDYptisANim2K0A2KbYrgDYptixANim2LIA2KbZhQDYptmGANim2YcA2KbZiADYptmJANim2YoA2KbbhgDYptuHANim24gA2KbbkADYptuVANinANin2YPYqNixANin2YTZhNmHANin2YsA2KfZtADYqADYqNisANio2K0A2KjYrdmKANio2K4A2KjYrtmKANio2LEA2KjYsgDYqNmFANio2YYA2KjZhwDYqNmJANio2YoA2KkA2KoA2KrYrADYqtis2YUA2KrYrNmJANiq2KzZigDYqtitANiq2K3YrADYqtit2YUA2KrYrgDYqtiu2YUA2KrYrtmJANiq2K7ZigDYqtixANiq2LIA2KrZhQDYqtmF2KwA2KrZhditANiq2YXYrgDYqtmF2YkA2KrZhdmKANiq2YYA2KrZhwDYqtmJANiq2YoA2KsA2KvYrADYq9ixANir2LIA2KvZhQDYq9mGANir2YcA2KvZiQDYq9mKANisANis2K0A2KzYrdmJANis2K3ZigDYrNmEINis2YTYp9mE2YcA2KzZhQDYrNmF2K0A2KzZhdmJANis2YXZigDYrNmJANis2YoA2K0A2K3YrADYrdis2YoA2K3ZhQDYrdmF2YkA2K3ZhdmKANit2YkA2K3ZigDYrgDYrtisANiu2K0A2K7ZhQDYrtmJANiu2YoA2K8A2LAA2LDZsADYsQDYsdiz2YjZhADYsdmwANix24zYp9mEANiyANizANiz2KwA2LPYrNitANiz2KzZiQDYs9itANiz2K3YrADYs9iuANiz2K7ZiQDYs9iu2YoA2LPYsQDYs9mFANiz2YXYrADYs9mF2K0A2LPZhdmFANiz2YcA2LPZiQDYs9mKANi0ANi02KwA2LTYrNmKANi02K0A2LTYrdmFANi02K3ZigDYtNiuANi02LEA2LTZhQDYtNmF2K4A2LTZhdmFANi02YcA2LTZiQDYtNmKANi1ANi12K0A2LXYrditANi12K3ZigDYtdiuANi12LEA2LXZhNi52YUA2LXZhNmJANi12YTZiSDYp9mE2YTZhyDYudmE2YrZhyDZiNiz2YTZhQDYtdmE25IA2LXZhQDYtdmF2YUA2LXZiQDYtdmKANi2ANi22KwA2LbYrQDYttit2YkA2LbYrdmKANi22K4A2LbYrtmFANi22LEA2LbZhQDYttmJANi22YoA2LcA2LfYrQDYt9mFANi32YXYrQDYt9mF2YUA2LfZhdmKANi32YkA2LfZigDYuADYuNmFANi5ANi52KwA2LnYrNmFANi52YTZitmHANi52YUA2LnZhdmFANi52YXZiQDYudmF2YoA2LnZiQDYudmKANi6ANi62KwA2LrZhQDYutmF2YUA2LrZhdmJANi62YXZigDYutmJANi62YoA2YDZiwDZgNmOANmA2Y7ZkQDZgNmPANmA2Y/ZkQDZgNmQANmA2ZDZkQDZgNmRANmA2ZIA2YEA2YHYrADZgditANmB2K4A2YHYrtmFANmB2YUA2YHZhdmKANmB2YkA2YHZigDZggDZgtitANmC2YTbkgDZgtmFANmC2YXYrQDZgtmF2YUA2YLZhdmKANmC2YkA2YLZigDZgwDZg9inANmD2KwA2YPYrQDZg9iuANmD2YQA2YPZhQDZg9mF2YUA2YPZhdmKANmD2YkA2YPZigDZhADZhNiiANmE2KMA2YTYpQDZhNinANmE2KwA2YTYrNisANmE2KzZhQDZhNis2YoA2YTYrQDZhNit2YUA2YTYrdmJANmE2K3ZigDZhNiuANmE2K7ZhQDZhNmFANmE2YXYrQDZhNmF2YoA2YTZhwDZhNmJANmE2YoA2YUA2YXYpwDZhdisANmF2KzYrQDZhdis2K4A2YXYrNmFANmF2KzZigDZhditANmF2K3YrADZhdit2YUA2YXYrdmF2K8A2YXYrdmKANmF2K4A2YXYrtisANmF2K7ZhQDZhdiu2YoA2YXZhQDZhdmF2YoA2YXZiQDZhdmKANmGANmG2KwA2YbYrNitANmG2KzZhQDZhtis2YkA2YbYrNmKANmG2K0A2YbYrdmFANmG2K3ZiQDZhtit2YoA2YbYrgDZhtixANmG2LIA2YbZhQDZhtmF2YkA2YbZhdmKANmG2YYA2YbZhwDZhtmJANmG2YoA2YcA2YfYrADZh9mFANmH2YXYrADZh9mF2YUA2YfZiQDZh9mKANmH2bAA2YgA2YjYs9mE2YUA2YjZtADZiQDZidmwANmKANmK2KwA2YrYrNmKANmK2K0A2YrYrdmKANmK2K4A2YrYsQDZitiyANmK2YUA2YrZhdmFANmK2YXZigDZitmGANmK2YcA2YrZiQDZitmKANmK2bQA2a4A2a8A2bEA2bkA2boA2bsA2b4A2b8A2oAA2oMA2oQA2oYA2ocA2ogA2owA2o0A2o4A2pEA2pgA2qEA2qQA2qYA2qkA2q0A2q8A2rEA2rMA2roA2rsA2r4A24AA24EA24IA24UA24YA24cA24fZtADbiADbiQDbiwDbjADbkADbkgDbkwDgpJXgpLwA4KSW4KS8AOCkl+CkvADgpJzgpLwA4KSh4KS8AOCkouCkvADgpKkA4KSr4KS8AOCkr+CkvADgpLEA4KS0AOCmoeCmvADgpqLgprwA4Kav4Ka8AOCniwDgp4wA4KiW4Ki8AOCol+CovADgqJzgqLwA4Kir4Ki8AOCosuCovADgqLjgqLwA4Kyh4Ky8AOCsouCsvADgrYgA4K2LAOCtjADgrpQA4K+KAOCviwDgr4wA4LGIAOCzgADgs4cA4LOIAOCzigDgs4sA4LWKAOC1iwDgtYwA4LeaAOC3nADgt50A4LeeAOC5jeC4sgDguqvgupkA4Lqr4LqhAOC7jeC6sgDgvIsA4L2A4L61AOC9guC+twDgvYzgvrcA4L2R4L63AOC9luC+twDgvZvgvrcA4L2x4L2yAOC9seC9tADgvbHgvoAA4L6Q4L61AOC+kuC+twDgvpzgvrcA4L6h4L63AOC+puC+twDgvqvgvrcA4L6y4L2x4L6AAOC+suC+gADgvrPgvbHgvoAA4L6z4L6AAOGApgDhg5wA4YSAAOGEgQDhhIIA4YSDAOGEhADhhIUA4YSGAOGEhwDhhIgA4YSJAOGEigDhhIsA4YSMAOGEjQDhhI4A4YSPAOGEkADhhJEA4YSSAOGElADhhJUA4YSaAOGEnADhhJ0A4YSeAOGEoADhhKEA4YSiAOGEowDhhKcA4YSpAOGEqwDhhKwA4YStAOGErgDhhK8A4YSyAOGEtgDhhYAA4YWHAOGFjADhhZcA4YWYAOGFmQDhhaAA4YWhAOGFogDhhaMA4YWkAOGFpQDhhaYA4YWnAOGFqADhhakA4YWqAOGFqwDhhawA4YWtAOGFrgDhha8A4YWwAOGFsQDhhbIA4YWzAOGFtADhhbUA4YaEAOGGhQDhhogA4YaRAOGGkgDhhpQA4YaeAOGGoQDhhqoA4YasAOGGrQDhhrAA4YaxAOGGsgDhhrMA4Ya0AOGGtQDhh4cA4YeIAOGHjADhh44A4YeTAOGHlwDhh5kA4YedAOGHnwDhh7EA4YeyAOGshgDhrIgA4ayKAOGsjADhrI4A4aySAOGsuwDhrL0A4a2AAOGtgQDhrYMA4bSCAOG0lgDhtJcA4bScAOG0nQDhtKUA4bW7AOG2hQDhuIAA4biBAOG4ggDhuIMA4biEAOG4hQDhuIYA4biHAOG4iADhuIkA4biKAOG4iwDhuIwA4biNAOG4jgDhuI8A4biQAOG4kQDhuJIA4biTAOG4lADhuJUA4biWAOG4lwDhuJgA4biZAOG4mgDhuJsA4bicAOG4nQDhuJ4A4bifAOG4oADhuKEA4biiAOG4owDhuKQA4bilAOG4pgDhuKcA4bioAOG4qQDhuKoA4birAOG4rADhuK0A4biuAOG4rwDhuLAA4bixAOG4sgDhuLMA4bi0AOG4tQDhuLYA4bi3AOG4uADhuLkA4bi6AOG4uwDhuLwA4bi9AOG4vgDhuL8A4bmAAOG5gQDhuYIA4bmDAOG5hADhuYUA4bmGAOG5hwDhuYgA4bmJAOG5igDhuYsA4bmMAOG5jQDhuY4A4bmPAOG5kADhuZEA4bmSAOG5kwDhuZQA4bmVAOG5lgDhuZcA4bmYAOG5mQDhuZoA4bmbAOG5nADhuZ0A4bmeAOG5nwDhuaAA4bmhAOG5ogDhuaMA4bmkAOG5pQDhuaYA4bmnAOG5qADhuakA4bmqAOG5qwDhuawA4bmtAOG5rgDhua8A4bmwAOG5sQDhubIA4bmzAOG5tADhubUA4bm2AOG5twDhubgA4bm5AOG5ugDhubsA4bm8AOG5vQDhub4A4bm/AOG6gADhuoEA4bqCAOG6gwDhuoQA4bqFAOG6hgDhuocA4bqIAOG6iQDhuooA4bqLAOG6jADhuo0A4bqOAOG6jwDhupAA4bqRAOG6kgDhupMA4bqUAOG6lQDhupYA4bqXAOG6mADhupkA4bqgAOG6oQDhuqIA4bqjAOG6pADhuqUA4bqmAOG6pwDhuqgA4bqpAOG6qgDhuqsA4bqsAOG6rQDhuq4A4bqvAOG6sADhurEA4bqyAOG6swDhurQA4bq1AOG6tgDhurcA4bq4AOG6uQDhuroA4bq7AOG6vADhur0A4bq+AOG6vwDhu4AA4buBAOG7ggDhu4MA4buEAOG7hQDhu4YA4buHAOG7iADhu4kA4buKAOG7iwDhu4wA4buNAOG7jgDhu48A4buQAOG7kQDhu5IA4buTAOG7lADhu5UA4buWAOG7lwDhu5gA4buZAOG7mgDhu5sA4bucAOG7nQDhu54A4bufAOG7oADhu6EA4buiAOG7owDhu6QA4bulAOG7pgDhu6cA4buoAOG7qQDhu6oA4burAOG7rADhu60A4buuAOG7rwDhu7AA4buxAOG7sgDhu7MA4bu0AOG7tQDhu7YA4bu3AOG7uADhu7kA4byAAOG8gQDhvIIA4byDAOG8hADhvIUA4byGAOG8hwDhvIgA4byJAOG8igDhvIsA4byMAOG8jQDhvI4A4byPAOG8kADhvJEA4bySAOG8kwDhvJQA4byVAOG8mADhvJkA4byaAOG8mwDhvJwA4bydAOG8oADhvKEA4byiAOG8owDhvKQA4bylAOG8pgDhvKcA4byoAOG8qQDhvKoA4byrAOG8rADhvK0A4byuAOG8rwDhvLAA4byxAOG8sgDhvLMA4by0AOG8tQDhvLYA4by3AOG8uADhvLkA4by6AOG8uwDhvLwA4by9AOG8vgDhvL8A4b2AAOG9gQDhvYIA4b2DAOG9hADhvYUA4b2IAOG9iQDhvYoA4b2LAOG9jADhvY0A4b2QAOG9kQDhvZIA4b2TAOG9lADhvZUA4b2WAOG9lwDhvZkA4b2bAOG9nQDhvZ8A4b2gAOG9oQDhvaIA4b2jAOG9pADhvaUA4b2mAOG9pwDhvagA4b2pAOG9qgDhvasA4b2sAOG9rQDhva4A4b2vAOG9sADhvbIA4b20AOG9tgDhvbgA4b26AOG9vADhvoAA4b6BAOG+ggDhvoMA4b6EAOG+hQDhvoYA4b6HAOG+iADhvokA4b6KAOG+iwDhvowA4b6NAOG+jgDhvo8A4b6QAOG+kQDhvpIA4b6TAOG+lADhvpUA4b6WAOG+lwDhvpgA4b6ZAOG+mgDhvpsA4b6cAOG+nQDhvp4A4b6fAOG+oADhvqEA4b6iAOG+owDhvqQA4b6lAOG+pgDhvqcA4b6oAOG+qQDhvqoA4b6rAOG+rADhvq0A4b6uAOG+rwDhvrAA4b6xAOG+sgDhvrMA4b60AOG+tgDhvrcA4b64AOG+uQDhvroA4b68AOG/ggDhv4MA4b+EAOG/hgDhv4cA4b+IAOG/igDhv4wA4b+QAOG/kQDhv5IA4b+WAOG/lwDhv5gA4b+ZAOG/mgDhv6AA4b+hAOG/ogDhv6QA4b+lAOG/pgDhv6cA4b+oAOG/qQDhv6oA4b+sAOG/sgDhv7MA4b+0AOG/tgDhv7cA4b+4AOG/ugDhv7wA4oCQAOKAkwDigJQA4oCy4oCyAOKAsuKAsuKAsgDigLLigLLigLLigLIA4oC14oC1AOKAteKAteKAtQDigqkA4oaQAOKGkQDihpIA4oaTAOKGmgDihpsA4oauAOKHjQDih44A4oePAOKIggDiiIQA4oiHAOKIiQDiiIwA4oiRAOKIkgDiiKQA4oimAOKIq+KIqwDiiKviiKviiKsA4oir4oir4oir4oirAOKIruKIrgDiiK7iiK7iiK4A4omBAOKJhADiiYcA4omJAOKJoADiiaIA4omtAOKJrgDiia8A4omwAOKJsQDiibQA4om1AOKJuADiibkA4oqAAOKKgQDiioQA4oqFAOKKiADiiokA4oqsAOKKrQDiiq4A4oqvAOKLoADii6EA4ouiAOKLowDii6oA4ourAOKLrADii60A4pSCAOKWoADil4sA4qaFAOKmhgDiq53MuADitaEA44CBAOOAggDjgIgA44CJAOOAigDjgIsA44CMAOOAjQDjgI4A44CPAOOAkADjgJEA44CSAOOAlADjgJRT44CVAOOAlOS4ieOAlQDjgJTkuozjgJUA44CU5Yud44CVAOOAlOWuieOAlQDjgJTmiZPjgJUA44CU5pWX44CVAOOAlOacrOOAlQDjgJTngrnjgJUA44CU55uX44CVAOOAlQDjgJYA44CXAOOBjADjgY4A44GQAOOBkgDjgZQA44GWAOOBmADjgZoA44GcAOOBngDjgaAA44GiAOOBpQDjgacA44GpAOOBsADjgbEA44GzAOOBtADjgbYA44G3AOOBuQDjgboA44G744GLAOOBvADjgb0A44KI44KKAOOClADjgpkA44KaAOOCngDjgqEA44KiAOOCouODkeODvOODiADjgqLjg6vjg5XjgqEA44Ki44Oz44Oa44KiAOOCouODvOODqwDjgqMA44KkAOOCpOODi+ODs+OCsADjgqTjg7Pjg4EA44KlAOOCpgDjgqbjgqnjg7MA44KnAOOCqADjgqjjgrnjgq/jg7zjg4kA44Ko44O844Kr44O8AOOCqQDjgqoA44Kq44Oz44K5AOOCquODvOODoADjgqsA44Kr44Kk44OqAOOCq+ODqeODg+ODiADjgqvjg63jg6rjg7wA44KsAOOCrOODreODswDjgqzjg7Pjg54A44KtAOOCreODpeODquODvADjgq3jg60A44Kt44Ot44Kw44Op44OgAOOCreODreODoeODvOODiOODqwDjgq3jg63jg6/jg4Pjg4gA44KuAOOCruOCrADjgq7jg4vjg7wA44Ku44Or44OA44O8AOOCrwDjgq/jg6vjgrzjgqTjg60A44Kv44Ot44O844ONAOOCsADjgrDjg6njg6AA44Kw44Op44Og44OI44OzAOOCsQDjgrHjg7zjgrkA44KyAOOCswDjgrPjgrMA44Kz44OIAOOCs+ODq+ODigDjgrPjg7zjg50A44K0AOOCtQDjgrXjgqTjgq/jg6sA44K144Oz44OB44O844OgAOOCtgDjgrcA44K344Oq44Oz44KwAOOCuADjgrkA44K6AOOCuwDjgrvjg7Pjg4EA44K744Oz44OIAOOCvADjgr0A44K+AOOCvwDjg4AA44OA44O844K5AOODgQDjg4IA44ODAOODhADjg4UA44OGAOODhwDjg4fjgrcA44OIAOODiOODswDjg4kA44OJ44OrAOODigDjg4rjg44A44OLAOODjADjg40A44OOAOODjuODg+ODiADjg48A44OP44Kk44OEAOODkADjg5Djg7zjg6zjg6sA44ORAOODkeODvOOCu+ODs+ODiADjg5Hjg7zjg4QA44OSAOODkwDjg5Pjg6sA44OUAOODlOOCouOCueODiOODqwDjg5Tjgq/jg6sA44OU44KzAOODlQDjg5XjgqHjg6njg4Pjg4kA44OV44Kj44O844OIAOODleODqeODswDjg5YA44OW44OD44K344Kn44OrAOODlwDjg5gA44OY44Kv44K/44O844OrAOODmOODq+ODhADjg5kA44OZ44O844K/AOODmgDjg5rjgr0A44Oa44OL44OSAOODmuODs+OCuQDjg5rjg7zjgrgA44ObAOODm+ODswDjg5vjg7zjg6sA44Ob44O844OzAOODnADjg5zjg6vjg4gA44OdAOODneOCpOODs+ODiADjg53jg7Pjg4kA44OeAOODnuOCpOOCr+ODrQDjg57jgqTjg6sA44Oe44OD44OPAOODnuODq+OCrwDjg57jg7Pjgrfjg6fjg7MA44OfAOODn+OCr+ODreODswDjg5/jg6oA44Of44Oq44OQ44O844OrAOODoADjg6EA44Oh44KsAOODoeOCrOODiOODswDjg6Hjg7zjg4jjg6sA44OiAOODowDjg6QA44Ok44O844OJAOODpOODvOODqwDjg6UA44OmAOODpuOCouODswDjg6cA44OoAOODqQDjg6oA44Oq44OD44OI44OrAOODquODqQDjg6sA44Or44OU44O8AOODq+ODvOODluODqwDjg6wA44Os44OgAOODrOODs+ODiOOCsuODswDjg60A44OvAOODr+ODg+ODiADjg7AA44OxAOODsgDjg7MA44O0AOODtwDjg7gA44O5AOODugDjg7sA44O8AOODvgDjkp4A45K5AOOSuwDjk58A45SVAOObrgDjm7wA456BAOOgrwDjoaIA46G8AOOjhwDjo6MA46ScAOOkugDjqK4A46msAOOrpADjrIgA46yZAOOtiQDjrp0A47CYAOOxjgDjtLMA47aWAOO6rADjurgA47ybAOO/vADkgIgA5ICYAOSAuQDkgYYA5IKWAOSDowDkhK8A5IiCAOSIpwDkiqAA5IyBAOSMtADkjZkA5I+VAOSPmQDkkIsA5JGrAOSUqwDklZ0A5JWhAOSVqwDkl5cA5Je5AOSYtQDkmr4A5JuHAOSmlQDkp6YA5KmuAOSptgDkqrIA5KyzAOSvjgDks44A5LOtAOSzuADktZYA5LiAAOS4gQDkuIMA5LiJAOS4igDkuIsA5LiNAOS4mQDkuKYA5LioAOS4rQDkuLIA5Li2AOS4uADkuLkA5Li9AOS4vwDkuYEA5LmZAOS5nQDkuoIA5LqFAOS6hgDkuowA5LqUAOS6oADkuqQA5LquAOS6ugDku4AA5LuMAOS7pADkvIEA5LyRAOS9oADkvoAA5L6GAOS+iwDkvq4A5L67AOS+vwDlgIIA5YCrAOWBugDlgpkA5YOPAOWDmgDlg6cA5YSqAOWEvwDlhYAA5YWFAOWFjQDlhZQA5YWkAOWFpQDlhacA5YWoAOWFqQDlhasA5YWtAOWFtwDlhoAA5YaCAOWGjQDlhpIA5YaVAOWGlgDlhpcA5YaZAOWGpADlhqsA5YasAOWGtQDlhrcA5YeJAOWHjADlh5wA5YeeAOWHoADlh7UA5YiAAOWIgwDliIcA5YiXAOWInQDliKkA5Yi6AOWIuwDliYYA5YmNAOWJsgDlibcA5YqJAOWKmwDliqMA5YqzAOWKtADli4cA5YuJAOWLkgDli54A5YukAOWLtQDli7kA5Yu6AOWMhQDljIYA5YyVAOWMlwDljJoA5Yy4AOWMuwDljL8A5Y2BAOWNhADljYUA5Y2JAOWNkQDljZQA5Y2aAOWNnADljakA5Y2wAOWNswDljbUA5Y29AOWNvwDljoIA5Y62AOWPgwDlj4gA5Y+KAOWPjADlj58A5Y+jAOWPpQDlj6sA5Y+vAOWPsQDlj7MA5ZCGAOWQiADlkI0A5ZCPAOWQnQDlkLgA5ZC5AOWRggDlkYgA5ZGoAOWSngDlkqIA5ZK9AOWTtgDllJAA5ZWPAOWVkwDllZUA5ZWjAOWWhADllocA5ZaZAOWWnQDllqsA5ZazAOWWtgDll4AA5ZeCAOWXogDlmIYA5ZmRAOWZqADlmbQA5ZuXAOWbmwDlm7kA5ZyWAOWclwDlnJ8A5ZywAOWeiwDln44A5Z+0AOWgjQDloLEA5aCyAOWhgADloZoA5aGeAOWiqADloqwA5aKzAOWjmADlo58A5aOrAOWjrgDlo7AA5aOyAOWjtwDlpIIA5aSGAOWkigDlpJUA5aSaAOWknADlpKIA5aSnAOWkp+atowDlpKkA5aWEAOWliADlpZEA5aWUAOWlogDlpbMA5aeYAOWnrADlqJsA5ainAOWpogDlqaYA5aq1AOWsiADlrKgA5ay+AOWtkADlrZcA5a2mAOWugADlroUA5a6XAOWvgwDlr5gA5a+nAOWvrgDlr7MA5a+4AOWvvwDlsIYA5bCPAOWwogDlsLgA5bC/AOWxoADlsaIA5bGkAOWxpQDlsa4A5bGxAOWyjQDls4AA5bSZAOW1gwDltZAA5bWrAOW1rgDltbwA5bayAOW2ugDlt5sA5behAOW3ogDlt6UA5bemAOW3sQDlt70A5be+AOW4qADluL0A5bmpAOW5sgDlubPmiJAA5bm0AOW5ugDlubwA5bm/AOW6pgDlurAA5bqzAOW6tgDlu4kA5buKAOW7kgDlu5MA5buZAOW7rADlu7QA5bu+AOW8hADlvIsA5byTAOW8ogDlvZAA5b2TAOW9oQDlvaIA5b2pAOW9qwDlvbMA5b6LAOW+jADlvpcA5b6aAOW+qQDlvq0A5b+DAOW/jQDlv5cA5b+1AOW/uQDmgJIA5oCcAOaBtQDmgoEA5oKUAOaDhwDmg5gA5oOhAOaEiADmhYQA5oWIAOaFjADmhY4A5oWgAOaFqADmhboA5oaOAOaGkADmhqQA5oavAOaGsgDmh54A5oeyAOaHtgDmiIAA5oiIAOaIkADmiJsA5oiuAOaItADmiLYA5omLAOaJkwDmiZ0A5oqVAOaKsQDmi4kA5ouPAOaLkwDmi5QA5ou8AOaLvgDmjIcA5oy9AOaNkADmjZUA5o2oAOaNuwDmjoMA5o6gAOaOqQDmj4QA5o+FAOaPpADmkJwA5pCiAOaRkgDmkakA5pG3AOaRvgDmkpoA5pKdAOaThADmlK8A5pS0AOaVjwDmlZYA5pWsAOaVuADmlocA5paXAOaWmQDmlqQA5pawAOaWuQDml4UA5pegAOaXogDml6MA5pelAOaYjuayuwDmmJMA5pigAOaYreWSjADmmYkA5pm0AOaaiADmmpEA5pqcAOaatADmm4YA5puwAOabtADmm7gA5pyAAOaciADmnIkA5pyXAOacmwDmnKEA5pyoAOadjgDmnZMA5p2WAOadngDmnbsA5p6FAOaelwDmn7MA5p+6AOaglwDmoJ8A5qCqAOagquW8j+S8muekvgDmoZIA5qKBAOaihQDmoo4A5qKoAOaklADmpYIA5qajAOanqgDmqIIA5qiTAOaqqADmq5MA5qubAOashADmrKAA5qyhAOatlADmraIA5q2jAOatsgDmrbcA5q25AOaunwDmrq4A5q6zAOauugDmrrsA5q+LAOavjQDmr5QA5q+bAOawjwDmsJQA5rC0AOaxjgDmsacA5rKIAOayvwDms4wA5rONAOazpQDms6gA5rSWAOa0mwDmtJ4A5rS0AOa0vgDmtYEA5rWpAOa1qgDmtbcA5rW4AOa2hQDmt4sA5reaAOa3qgDmt7kA5riaAOa4rwDmua4A5rqAAOa6nADmuroA5ruHAOa7iwDmu5EA5rubAOa8jwDmvJQA5ryiAOa8owDmva4A5r+GAOa/qwDmv74A54CbAOeAngDngLkA54GKAOeBqwDngbAA54G3AOeBvQDngpkA54KtAOeDiADng5kA54ShAOeFhQDnhYkA54WuAOeGnADnh44A54eQAOeIkADniJsA54ioAOeIqgDniKsA54i1AOeItgDniLsA54i/AOeJhwDniZAA54mZAOeJmwDniaIA54m5AOeKgADnipUA54qsAOeKrwDni4AA54u8AOeMqgDnjbUA5426AOeOhADnjocA546JAOeOiwDnjqUA546yAOePngDnkIYA55CJAOeQogDnkYcA55GcAOeRqQDnkbEA55KFAOeSiQDnkpgA55OKAOeTnADnk6YA55SGAOeUmADnlJ8A55SkAOeUqADnlLAA55SyAOeUswDnlLcA55S7AOeUvgDnlZkA55WlAOeVsADnlosA55aSAOeXogDnmJAA55idAOeYnwDnmYIA55mpAOeZtgDnmb0A55quAOeavwDnm4oA55ubAOebowDnm6cA55uuAOebtADnnIEA55yeAOecnwDnnYAA552KAOeeiwDnnqcA55+bAOefogDnn7MA56GOAOehqwDnoowA56KRAOejigDno4wA56O7AOekqgDnpLoA56S8AOekvgDnpYgA56WJAOelkADnpZYA56WdAOelngDnpaUA56W/AOemgQDnpo0A56aOAOemjwDnpq4A56a4AOemvgDnp4oA56eYAOenqwDnqJwA56mAAOepigDnqY8A56m0AOepugDnqoEA56qxAOeriwDnq64A56u5AOesoADnro8A56+AAOevhgDnr4kA57C+AOexoADnsbMA57G7AOeykgDnsr4A57OSAOezlgDns6MA57OnAOezqADns7gA57SAAOe0kADntKIA57SvAOe1ggDntZsA57WjAOe2oADntr4A57eHAOe3tADnuIIA57iJAOe4twDnuYEA57mFAOe8tgDnvL4A572RAOe9sgDnvbkA5726AOe+hQDnvooA576VAOe+mgDnvr0A57+6AOiAgQDogIUA6ICMAOiAkgDogLMA6IGGAOiBoADoga8A6IGwAOiBvgDogb8A6IKJAOiCiwDogq0A6IKyAOiEgwDohL4A6IeYAOiHowDoh6gA6IeqAOiHrQDoh7MA6Ie8AOiIgQDoiIQA6IiMAOiImADoiJsA6IifAOiJrgDoia8A6ImyAOiJuADoibkA6IqLAOiKkQDoip0A6IqxAOiKswDoir0A6IulAOiLpgDojJ0A6IyjAOiMtgDojZIA6I2TAOiNowDojq0A6I69AOiPiQDoj4oA6I+MAOiPnADoj6cA6I+vAOiPsQDokL0A6JGJAOiRlwDok64A6JOxAOiTswDok7wA6JSWAOiVpADol40A6Je6AOiYhgDomJIA6JitAOiYvwDomY0A6JmQAOiZnADomacA6JmpAOiZqwDomogA6JqpAOibogDonI4A6JyoAOidqwDonbkA6J6GAOieugDon6EA6KCBAOignwDooYAA6KGMAOihoADooaMA6KOCAOijjwDoo5cA6KOeAOijoQDoo7gA6KO6AOikkADopYEA6KWkAOilvgDopoYA6KaLAOimlgDop5IA6KejAOiogADoqqAA6KqqAOiqvwDoq4sA6KuSAOirlgDoq60A6Ku4AOirvgDorIEA6Ky5AOitmADoroAA6K6KAOiwtwDosYYA6LGIAOixlQDosbgA6LKdAOiyoQDosqkA6LKrAOizgQDos4IA6LOHAOiziADos5MA6LSIAOi0mwDotaQA6LWwAOi1twDotrMA6La8AOi3iwDot68A6LewAOi6qwDou4oA6LuUAOi8pgDovKoA6Ly4AOi8uwDovaIA6L6bAOi+ngDovrAA6L61AOi+tgDpgKMA6YC4AOmBigDpgakA6YGyAOmBvADpgo8A6YKRAOmClADpg44A6YOeAOmDsQDpg70A6YSRAOmEmwDphYkA6YWqAOmGmQDphrQA6YeGAOmHjADph48A6YeRAOmItADpiLgA6Ym2AOmJvADpi5cA6YuYAOmMhADpjYoA6Y+5AOmQlQDplbcA6ZaAAOmWiwDplq0A6Za3AOmYnADpmK4A6ZmLAOmZjQDpmbUA6Zm4AOmZvADpmoYA6ZqjAOmatgDpmrcA6Zq4AOmauQDpm4MA6ZuiAOmbowDpm6gA6Zu2AOmbtwDpnKMA6ZyyAOmdiADpnZEA6Z2WAOmdngDpnaIA6Z2pAOmfiwDpn5sA6Z+gAOmfrQDpn7MA6Z+/AOmggQDpoIUA6aCLAOmgmADpoKkA6aC7AOmhngDpoqgA6aObAOmjnwDpo6IA6aOvAOmjvADppKgA6aSpAOmmlgDpppkA6aanAOmmrADpp4IA6aexAOmnvgDpqaoA6aqoAOmrmADpq58A6aySAOmspQDprK8A6ayyAOmsvADprZoA6a2vAOmxgADpsZcA6bOlAOmzvQDptacA6ba0AOm3ugDpuJ4A6bm1AOm5vwDpupcA6bqfAOm6pQDpursA6buDAOm7jQDpu44A6buRAOm7uQDpu70A6bu+AOm8hQDpvI4A6byPAOm8kwDpvJYA6bygAOm8uwDpvYMA6b2KAOm9kgDpvo0A6b6OAOm+nADpvp8A6b6gAOqcpwDqna8A6qy3AOqtkgDqsIAA6rCBAOqwggDqsIMA6rCEAOqwhQDqsIYA6rCHAOqwiADqsIkA6rCKAOqwiwDqsIwA6rCNAOqwjgDqsI8A6rCQAOqwkQDqsJIA6rCTAOqwlADqsJUA6rCWAOqwlwDqsJgA6rCZAOqwmgDqsJsA6rCcAOqwnQDqsJ4A6rCfAOqwoADqsKEA6rCiAOqwowDqsKQA6rClAOqwpgDqsKcA6rCoAOqwqQDqsKoA6rCrAOqwrADqsK0A6rCuAOqwrwDqsLAA6rCxAOqwsgDqsLMA6rC0AOqwtQDqsLYA6rC3AOqwuADqsLkA6rC6AOqwuwDqsLwA6rC9AOqwvgDqsL8A6rGAAOqxgQDqsYIA6rGDAOqxhADqsYUA6rGGAOqxhwDqsYgA6rGJAOqxigDqsYsA6rGMAOqxjQDqsY4A6rGPAOqxkADqsZEA6rGSAOqxkwDqsZQA6rGVAOqxlgDqsZcA6rGYAOqxmQDqsZoA6rGbAOqxnADqsZ0A6rGeAOqxnwDqsaAA6rGhAOqxogDqsaMA6rGkAOqxpQDqsaYA6rGnAOqxqADqsakA6rGqAOqxqwDqsawA6rGtAOqxrgDqsa8A6rGwAOqxsQDqsbIA6rGzAOqxtADqsbUA6rG2AOqxtwDqsbgA6rG5AOqxugDqsbsA6rG8AOqxvQDqsb4A6rG/AOqygADqsoEA6rKCAOqygwDqsoQA6rKFAOqyhgDqsocA6rKIAOqyiQDqsooA6rKLAOqyjADqso0A6rKOAOqyjwDqspAA6rKRAOqykgDqspMA6rKUAOqylQDqspYA6rKXAOqymADqspkA6rKaAOqymwDqspwA6rKdAOqyngDqsp8A6rKgAOqyoQDqsqIA6rKjAOqypADqsqUA6rKmAOqypwDqsqgA6rKpAOqyqgDqsqsA6rKsAOqyrQDqsq4A6rKvAOqysADqsrEA6rKyAOqyswDqsrQA6rK1AOqytgDqsrcA6rK4AOqyuQDqsroA6rK7AOqyvADqsr0A6rK+AOqyvwDqs4AA6rOBAOqzggDqs4MA6rOEAOqzhQDqs4YA6rOHAOqziADqs4kA6rOKAOqziwDqs4wA6rONAOqzjgDqs48A6rOQAOqzkQDqs5IA6rOTAOqzlADqs5UA6rOWAOqzlwDqs5gA6rOZAOqzmgDqs5sA6rOcAOqznQDqs54A6rOfAOqzoADqs6EA6rOiAOqzowDqs6QA6rOlAOqzpgDqs6cA6rOoAOqzqQDqs6oA6rOrAOqzrADqs60A6rOuAOqzrwDqs7AA6rOxAOqzsgDqs7MA6rO0AOqztQDqs7YA6rO3AOqzuADqs7kA6rO6AOqzuwDqs7wA6rO9AOqzvgDqs78A6rSAAOq0gQDqtIIA6rSDAOq0hADqtIUA6rSGAOq0hwDqtIgA6rSJAOq0igDqtIsA6rSMAOq0jQDqtI4A6rSPAOq0kADqtJEA6rSSAOq0kwDqtJQA6rSVAOq0lgDqtJcA6rSYAOq0mQDqtJoA6rSbAOq0nADqtJ0A6rSeAOq0nwDqtKAA6rShAOq0ogDqtKMA6rSkAOq0pQDqtKYA6rSnAOq0qADqtKkA6rSqAOq0qwDqtKwA6rStAOq0rgDqtK8A6rSwAOq0sQDqtLIA6rSzAOq0tADqtLUA6rS2AOq0twDqtLgA6rS5AOq0ugDqtLsA6rS8AOq0vQDqtL4A6rS/AOq1gADqtYEA6rWCAOq1gwDqtYQA6rWFAOq1hgDqtYcA6rWIAOq1iQDqtYoA6rWLAOq1jADqtY0A6rWOAOq1jwDqtZAA6rWRAOq1kgDqtZMA6rWUAOq1lQDqtZYA6rWXAOq1mADqtZkA6rWaAOq1mwDqtZwA6rWdAOq1ngDqtZ8A6rWgAOq1oQDqtaIA6rWjAOq1pADqtaUA6rWmAOq1pwDqtagA6rWpAOq1qgDqtasA6rWsAOq1rQDqta4A6rWvAOq1sADqtbEA6rWyAOq1swDqtbQA6rW1AOq1tgDqtbcA6rW4AOq1uQDqtboA6rW7AOq1vADqtb0A6rW+AOq1vwDqtoAA6raBAOq2ggDqtoMA6raEAOq2hQDqtoYA6raHAOq2iADqtokA6raKAOq2iwDqtowA6raNAOq2jgDqto8A6raQAOq2kQDqtpIA6raTAOq2lADqtpUA6raWAOq2lwDqtpgA6raZAOq2mgDqtpsA6racAOq2nQDqtp4A6rafAOq2oADqtqEA6raiAOq2owDqtqQA6ralAOq2pgDqtqcA6raoAOq2qQDqtqoA6rarAOq2rADqtq0A6rauAOq2rwDqtrAA6raxAOq2sgDqtrMA6ra0AOq2tQDqtrYA6ra3AOq2uADqtrkA6ra6AOq2uwDqtrwA6ra9AOq2vgDqtr8A6reAAOq3gQDqt4IA6reDAOq3hADqt4UA6reGAOq3hwDqt4gA6reJAOq3igDqt4sA6reMAOq3jQDqt44A6rePAOq3kADqt5EA6reSAOq3kwDqt5QA6reVAOq3lgDqt5cA6reYAOq3mQDqt5oA6rebAOq3nADqt50A6reeAOq3nwDqt6AA6rehAOq3ogDqt6MA6rekAOq3pQDqt6YA6renAOq3qADqt6kA6reqAOq3qwDqt6wA6retAOq3rgDqt68A6rewAOq3sQDqt7IA6rezAOq3tADqt7UA6re2AOq3twDqt7gA6re5AOq3ugDqt7sA6re8AOq3vQDqt74A6re/AOq4gADquIEA6riCAOq4gwDquIQA6riFAOq4hgDquIcA6riIAOq4iQDquIoA6riLAOq4jADquI0A6riOAOq4jwDquJAA6riRAOq4kgDquJMA6riUAOq4lQDquJYA6riXAOq4mADquJkA6riaAOq4mwDquJwA6ridAOq4ngDquJ8A6rigAOq4oQDquKIA6rijAOq4pADquKUA6rimAOq4pwDquKgA6ripAOq4qgDquKsA6risAOq4rQDquK4A6rivAOq4sADquLEA6riyAOq4swDquLQA6ri1AOq4tgDquLcA6ri4AOq4uQDquLoA6ri7AOq4vADquL0A6ri+AOq4vwDquYAA6rmBAOq5ggDquYMA6rmEAOq5hQDquYYA6rmHAOq5iADquYkA6rmKAOq5iwDquYwA6rmNAOq5jgDquY8A6rmQAOq5kQDquZIA6rmTAOq5lADquZUA6rmWAOq5lwDquZgA6rmZAOq5mgDquZsA6rmcAOq5nQDquZ4A6rmfAOq5oADquaEA6rmiAOq5owDquaQA6rmlAOq5pgDquacA6rmoAOq5qQDquaoA6rmrAOq5rADqua0A6rmuAOq5rwDqubAA6rmxAOq5sgDqubMA6rm0AOq5tQDqubYA6rm3AOq5uADqubkA6rm6AOq5uwDqubwA6rm9AOq5vgDqub8A6rqAAOq6gQDquoIA6rqDAOq6hADquoUA6rqGAOq6hwDquogA6rqJAOq6igDquosA6rqMAOq6jQDquo4A6rqPAOq6kADqupEA6rqSAOq6kwDqupQA6rqVAOq6lgDqupcA6rqYAOq6mQDqupoA6rqbAOq6nADqup0A6rqeAOq6nwDquqAA6rqhAOq6ogDquqMA6rqkAOq6pQDquqYA6rqnAOq6qADquqkA6rqqAOq6qwDquqwA6rqtAOq6rgDquq8A6rqwAOq6sQDqurIA6rqzAOq6tADqurUA6rq2AOq6twDqurgA6rq5AOq6ugDqursA6rq8AOq6vQDqur4A6rq/AOq7gADqu4EA6ruCAOq7gwDqu4QA6ruFAOq7hgDqu4cA6ruIAOq7iQDqu4oA6ruLAOq7jADqu40A6ruOAOq7jwDqu5AA6ruRAOq7kgDqu5MA6ruUAOq7lQDqu5YA6ruXAOq7mADqu5kA6ruaAOq7mwDqu5wA6rudAOq7ngDqu58A6rugAOq7oQDqu6IA6rujAOq7pADqu6UA6rumAOq7pwDqu6gA6rupAOq7qgDqu6sA6rusAOq7rQDqu64A6ruvAOq7sADqu7EA6ruyAOq7swDqu7QA6ru1AOq7tgDqu7cA6ru4AOq7uQDqu7oA6ru7AOq7vADqu70A6ru+AOq7vwDqvIAA6ryBAOq8ggDqvIMA6ryEAOq8hQDqvIYA6ryHAOq8iADqvIkA6ryKAOq8iwDqvIwA6ryNAOq8jgDqvI8A6ryQAOq8kQDqvJIA6ryTAOq8lADqvJUA6ryWAOq8lwDqvJgA6ryZAOq8mgDqvJsA6rycAOq8nQDqvJ4A6ryfAOq8oADqvKEA6ryiAOq8owDqvKQA6rylAOq8pgDqvKcA6ryoAOq8qQDqvKoA6ryrAOq8rADqvK0A6ryuAOq8rwDqvLAA6ryxAOq8sgDqvLMA6ry0AOq8tQDqvLYA6ry3AOq8uADqvLkA6ry6AOq8uwDqvLwA6ry9AOq8vgDqvL8A6r2AAOq9gQDqvYIA6r2DAOq9hADqvYUA6r2GAOq9hwDqvYgA6r2JAOq9igDqvYsA6r2MAOq9jQDqvY4A6r2PAOq9kADqvZEA6r2SAOq9kwDqvZQA6r2VAOq9lgDqvZcA6r2YAOq9mQDqvZoA6r2bAOq9nADqvZ0A6r2eAOq9nwDqvaAA6r2hAOq9ogDqvaMA6r2kAOq9pQDqvaYA6r2nAOq9qADqvakA6r2qAOq9qwDqvawA6r2tAOq9rgDqva8A6r2wAOq9sQDqvbIA6r2zAOq9tADqvbUA6r22AOq9twDqvbgA6r25AOq9ugDqvbsA6r28AOq9vQDqvb4A6r2/AOq+gADqvoEA6r6CAOq+gwDqvoQA6r6FAOq+hgDqvocA6r6IAOq+iQDqvooA6r6LAOq+jADqvo0A6r6OAOq+jwDqvpAA6r6RAOq+kgDqvpMA6r6UAOq+lQDqvpYA6r6XAOq+mADqvpkA6r6aAOq+mwDqvpwA6r6dAOq+ngDqvp8A6r6gAOq+oQDqvqIA6r6jAOq+pADqvqUA6r6mAOq+pwDqvqgA6r6pAOq+qgDqvqsA6r6sAOq+rQDqvq4A6r6vAOq+sADqvrEA6r6yAOq+swDqvrQA6r61AOq+tgDqvrcA6r64AOq+uQDqvroA6r67AOq+vADqvr0A6r6+AOq+vwDqv4AA6r+BAOq/ggDqv4MA6r+EAOq/hQDqv4YA6r+HAOq/iADqv4kA6r+KAOq/iwDqv4wA6r+NAOq/jgDqv48A6r+QAOq/kQDqv5IA6r+TAOq/lADqv5UA6r+WAOq/lwDqv5gA6r+ZAOq/mgDqv5sA6r+cAOq/nQDqv54A6r+fAOq/oADqv6EA6r+iAOq/owDqv6QA6r+lAOq/pgDqv6cA6r+oAOq/qQDqv6oA6r+rAOq/rADqv60A6r+uAOq/rwDqv7AA6r+xAOq/sgDqv7MA6r+0AOq/tQDqv7YA6r+3AOq/uADqv7kA6r+6AOq/uwDqv7wA6r+9AOq/vgDqv78A64CAAOuAgQDrgIIA64CDAOuAhADrgIUA64CGAOuAhwDrgIgA64CJAOuAigDrgIsA64CMAOuAjQDrgI4A64CPAOuAkADrgJEA64CSAOuAkwDrgJQA64CVAOuAlgDrgJcA64CYAOuAmQDrgJoA64CbAOuAnADrgJ0A64CeAOuAnwDrgKAA64ChAOuAogDrgKMA64CkAOuApQDrgKYA64CnAOuAqADrgKkA64CqAOuAqwDrgKwA64CtAOuArgDrgK8A64CwAOuAsQDrgLIA64CzAOuAtADrgLUA64C2AOuAtwDrgLgA64C5AOuAugDrgLsA64C8AOuAvQDrgL4A64C/AOuBgADrgYEA64GCAOuBgwDrgYQA64GFAOuBhgDrgYcA64GIAOuBiQDrgYoA64GLAOuBjADrgY0A64GOAOuBjwDrgZAA64GRAOuBkgDrgZMA64GUAOuBlQDrgZYA64GXAOuBmADrgZkA64GaAOuBmwDrgZwA64GdAOuBngDrgZ8A64GgAOuBoQDrgaIA64GjAOuBpADrgaUA64GmAOuBpwDrgagA64GpAOuBqgDrgasA64GsAOuBrQDrga4A64GvAOuBsADrgbEA64GyAOuBswDrgbQA64G1AOuBtgDrgbcA64G4AOuBuQDrgboA64G7AOuBvADrgb0A64G+AOuBvwDrgoAA64KBAOuCggDrgoMA64KEAOuChQDrgoYA64KHAOuCiADrgokA64KKAOuCiwDrgowA64KNAOuCjgDrgo8A64KQAOuCkQDrgpIA64KTAOuClADrgpUA64KWAOuClwDrgpgA64KZAOuCmgDrgpsA64KcAOuCnQDrgp4A64KfAOuCoADrgqEA64KiAOuCowDrgqQA64KlAOuCpgDrgqcA64KoAOuCqQDrgqoA64KrAOuCrADrgq0A64KuAOuCrwDrgrAA64KxAOuCsgDrgrMA64K0AOuCtQDrgrYA64K3AOuCuADrgrkA64K6AOuCuwDrgrwA64K9AOuCvgDrgr8A64OAAOuDgQDrg4IA64ODAOuDhADrg4UA64OGAOuDhwDrg4gA64OJAOuDigDrg4sA64OMAOuDjQDrg44A64OPAOuDkADrg5EA64OSAOuDkwDrg5QA64OVAOuDlgDrg5cA64OYAOuDmQDrg5oA64ObAOuDnADrg50A64OeAOuDnwDrg6AA64OhAOuDogDrg6MA64OkAOuDpQDrg6YA64OnAOuDqADrg6kA64OqAOuDqwDrg6wA64OtAOuDrgDrg68A64OwAOuDsQDrg7IA64OzAOuDtADrg7UA64O2AOuDtwDrg7gA64O5AOuDugDrg7sA64O8AOuDvQDrg74A64O/AOuEgADrhIEA64SCAOuEgwDrhIQA64SFAOuEhgDrhIcA64SIAOuEiQDrhIoA64SLAOuEjADrhI0A64SOAOuEjwDrhJAA64SRAOuEkgDrhJMA64SUAOuElQDrhJYA64SXAOuEmADrhJkA64SaAOuEmwDrhJwA64SdAOuEngDrhJ8A64SgAOuEoQDrhKIA64SjAOuEpADrhKUA64SmAOuEpwDrhKgA64SpAOuEqgDrhKsA64SsAOuErQDrhK4A64SvAOuEsADrhLEA64SyAOuEswDrhLQA64S1AOuEtgDrhLcA64S4AOuEuQDrhLoA64S7AOuEvADrhL0A64S+AOuEvwDrhYAA64WBAOuFggDrhYMA64WEAOuFhQDrhYYA64WHAOuFiADrhYkA64WKAOuFiwDrhYwA64WNAOuFjgDrhY8A64WQAOuFkQDrhZIA64WTAOuFlADrhZUA64WWAOuFlwDrhZgA64WZAOuFmgDrhZsA64WcAOuFnQDrhZ4A64WfAOuFoADrhaEA64WiAOuFowDrhaQA64WlAOuFpgDrhacA64WoAOuFqQDrhaoA64WrAOuFrADrha0A64WuAOuFrwDrhbAA64WxAOuFsgDrhbMA64W0AOuFtQDrhbYA64W3AOuFuADrhbkA64W6AOuFuwDrhbwA64W9AOuFvgDrhb8A64aAAOuGgQDrhoIA64aDAOuGhADrhoUA64aGAOuGhwDrhogA64aJAOuGigDrhosA64aMAOuGjQDrho4A64aPAOuGkADrhpEA64aSAOuGkwDrhpQA64aVAOuGlgDrhpcA64aYAOuGmQDrhpoA64abAOuGnADrhp0A64aeAOuGnwDrhqAA64ahAOuGogDrhqMA64akAOuGpQDrhqYA64anAOuGqADrhqkA64aqAOuGqwDrhqwA64atAOuGrgDrhq8A64awAOuGsQDrhrIA64azAOuGtADrhrUA64a2AOuGtwDrhrgA64a5AOuGugDrhrsA64a8AOuGvQDrhr4A64a/AOuHgADrh4EA64eCAOuHgwDrh4QA64eFAOuHhgDrh4cA64eIAOuHiQDrh4oA64eLAOuHjADrh40A64eOAOuHjwDrh5AA64eRAOuHkgDrh5MA64eUAOuHlQDrh5YA64eXAOuHmADrh5kA64eaAOuHmwDrh5wA64edAOuHngDrh58A64egAOuHoQDrh6IA64ejAOuHpADrh6UA64emAOuHpwDrh6gA64epAOuHqgDrh6sA64esAOuHrQDrh64A64evAOuHsADrh7EA64eyAOuHswDrh7QA64e1AOuHtgDrh7cA64e4AOuHuQDrh7oA64e7AOuHvADrh70A64e+AOuHvwDriIAA64iBAOuIggDriIMA64iEAOuIhQDriIYA64iHAOuIiADriIkA64iKAOuIiwDriIwA64iNAOuIjgDriI8A64iQAOuIkQDriJIA64iTAOuIlADriJUA64iWAOuIlwDriJgA64iZAOuImgDriJsA64icAOuInQDriJ4A64ifAOuIoADriKEA64iiAOuIowDriKQA64ilAOuIpgDriKcA64ioAOuIqQDriKoA64irAOuIrADriK0A64iuAOuIrwDriLAA64ixAOuIsgDriLMA64i0AOuItQDriLYA64i3AOuIuADriLkA64i6AOuIuwDriLwA64i9AOuIvgDriL8A64mAAOuJgQDriYIA64mDAOuJhADriYUA64mGAOuJhwDriYgA64mJAOuJigDriYsA64mMAOuJjQDriY4A64mPAOuJkADriZEA64mSAOuJkwDriZQA64mVAOuJlgDriZcA64mYAOuJmQDriZoA64mbAOuJnADriZ0A64meAOuJnwDriaAA64mhAOuJogDriaMA64mkAOuJpQDriaYA64mnAOuJqADriakA64mqAOuJqwDriawA64mtAOuJrgDria8A64mwAOuJsQDribIA64mzAOuJtADribUA64m2AOuJtwDribgA64m5AOuJugDribsA64m8AOuJvQDrib4A64m/AOuKgADrioEA64qCAOuKgwDrioQA64qFAOuKhgDriocA64qIAOuKiQDriooA64qLAOuKjADrio0A64qOAOuKjwDripAA64qRAOuKkgDripMA64qUAOuKlQDripYA64qXAOuKmADripkA64qaAOuKmwDripwA64qdAOuKngDrip8A64qgAOuKoQDriqIA64qjAOuKpADriqUA64qmAOuKpwDriqgA64qpAOuKqgDriqsA64qsAOuKrQDriq4A64qvAOuKsADrirEA64qyAOuKswDrirQA64q1AOuKtgDrircA64q4AOuKuQDriroA64q7AOuKvADrir0A64q+AOuKvwDri4AA64uBAOuLggDri4MA64uEAOuLhQDri4YA64uHAOuLiADri4kA64uKAOuLiwDri4wA64uNAOuLjgDri48A64uQAOuLkQDri5IA64uTAOuLlADri5UA64uWAOuLlwDri5gA64uZAOuLmgDri5sA64ucAOuLnQDri54A64ufAOuLoADri6EA64uiAOuLowDri6QA64ulAOuLpgDri6cA64uoAOuLqQDri6oA64urAOuLrADri60A64uuAOuLrwDri7AA64uxAOuLsgDri7MA64u0AOuLtQDri7YA64u3AOuLuADri7kA64u6AOuLuwDri7wA64u9AOuLvgDri78A64yAAOuMgQDrjIIA64yDAOuMhADrjIUA64yGAOuMhwDrjIgA64yJAOuMigDrjIsA64yMAOuMjQDrjI4A64yPAOuMkADrjJEA64ySAOuMkwDrjJQA64yVAOuMlgDrjJcA64yYAOuMmQDrjJoA64ybAOuMnADrjJ0A64yeAOuMnwDrjKAA64yhAOuMogDrjKMA64ykAOuMpQDrjKYA64ynAOuMqADrjKkA64yqAOuMqwDrjKwA64ytAOuMrgDrjK8A64ywAOuMsQDrjLIA64yzAOuMtADrjLUA64y2AOuMtwDrjLgA64y5AOuMugDrjLsA64y8AOuMvQDrjL4A64y/AOuNgADrjYEA642CAOuNgwDrjYQA642FAOuNhgDrjYcA642IAOuNiQDrjYoA642LAOuNjADrjY0A642OAOuNjwDrjZAA642RAOuNkgDrjZMA642UAOuNlQDrjZYA642XAOuNmADrjZkA642aAOuNmwDrjZwA642dAOuNngDrjZ8A642gAOuNoQDrjaIA642jAOuNpADrjaUA642mAOuNpwDrjagA642pAOuNqgDrjasA642sAOuNrQDrja4A642vAOuNsADrjbEA642yAOuNswDrjbQA6421AOuNtgDrjbcA6424AOuNuQDrjboA6427AOuNvADrjb0A642+AOuNvwDrjoAA646BAOuOggDrjoMA646EAOuOhQDrjoYA646HAOuOiADrjokA646KAOuOiwDrjowA646NAOuOjgDrjo8A646QAOuOkQDrjpIA646TAOuOlADrjpUA646WAOuOlwDrjpgA646ZAOuOmgDrjpsA646cAOuOnQDrjp4A646fAOuOoADrjqEA646iAOuOowDrjqQA646lAOuOpgDrjqcA646oAOuOqQDrjqoA646rAOuOrADrjq0A646uAOuOrwDrjrAA646xAOuOsgDrjrMA6460AOuOtQDrjrYA6463AOuOuADrjrkA6466AOuOuwDrjrwA6469AOuOvgDrjr8A64+AAOuPgQDrj4IA64+DAOuPhADrj4UA64+GAOuPhwDrj4gA64+JAOuPigDrj4sA64+MAOuPjQDrj44A64+PAOuPkADrj5EA64+SAOuPkwDrj5QA64+VAOuPlgDrj5cA64+YAOuPmQDrj5oA64+bAOuPnADrj50A64+eAOuPnwDrj6AA64+hAOuPogDrj6MA64+kAOuPpQDrj6YA64+nAOuPqADrj6kA64+qAOuPqwDrj6wA64+tAOuPrgDrj68A64+wAOuPsQDrj7IA64+zAOuPtADrj7UA64+2AOuPtwDrj7gA64+5AOuPugDrj7sA64+8AOuPvQDrj74A64+/AOuQgADrkIEA65CCAOuQgwDrkIQA65CFAOuQhgDrkIcA65CIAOuQiQDrkIoA65CLAOuQjADrkI0A65COAOuQjwDrkJAA65CRAOuQkgDrkJMA65CUAOuQlQDrkJYA65CXAOuQmADrkJkA65CaAOuQmwDrkJwA65CdAOuQngDrkJ8A65CgAOuQoQDrkKIA65CjAOuQpADrkKUA65CmAOuQpwDrkKgA65CpAOuQqgDrkKsA65CsAOuQrQDrkK4A65CvAOuQsADrkLEA65CyAOuQswDrkLQA65C1AOuQtgDrkLcA65C4AOuQuQDrkLoA65C7AOuQvADrkL0A65C+AOuQvwDrkYAA65GBAOuRggDrkYMA65GEAOuRhQDrkYYA65GHAOuRiADrkYkA65GKAOuRiwDrkYwA65GNAOuRjgDrkY8A65GQAOuRkQDrkZIA65GTAOuRlADrkZUA65GWAOuRlwDrkZgA65GZAOuRmgDrkZsA65GcAOuRnQDrkZ4A65GfAOuRoADrkaEA65GiAOuRowDrkaQA65GlAOuRpgDrkacA65GoAOuRqQDrkaoA65GrAOuRrADrka0A65GuAOuRrwDrkbAA65GxAOuRsgDrkbMA65G0AOuRtQDrkbYA65G3AOuRuADrkbkA65G6AOuRuwDrkbwA65G9AOuRvgDrkb8A65KAAOuSgQDrkoIA65KDAOuShADrkoUA65KGAOuShwDrkogA65KJAOuSigDrkosA65KMAOuSjQDrko4A65KPAOuSkADrkpEA65KSAOuSkwDrkpQA65KVAOuSlgDrkpcA65KYAOuSmQDrkpoA65KbAOuSnADrkp0A65KeAOuSnwDrkqAA65KhAOuSogDrkqMA65KkAOuSpQDrkqYA65KnAOuSqADrkqkA65KqAOuSqwDrkqwA65KtAOuSrgDrkq8A65KwAOuSsQDrkrIA65KzAOuStADrkrUA65K2AOuStwDrkrgA65K5AOuSugDrkrsA65K8AOuSvQDrkr4A65K/AOuTgADrk4EA65OCAOuTgwDrk4QA65OFAOuThgDrk4cA65OIAOuTiQDrk4oA65OLAOuTjADrk40A65OOAOuTjwDrk5AA65ORAOuTkgDrk5MA65OUAOuTlQDrk5YA65OXAOuTmADrk5kA65OaAOuTmwDrk5wA65OdAOuTngDrk58A65OgAOuToQDrk6IA65OjAOuTpADrk6UA65OmAOuTpwDrk6gA65OpAOuTqgDrk6sA65OsAOuTrQDrk64A65OvAOuTsADrk7EA65OyAOuTswDrk7QA65O1AOuTtgDrk7cA65O4AOuTuQDrk7oA65O7AOuTvADrk70A65O+AOuTvwDrlIAA65SBAOuUggDrlIMA65SEAOuUhQDrlIYA65SHAOuUiADrlIkA65SKAOuUiwDrlIwA65SNAOuUjgDrlI8A65SQAOuUkQDrlJIA65STAOuUlADrlJUA65SWAOuUlwDrlJgA65SZAOuUmgDrlJsA65ScAOuUnQDrlJ4A65SfAOuUoADrlKEA65SiAOuUowDrlKQA65SlAOuUpgDrlKcA65SoAOuUqQDrlKoA65SrAOuUrADrlK0A65SuAOuUrwDrlLAA65SxAOuUsgDrlLMA65S0AOuUtQDrlLYA65S3AOuUuADrlLkA65S6AOuUuwDrlLwA65S9AOuUvgDrlL8A65WAAOuVgQDrlYIA65WDAOuVhADrlYUA65WGAOuVhwDrlYgA65WJAOuVigDrlYsA65WMAOuVjQDrlY4A65WPAOuVkADrlZEA65WSAOuVkwDrlZQA65WVAOuVlgDrlZcA65WYAOuVmQDrlZoA65WbAOuVnADrlZ0A65WeAOuVnwDrlaAA65WhAOuVogDrlaMA65WkAOuVpQDrlaYA65WnAOuVqADrlakA65WqAOuVqwDrlawA65WtAOuVrgDrla8A65WwAOuVsQDrlbIA65WzAOuVtADrlbUA65W2AOuVtwDrlbgA65W5AOuVugDrlbsA65W8AOuVvQDrlb4A65W/AOuWgADrloEA65aCAOuWgwDrloQA65aFAOuWhgDrlocA65aIAOuWiQDrlooA65aLAOuWjADrlo0A65aOAOuWjwDrlpAA65aRAOuWkgDrlpMA65aUAOuWlQDrlpYA65aXAOuWmADrlpkA65aaAOuWmwDrlpwA65adAOuWngDrlp8A65agAOuWoQDrlqIA65ajAOuWpADrlqUA65amAOuWpwDrlqgA65apAOuWqgDrlqsA65asAOuWrQDrlq4A65avAOuWsADrlrEA65ayAOuWswDrlrQA65a1AOuWtgDrlrcA65a4AOuWuQDrlroA65a7AOuWvADrlr0A65a+AOuWvwDrl4AA65eBAOuXggDrl4MA65eEAOuXhQDrl4YA65eHAOuXiADrl4kA65eKAOuXiwDrl4wA65eNAOuXjgDrl48A65eQAOuXkQDrl5IA65eTAOuXlADrl5UA65eWAOuXlwDrl5gA65eZAOuXmgDrl5sA65ecAOuXnQDrl54A65efAOuXoADrl6EA65eiAOuXowDrl6QA65elAOuXpgDrl6cA65eoAOuXqQDrl6oA65erAOuXrADrl60A65euAOuXrwDrl7AA65exAOuXsgDrl7MA65e0AOuXtQDrl7YA65e3AOuXuADrl7kA65e6AOuXuwDrl7wA65e9AOuXvgDrl78A65iAAOuYgQDrmIIA65iDAOuYhADrmIUA65iGAOuYhwDrmIgA65iJAOuYigDrmIsA65iMAOuYjQDrmI4A65iPAOuYkADrmJEA65iSAOuYkwDrmJQA65iVAOuYlgDrmJcA65iYAOuYmQDrmJoA65ibAOuYnADrmJ0A65ieAOuYnwDrmKAA65ihAOuYogDrmKMA65ikAOuYpQDrmKYA65inAOuYqADrmKkA65iqAOuYqwDrmKwA65itAOuYrgDrmK8A65iwAOuYsQDrmLIA65izAOuYtADrmLUA65i2AOuYtwDrmLgA65i5AOuYugDrmLsA65i8AOuYvQDrmL4A65i/AOuZgADrmYEA65mCAOuZgwDrmYQA65mFAOuZhgDrmYcA65mIAOuZiQDrmYoA65mLAOuZjADrmY0A65mOAOuZjwDrmZAA65mRAOuZkgDrmZMA65mUAOuZlQDrmZYA65mXAOuZmADrmZkA65maAOuZmwDrmZwA65mdAOuZngDrmZ8A65mgAOuZoQDrmaIA65mjAOuZpADrmaUA65mmAOuZpwDrmagA65mpAOuZqgDrmasA65msAOuZrQDrma4A65mvAOuZsADrmbEA65myAOuZswDrmbQA65m1AOuZtgDrmbcA65m4AOuZuQDrmboA65m7AOuZvADrmb0A65m+AOuZvwDrmoAA65qBAOuaggDrmoMA65qEAOuahQDrmoYA65qHAOuaiADrmokA65qKAOuaiwDrmowA65qNAOuajgDrmo8A65qQAOuakQDrmpIA65qTAOualADrmpUA65qWAOualwDrmpgA65qZAOuamgDrmpsA65qcAOuanQDrmp4A65qfAOuaoADrmqEA65qiAOuaowDrmqQA65qlAOuapgDrmqcA65qoAOuaqQDrmqoA65qrAOuarADrmq0A65quAOuarwDrmrAA65qxAOuasgDrmrMA65q0AOuatQDrmrYA65q3AOuauADrmrkA65q6AOuauwDrmrwA65q9AOuavgDrmr8A65uAAOubgQDrm4IA65uDAOubhADrm4UA65uGAOubhwDrm4gA65uJAOubigDrm4sA65uMAOubjQDrm44A65uPAOubkADrm5EA65uSAOubkwDrm5QA65uVAOublgDrm5cA65uYAOubmQDrm5oA65ubAOubnADrm50A65ueAOubnwDrm6AA65uhAOubogDrm6MA65ukAOubpQDrm6YA65unAOubqADrm6kA65uqAOubqwDrm6wA65utAOubrgDrm68A65uwAOubsQDrm7IA65uzAOubtADrm7UA65u2AOubtwDrm7gA65u5AOubugDrm7sA65u8AOubvQDrm74A65u/AOucgADrnIEA65yCAOucgwDrnIQA65yFAOuchgDrnIcA65yIAOuciQDrnIoA65yLAOucjADrnI0A65yOAOucjwDrnJAA65yRAOuckgDrnJMA65yUAOuclQDrnJYA65yXAOucmADrnJkA65yaAOucmwDrnJwA65ydAOucngDrnJ8A65ygAOucoQDrnKIA65yjAOucpADrnKUA65ymAOucpwDrnKgA65ypAOucqgDrnKsA65ysAOucrQDrnK4A65yvAOucsADrnLEA65yyAOucswDrnLQA65y1AOuctgDrnLcA65y4AOucuQDrnLoA65y7AOucvADrnL0A65y+AOucvwDrnYAA652BAOudggDrnYMA652EAOudhQDrnYYA652HAOudiADrnYkA652KAOudiwDrnYwA652NAOudjgDrnY8A652QAOudkQDrnZIA652TAOudlADrnZUA652WAOudlwDrnZgA652ZAOudmgDrnZsA652cAOudnQDrnZ4A652fAOudoADrnaEA652iAOudowDrnaQA652lAOudpgDrnacA652oAOudqQDrnaoA652rAOudrADrna0A652uAOudrwDrnbAA652xAOudsgDrnbMA6520AOudtQDrnbYA6523AOuduADrnbkA6526AOuduwDrnbwA6529AOudvgDrnb8A656AAOuegQDrnoIA656DAOuehADrnoUA656GAOuehwDrnogA656JAOueigDrnosA656MAOuejQDrno4A656PAOuekADrnpEA656SAOuekwDrnpQA656VAOuelgDrnpcA656YAOuemQDrnpoA656bAOuenADrnp0A656eAOuenwDrnqAA656hAOueogDrnqMA656kAOuepQDrnqYA656nAOueqADrnqkA656qAOueqwDrnqwA656tAOuergDrnq8A656wAOuesQDrnrIA656zAOuetADrnrUA6562AOuetwDrnrgA6565AOueugDrnrsA6568AOuevQDrnr4A656/AOufgADrn4EA65+CAOufgwDrn4QA65+FAOufhgDrn4cA65+IAOufiQDrn4oA65+LAOufjADrn40A65+OAOufjwDrn5AA65+RAOufkgDrn5MA65+UAOuflQDrn5YA65+XAOufmADrn5kA65+aAOufmwDrn5wA65+dAOufngDrn58A65+gAOufoQDrn6IA65+jAOufpADrn6UA65+mAOufpwDrn6gA65+pAOufqgDrn6sA65+sAOufrQDrn64A65+vAOufsADrn7EA65+yAOufswDrn7QA65+1AOuftgDrn7cA65+4AOufuQDrn7oA65+7AOufvADrn70A65++AOufvwDroIAA66CBAOugggDroIMA66CEAOughQDroIYA66CHAOugiADroIkA66CKAOugiwDroIwA66CNAOugjgDroI8A66CQAOugkQDroJIA66CTAOuglADroJUA66CWAOuglwDroJgA66CZAOugmgDroJsA66CcAOugnQDroJ4A66CfAOugoADroKEA66CiAOugowDroKQA66ClAOugpgDroKcA66CoAOugqQDroKoA66CrAOugrADroK0A66CuAOugrwDroLAA66CxAOugsgDroLMA66C0AOugtQDroLYA66C3AOuguADroLkA66C6AOuguwDroLwA66C9AOugvgDroL8A66GAAOuhgQDroYIA66GDAOuhhADroYUA66GGAOuhhwDroYgA66GJAOuhigDroYsA66GMAOuhjQDroY4A66GPAOuhkADroZEA66GSAOuhkwDroZQA66GVAOuhlgDroZcA66GYAOuhmQDroZoA66GbAOuhnADroZ0A66GeAOuhnwDroaAA66GhAOuhogDroaMA66GkAOuhpQDroaYA66GnAOuhqADroakA66GqAOuhqwDroawA66GtAOuhrgDroa8A66GwAOuhsQDrobIA66GzAOuhtADrobUA66G2AOuhtwDrobgA66G5AOuhugDrobsA66G8AOuhvQDrob4A66G/AOuigADrooEA66KCAOuigwDrooQA66KFAOuihgDroocA66KIAOuiiQDroooA66KLAOuijADroo0A66KOAOuijwDropAA66KRAOuikgDropMA66KUAOuilQDropYA66KXAOuimADropkA66KaAOuimwDropwA66KdAOuingDrop8A66KgAOuioQDroqIA66KjAOuipADroqUA66KmAOuipwDroqgA66KpAOuiqgDroqsA66KsAOuirQDroq4A66KvAOuisADrorEA66KyAOuiswDrorQA66K1AOuitgDrorcA66K4AOuiuQDroroA66K7AOuivADror0A66K+AOuivwDro4AA66OBAOujggDro4MA66OEAOujhQDro4YA66OHAOujiADro4kA66OKAOujiwDro4wA66ONAOujjgDro48A66OQAOujkQDro5IA66OTAOujlADro5UA66OWAOujlwDro5gA66OZAOujmgDro5sA66OcAOujnQDro54A66OfAOujoADro6EA66OiAOujowDro6QA66OlAOujpgDro6cA66OoAOujqQDro6oA66OrAOujrADro60A66OuAOujrwDro7AA66OxAOujsgDro7MA66O0AOujtQDro7YA66O3AOujuADro7kA66O6AOujuwDro7wA66O9AOujvgDro78A66SAAOukgQDrpIIA66SDAOukhADrpIUA66SGAOukhwDrpIgA66SJAOukigDrpIsA66SMAOukjQDrpI4A66SPAOukkADrpJEA66SSAOukkwDrpJQA66SVAOuklgDrpJcA66SYAOukmQDrpJoA66SbAOuknADrpJ0A66SeAOuknwDrpKAA66ShAOukogDrpKMA66SkAOukpQDrpKYA66SnAOukqADrpKkA66SqAOukqwDrpKwA66StAOukrgDrpK8A66SwAOuksQDrpLIA66SzAOuktADrpLUA66S2AOuktwDrpLgA66S5AOukugDrpLsA66S8AOukvQDrpL4A66S/AOulgADrpYEA66WCAOulgwDrpYQA66WFAOulhgDrpYcA66WIAOuliQDrpYoA66WLAOuljADrpY0A66WOAOuljwDrpZAA66WRAOulkgDrpZMA66WUAOullQDrpZYA66WXAOulmADrpZkA66WaAOulmwDrpZwA66WdAOulngDrpZ8A66WgAOuloQDrpaIA66WjAOulpADrpaUA66WmAOulpwDrpagA66WpAOulqgDrpasA66WsAOulrQDrpa4A66WvAOulsADrpbEA66WyAOulswDrpbQA66W1AOultgDrpbcA66W4AOuluQDrpboA66W7AOulvADrpb0A66W+AOulvwDrpoAA66aBAOumggDrpoMA66aEAOumhQDrpoYA66aHAOumiADrpokA66aKAOumiwDrpowA66aNAOumjgDrpo8A66aQAOumkQDrppIA66aTAOumlADrppUA66aWAOumlwDrppgA66aZAOummgDrppsA66acAOumnQDrpp4A66afAOumoADrpqEA66aiAOumowDrpqQA66alAOumpgDrpqcA66aoAOumqQDrpqoA66arAOumrADrpq0A66auAOumrwDrprAA66axAOumsgDrprMA66a0AOumtQDrprYA66a3AOumuADrprkA66a6AOumuwDrprwA66a9AOumvgDrpr8A66eAAOungQDrp4IA66eDAOunhADrp4UA66eGAOunhwDrp4gA66eJAOunigDrp4sA66eMAOunjQDrp44A66ePAOunkADrp5EA66eSAOunkwDrp5QA66eVAOunlgDrp5cA66eYAOunmQDrp5oA66ebAOunnADrp50A66eeAOunnwDrp6AA66ehAOunogDrp6MA66ekAOunpQDrp6YA66enAOunqADrp6kA66eqAOunqwDrp6wA66etAOunrgDrp68A66ewAOunsQDrp7IA66ezAOuntADrp7UA66e2AOuntwDrp7gA66e5AOunugDrp7sA66e8AOunvQDrp74A66e/AOuogADrqIEA66iCAOuogwDrqIQA66iFAOuohgDrqIcA66iIAOuoiQDrqIoA66iLAOuojADrqI0A66iOAOuojwDrqJAA66iRAOuokgDrqJMA66iUAOuolQDrqJYA66iXAOuomADrqJkA66iaAOuomwDrqJwA66idAOuongDrqJ8A66igAOuooQDrqKIA66ijAOuopADrqKUA66imAOuopwDrqKgA66ipAOuoqgDrqKsA66isAOuorQDrqK4A66ivAOuosADrqLEA66iyAOuoswDrqLQA66i1AOuotgDrqLcA66i4AOuouQDrqLoA66i7AOuovADrqL0A66i+AOuovwDrqYAA66mBAOupggDrqYMA66mEAOuphQDrqYYA66mHAOupiADrqYkA66mKAOupiwDrqYwA66mNAOupjgDrqY8A66mQAOupkQDrqZIA66mTAOuplADrqZUA66mWAOuplwDrqZgA66mZAOupmgDrqZsA66mcAOupnQDrqZ4A66mfAOupoADrqaEA66miAOupowDrqaQA66mlAOuppgDrqacA66moAOupqQDrqaoA66mrAOuprADrqa0A66muAOuprwDrqbAA66mxAOupsgDrqbMA66m0AOuptQDrqbYA66m3AOupuADrqbkA66m6AOupuwDrqbwA66m9AOupvgDrqb8A66qAAOuqgQDrqoIA66qDAOuqhADrqoUA66qGAOuqhwDrqogA66qJAOuqigDrqosA66qMAOuqjQDrqo4A66qPAOuqkADrqpEA66qSAOuqkwDrqpQA66qVAOuqlgDrqpcA66qYAOuqmQDrqpoA66qbAOuqnADrqp0A66qeAOuqnwDrqqAA66qhAOuqogDrqqMA66qkAOuqpQDrqqYA66qnAOuqqADrqqkA66qqAOuqqwDrqqwA66qtAOuqrgDrqq8A66qwAOuqsQDrqrIA66qzAOuqtADrqrUA66q2AOuqtwDrqrgA66q5AOuqugDrqrsA66q8AOuqvQDrqr4A66q/AOurgADrq4EA66uCAOurgwDrq4QA66uFAOurhgDrq4cA66uIAOuriQDrq4oA66uLAOurjADrq40A66uOAOurjwDrq5AA66uRAOurkgDrq5MA66uUAOurlQDrq5YA66uXAOurmADrq5kA66uaAOurmwDrq5wA66udAOurngDrq58A66ugAOuroQDrq6IA66ujAOurpADrq6UA66umAOurpwDrq6gA66upAOurqgDrq6sA66usAOurrQDrq64A66uvAOursADrq7EA66uyAOurswDrq7QA66u1AOurtgDrq7cA66u4AOuruQDrq7oA66u7AOurvADrq70A66u+AOurvwDrrIAA66yBAOusggDrrIMA66yEAOushQDrrIYA66yHAOusiADrrIkA66yKAOusiwDrrIwA66yNAOusjgDrrI8A66yQAOuskQDrrJIA66yTAOuslADrrJUA66yWAOuslwDrrJgA66yZAOusmgDrrJsA66ycAOusnQDrrJ4A66yfAOusoADrrKEA66yiAOusowDrrKQA66ylAOuspgDrrKcA66yoAOusqQDrrKoA66yrAOusrADrrK0A66yuAOusrwDrrLAA66yxAOussgDrrLMA66y0AOustQDrrLYA66y3AOusuADrrLkA66y6AOusuwDrrLwA66y9AOusvgDrrL8A662AAOutgQDrrYIA662DAOuthADrrYUA662GAOuthwDrrYgA662JAOutigDrrYsA662MAOutjQDrrY4A662PAOutkADrrZEA662SAOutkwDrrZQA662VAOutlgDrrZcA662YAOutmQDrrZoA662bAOutnADrrZ0A662eAOutnwDrraAA662hAOutogDrraMA662kAOutpQDrraYA662nAOutqADrrakA662qAOutqwDrrawA662tAOutrgDrra8A662wAOutsQDrrbIA662zAOuttADrrbUA6622AOuttwDrrbgA6625AOutugDrrbsA6628AOutvQDrrb4A662/AOuugADrroEA666CAOuugwDrroQA666FAOuuhgDrrocA666IAOuuiQDrrooA666LAOuujADrro0A666OAOuujwDrrpAA666RAOuukgDrrpMA666UAOuulQDrrpYA666XAOuumADrrpkA666aAOuumwDrrpwA666dAOuungDrrp8A666gAOuuoQDrrqIA666jAOuupADrrqUA666mAOuupwDrrqgA666pAOuuqgDrrqsA666sAOuurQDrrq4A666vAOuusADrrrEA666yAOuuswDrrrQA6661AOuutgDrrrcA6664AOuuuQDrrroA6667AOuuvADrrr0A666+AOuuvwDrr4AA66+BAOuvggDrr4MA66+EAOuvhQDrr4YA66+HAOuviADrr4kA66+KAOuviwDrr4wA66+NAOuvjgDrr48A66+QAOuvkQDrr5IA66+TAOuvlADrr5UA66+WAOuvlwDrr5gA66+ZAOuvmgDrr5sA66+cAOuvnQDrr54A66+fAOuvoADrr6EA66+iAOuvowDrr6QA66+lAOuvpgDrr6cA66+oAOuvqQDrr6oA66+rAOuvrADrr60A66+uAOuvrwDrr7AA66+xAOuvsgDrr7MA66+0AOuvtQDrr7YA66+3AOuvuADrr7kA66+6AOuvuwDrr7wA66+9AOuvvgDrr78A67CAAOuwgQDrsIIA67CDAOuwhADrsIUA67CGAOuwhwDrsIgA67CJAOuwigDrsIsA67CMAOuwjQDrsI4A67CPAOuwkADrsJEA67CSAOuwkwDrsJQA67CVAOuwlgDrsJcA67CYAOuwmQDrsJoA67CbAOuwnADrsJ0A67CeAOuwnwDrsKAA67ChAOuwogDrsKMA67CkAOuwpQDrsKYA67CnAOuwqADrsKkA67CqAOuwqwDrsKwA67CtAOuwrgDrsK8A67CwAOuwsQDrsLIA67CzAOuwtADrsLUA67C2AOuwtwDrsLgA67C5AOuwugDrsLsA67C8AOuwvQDrsL4A67C/AOuxgADrsYEA67GCAOuxgwDrsYQA67GFAOuxhgDrsYcA67GIAOuxiQDrsYoA67GLAOuxjADrsY0A67GOAOuxjwDrsZAA67GRAOuxkgDrsZMA67GUAOuxlQDrsZYA67GXAOuxmADrsZkA67GaAOuxmwDrsZwA67GdAOuxngDrsZ8A67GgAOuxoQDrsaIA67GjAOuxpADrsaUA67GmAOuxpwDrsagA67GpAOuxqgDrsasA67GsAOuxrQDrsa4A67GvAOuxsADrsbEA67GyAOuxswDrsbQA67G1AOuxtgDrsbcA67G4AOuxuQDrsboA67G7AOuxvADrsb0A67G+AOuxvwDrsoAA67KBAOuyggDrsoMA67KEAOuyhQDrsoYA67KHAOuyiADrsokA67KKAOuyiwDrsowA67KNAOuyjgDrso8A67KQAOuykQDrspIA67KTAOuylADrspUA67KWAOuylwDrspgA67KZAOuymgDrspsA67KcAOuynQDrsp4A67KfAOuyoADrsqEA67KiAOuyowDrsqQA67KlAOuypgDrsqcA67KoAOuyqQDrsqoA67KrAOuyrADrsq0A67KuAOuyrwDrsrAA67KxAOuysgDrsrMA67K0AOuytQDrsrYA67K3AOuyuADrsrkA67K6AOuyuwDrsrwA67K9AOuyvgDrsr8A67OAAOuzgQDrs4IA67ODAOuzhADrs4UA67OGAOuzhwDrs4gA67OJAOuzigDrs4sA67OMAOuzjQDrs44A67OPAOuzkADrs5EA67OSAOuzkwDrs5QA67OVAOuzlgDrs5cA67OYAOuzmQDrs5oA67ObAOuznADrs50A67OeAOuznwDrs6AA67OhAOuzogDrs6MA67OkAOuzpQDrs6YA67OnAOuzqADrs6kA67OqAOuzqwDrs6wA67OtAOuzrgDrs68A67OwAOuzsQDrs7IA67OzAOuztADrs7UA67O2AOuztwDrs7gA67O5AOuzugDrs7sA67O8AOuzvQDrs74A67O/AOu0gADrtIEA67SCAOu0gwDrtIQA67SFAOu0hgDrtIcA67SIAOu0iQDrtIoA67SLAOu0jADrtI0A67SOAOu0jwDrtJAA67SRAOu0kgDrtJMA67SUAOu0lQDrtJYA67SXAOu0mADrtJkA67SaAOu0mwDrtJwA67SdAOu0ngDrtJ8A67SgAOu0oQDrtKIA67SjAOu0pADrtKUA67SmAOu0pwDrtKgA67SpAOu0qgDrtKsA67SsAOu0rQDrtK4A67SvAOu0sADrtLEA67SyAOu0swDrtLQA67S1AOu0tgDrtLcA67S4AOu0uQDrtLoA67S7AOu0vADrtL0A67S+AOu0vwDrtYAA67WBAOu1ggDrtYMA67WEAOu1hQDrtYYA67WHAOu1iADrtYkA67WKAOu1iwDrtYwA67WNAOu1jgDrtY8A67WQAOu1kQDrtZIA67WTAOu1lADrtZUA67WWAOu1lwDrtZgA67WZAOu1mgDrtZsA67WcAOu1nQDrtZ4A67WfAOu1oADrtaEA67WiAOu1owDrtaQA67WlAOu1pgDrtacA67WoAOu1qQDrtaoA67WrAOu1rADrta0A67WuAOu1rwDrtbAA67WxAOu1sgDrtbMA67W0AOu1tQDrtbYA67W3AOu1uADrtbkA67W6AOu1uwDrtbwA67W9AOu1vgDrtb8A67aAAOu2gQDrtoIA67aDAOu2hADrtoUA67aGAOu2hwDrtogA67aJAOu2igDrtosA67aMAOu2jQDrto4A67aPAOu2kADrtpEA67aSAOu2kwDrtpQA67aVAOu2lgDrtpcA67aYAOu2mQDrtpoA67abAOu2nADrtp0A67aeAOu2nwDrtqAA67ahAOu2ogDrtqMA67akAOu2pQDrtqYA67anAOu2qADrtqkA67aqAOu2qwDrtqwA67atAOu2rgDrtq8A67awAOu2sQDrtrIA67azAOu2tADrtrUA67a2AOu2twDrtrgA67a5AOu2ugDrtrsA67a8AOu2vQDrtr4A67a/AOu3gADrt4EA67eCAOu3gwDrt4QA67eFAOu3hgDrt4cA67eIAOu3iQDrt4oA67eLAOu3jADrt40A67eOAOu3jwDrt5AA67eRAOu3kgDrt5MA67eUAOu3lQDrt5YA67eXAOu3mADrt5kA67eaAOu3mwDrt5wA67edAOu3ngDrt58A67egAOu3oQDrt6IA67ejAOu3pADrt6UA67emAOu3pwDrt6gA67epAOu3qgDrt6sA67esAOu3rQDrt64A67evAOu3sADrt7EA67eyAOu3swDrt7QA67e1AOu3tgDrt7cA67e4AOu3uQDrt7oA67e7AOu3vADrt70A67e+AOu3vwDruIAA67iBAOu4ggDruIMA67iEAOu4hQDruIYA67iHAOu4iADruIkA67iKAOu4iwDruIwA67iNAOu4jgDruI8A67iQAOu4kQDruJIA67iTAOu4lADruJUA67iWAOu4lwDruJgA67iZAOu4mgDruJsA67icAOu4nQDruJ4A67ifAOu4oADruKEA67iiAOu4owDruKQA67ilAOu4pgDruKcA67ioAOu4qQDruKoA67irAOu4rADruK0A67iuAOu4rwDruLAA67ixAOu4sgDruLMA67i0AOu4tQDruLYA67i3AOu4uADruLkA67i6AOu4uwDruLwA67i9AOu4vgDruL8A67mAAOu5gQDruYIA67mDAOu5hADruYUA67mGAOu5hwDruYgA67mJAOu5igDruYsA67mMAOu5jQDruY4A67mPAOu5kADruZEA67mSAOu5kwDruZQA67mVAOu5lgDruZcA67mYAOu5mQDruZoA67mbAOu5nADruZ0A67meAOu5nwDruaAA67mhAOu5ogDruaMA67mkAOu5pQDruaYA67mnAOu5qADruakA67mqAOu5qwDruawA67mtAOu5rgDrua8A67mwAOu5sQDrubIA67mzAOu5tADrubUA67m2AOu5twDrubgA67m5AOu5ugDrubsA67m8AOu5vQDrub4A67m/AOu6gADruoEA67qCAOu6gwDruoQA67qFAOu6hgDruocA67qIAOu6iQDruooA67qLAOu6jADruo0A67qOAOu6jwDrupAA67qRAOu6kgDrupMA67qUAOu6lQDrupYA67qXAOu6mADrupkA67qaAOu6mwDrupwA67qdAOu6ngDrup8A67qgAOu6oQDruqIA67qjAOu6pADruqUA67qmAOu6pwDruqgA67qpAOu6qgDruqsA67qsAOu6rQDruq4A67qvAOu6sADrurEA67qyAOu6swDrurQA67q1AOu6tgDrurcA67q4AOu6uQDruroA67q7AOu6vADrur0A67q+AOu6vwDru4AA67uBAOu7ggDru4MA67uEAOu7hQDru4YA67uHAOu7iADru4kA67uKAOu7iwDru4wA67uNAOu7jgDru48A67uQAOu7kQDru5IA67uTAOu7lADru5UA67uWAOu7lwDru5gA67uZAOu7mgDru5sA67ucAOu7nQDru54A67ufAOu7oADru6EA67uiAOu7owDru6QA67ulAOu7pgDru6cA67uoAOu7qQDru6oA67urAOu7rADru60A67uuAOu7rwDru7AA67uxAOu7sgDru7MA67u0AOu7tQDru7YA67u3AOu7uADru7kA67u6AOu7uwDru7wA67u9AOu7vgDru78A67yAAOu8gQDrvIIA67yDAOu8hADrvIUA67yGAOu8hwDrvIgA67yJAOu8igDrvIsA67yMAOu8jQDrvI4A67yPAOu8kADrvJEA67ySAOu8kwDrvJQA67yVAOu8lgDrvJcA67yYAOu8mQDrvJoA67ybAOu8nADrvJ0A67yeAOu8nwDrvKAA67yhAOu8ogDrvKMA67ykAOu8pQDrvKYA67ynAOu8qADrvKkA67yqAOu8qwDrvKwA67ytAOu8rgDrvK8A67ywAOu8sQDrvLIA67yzAOu8tADrvLUA67y2AOu8twDrvLgA67y5AOu8ugDrvLsA67y8AOu8vQDrvL4A67y/AOu9gADrvYEA672CAOu9gwDrvYQA672FAOu9hgDrvYcA672IAOu9iQDrvYoA672LAOu9jADrvY0A672OAOu9jwDrvZAA672RAOu9kgDrvZMA672UAOu9lQDrvZYA672XAOu9mADrvZkA672aAOu9mwDrvZwA672dAOu9ngDrvZ8A672gAOu9oQDrvaIA672jAOu9pADrvaUA672mAOu9pwDrvagA672pAOu9qgDrvasA672sAOu9rQDrva4A672vAOu9sADrvbEA672yAOu9swDrvbQA6721AOu9tgDrvbcA6724AOu9uQDrvboA6727AOu9vADrvb0A672+AOu9vwDrvoAA676BAOu+ggDrvoMA676EAOu+hQDrvoYA676HAOu+iADrvokA676KAOu+iwDrvowA676NAOu+jgDrvo8A676QAOu+kQDrvpIA676TAOu+lADrvpUA676WAOu+lwDrvpgA676ZAOu+mgDrvpsA676cAOu+nQDrvp4A676fAOu+oADrvqEA676iAOu+owDrvqQA676lAOu+pgDrvqcA676oAOu+qQDrvqoA676rAOu+rADrvq0A676uAOu+rwDrvrAA676xAOu+sgDrvrMA6760AOu+tQDrvrYA6763AOu+uADrvrkA6766AOu+uwDrvrwA6769AOu+vgDrvr8A67+AAOu/gQDrv4IA67+DAOu/hADrv4UA67+GAOu/hwDrv4gA67+JAOu/igDrv4sA67+MAOu/jQDrv44A67+PAOu/kADrv5EA67+SAOu/kwDrv5QA67+VAOu/lgDrv5cA67+YAOu/mQDrv5oA67+bAOu/nADrv50A67+eAOu/nwDrv6AA67+hAOu/ogDrv6MA67+kAOu/pQDrv6YA67+nAOu/qADrv6kA67+qAOu/qwDrv6wA67+tAOu/rgDrv68A67+wAOu/sQDrv7IA67+zAOu/tADrv7UA67+2AOu/twDrv7gA67+5AOu/ugDrv7sA67+8AOu/vQDrv74A67+/AOyAgADsgIEA7ICCAOyAgwDsgIQA7ICFAOyAhgDsgIcA7ICIAOyAiQDsgIoA7ICLAOyAjADsgI0A7ICOAOyAjwDsgJAA7ICRAOyAkgDsgJMA7ICUAOyAlQDsgJYA7ICXAOyAmADsgJkA7ICaAOyAmwDsgJwA7ICdAOyAngDsgJ8A7ICgAOyAoQDsgKIA7ICjAOyApADsgKUA7ICmAOyApwDsgKgA7ICpAOyAqgDsgKsA7ICsAOyArQDsgK4A7ICvAOyAsADsgLEA7ICyAOyAswDsgLQA7IC1AOyAtgDsgLcA7IC4AOyAuQDsgLoA7IC7AOyAvADsgL0A7IC+AOyAvwDsgYAA7IGBAOyBggDsgYMA7IGEAOyBhQDsgYYA7IGHAOyBiADsgYkA7IGKAOyBiwDsgYwA7IGNAOyBjgDsgY8A7IGQAOyBkQDsgZIA7IGTAOyBlADsgZUA7IGWAOyBlwDsgZgA7IGZAOyBmgDsgZsA7IGcAOyBnQDsgZ4A7IGfAOyBoADsgaEA7IGiAOyBowDsgaQA7IGlAOyBpgDsgacA7IGoAOyBqQDsgaoA7IGrAOyBrADsga0A7IGuAOyBrwDsgbAA7IGxAOyBsgDsgbMA7IG0AOyBtQDsgbYA7IG3AOyBuADsgbkA7IG6AOyBuwDsgbwA7IG9AOyBvgDsgb8A7IKAAOyCgQDsgoIA7IKDAOyChADsgoUA7IKGAOyChwDsgogA7IKJAOyCigDsgosA7IKMAOyCjQDsgo4A7IKPAOyCkADsgpEA7IKSAOyCkwDsgpQA7IKVAOyClgDsgpcA7IKYAOyCmQDsgpoA7IKbAOyCnADsgp0A7IKeAOyCnwDsgqAA7IKhAOyCogDsgqMA7IKkAOyCpQDsgqYA7IKnAOyCqADsgqkA7IKqAOyCqwDsgqwA7IKtAOyCrgDsgq8A7IKwAOyCsQDsgrIA7IKzAOyCtADsgrUA7IK2AOyCtwDsgrgA7IK5AOyCugDsgrsA7IK8AOyCvQDsgr4A7IK/AOyDgADsg4EA7IOCAOyDgwDsg4QA7IOFAOyDhgDsg4cA7IOIAOyDiQDsg4oA7IOLAOyDjADsg40A7IOOAOyDjwDsg5AA7IORAOyDkgDsg5MA7IOUAOyDlQDsg5YA7IOXAOyDmADsg5kA7IOaAOyDmwDsg5wA7IOdAOyDngDsg58A7IOgAOyDoQDsg6IA7IOjAOyDpADsg6UA7IOmAOyDpwDsg6gA7IOpAOyDqgDsg6sA7IOsAOyDrQDsg64A7IOvAOyDsADsg7EA7IOyAOyDswDsg7QA7IO1AOyDtgDsg7cA7IO4AOyDuQDsg7oA7IO7AOyDvADsg70A7IO+AOyDvwDshIAA7ISBAOyEggDshIMA7ISEAOyEhQDshIYA7ISHAOyEiADshIkA7ISKAOyEiwDshIwA7ISNAOyEjgDshI8A7ISQAOyEkQDshJIA7ISTAOyElADshJUA7ISWAOyElwDshJgA7ISZAOyEmgDshJsA7IScAOyEnQDshJ4A7ISfAOyEoADshKEA7ISiAOyEowDshKQA7ISlAOyEpgDshKcA7ISoAOyEqQDshKoA7ISrAOyErADshK0A7ISuAOyErwDshLAA7ISxAOyEsgDshLMA7IS0AOyEtQDshLYA7IS3AOyEuADshLkA7IS6AOyEuwDshLwA7IS9AOyEvgDshL8A7IWAAOyFgQDshYIA7IWDAOyFhADshYUA7IWGAOyFhwDshYgA7IWJAOyFigDshYsA7IWMAOyFjQDshY4A7IWPAOyFkADshZEA7IWSAOyFkwDshZQA7IWVAOyFlgDshZcA7IWYAOyFmQDshZoA7IWbAOyFnADshZ0A7IWeAOyFnwDshaAA7IWhAOyFogDshaMA7IWkAOyFpQDshaYA7IWnAOyFqADshakA7IWqAOyFqwDshawA7IWtAOyFrgDsha8A7IWwAOyFsQDshbIA7IWzAOyFtADshbUA7IW2AOyFtwDshbgA7IW5AOyFugDshbsA7IW8AOyFvQDshb4A7IW/AOyGgADshoEA7IaCAOyGgwDshoQA7IaFAOyGhgDshocA7IaIAOyGiQDshooA7IaLAOyGjADsho0A7IaOAOyGjwDshpAA7IaRAOyGkgDshpMA7IaUAOyGlQDshpYA7IaXAOyGmADshpkA7IaaAOyGmwDshpwA7IadAOyGngDshp8A7IagAOyGoQDshqIA7IajAOyGpADshqUA7IamAOyGpwDshqgA7IapAOyGqgDshqsA7IasAOyGrQDshq4A7IavAOyGsADshrEA7IayAOyGswDshrQA7Ia1AOyGtgDshrcA7Ia4AOyGuQDshroA7Ia7AOyGvADshr0A7Ia+AOyGvwDsh4AA7IeBAOyHggDsh4MA7IeEAOyHhQDsh4YA7IeHAOyHiADsh4kA7IeKAOyHiwDsh4wA7IeNAOyHjgDsh48A7IeQAOyHkQDsh5IA7IeTAOyHlADsh5UA7IeWAOyHlwDsh5gA7IeZAOyHmgDsh5sA7IecAOyHnQDsh54A7IefAOyHoADsh6EA7IeiAOyHowDsh6QA7IelAOyHpgDsh6cA7IeoAOyHqQDsh6oA7IerAOyHrADsh60A7IeuAOyHrwDsh7AA7IexAOyHsgDsh7MA7Ie0AOyHtQDsh7YA7Ie3AOyHuADsh7kA7Ie6AOyHuwDsh7wA7Ie9AOyHvgDsh78A7IiAAOyIgQDsiIIA7IiDAOyIhADsiIUA7IiGAOyIhwDsiIgA7IiJAOyIigDsiIsA7IiMAOyIjQDsiI4A7IiPAOyIkADsiJEA7IiSAOyIkwDsiJQA7IiVAOyIlgDsiJcA7IiYAOyImQDsiJoA7IibAOyInADsiJ0A7IieAOyInwDsiKAA7IihAOyIogDsiKMA7IikAOyIpQDsiKYA7IinAOyIqADsiKkA7IiqAOyIqwDsiKwA7IitAOyIrgDsiK8A7IiwAOyIsQDsiLIA7IizAOyItADsiLUA7Ii2AOyItwDsiLgA7Ii5AOyIugDsiLsA7Ii8AOyIvQDsiL4A7Ii/AOyJgADsiYEA7ImCAOyJgwDsiYQA7ImFAOyJhgDsiYcA7ImIAOyJiQDsiYoA7ImLAOyJjADsiY0A7ImOAOyJjwDsiZAA7ImRAOyJkgDsiZMA7ImUAOyJlQDsiZYA7ImXAOyJmADsiZkA7ImaAOyJmwDsiZwA7ImdAOyJngDsiZ8A7ImgAOyJoQDsiaIA7ImjAOyJpADsiaUA7ImmAOyJpwDsiagA7ImpAOyJqgDsiasA7ImsAOyJrQDsia4A7ImvAOyJsADsibEA7ImyAOyJswDsibQA7Im1AOyJtgDsibcA7Im4AOyJuQDsiboA7Im7AOyJvADsib0A7Im+AOyJvwDsioAA7IqBAOyKggDsioMA7IqEAOyKhQDsioYA7IqHAOyKiADsiokA7IqKAOyKiwDsiowA7IqNAOyKjgDsio8A7IqQAOyKkQDsipIA7IqTAOyKlADsipUA7IqWAOyKlwDsipgA7IqZAOyKmgDsipsA7IqcAOyKnQDsip4A7IqfAOyKoADsiqEA7IqiAOyKowDsiqQA7IqlAOyKpgDsiqcA7IqoAOyKqQDsiqoA7IqrAOyKrADsiq0A7IquAOyKrwDsirAA7IqxAOyKsgDsirMA7Iq0AOyKtQDsirYA7Iq3AOyKuADsirkA7Iq6AOyKuwDsirwA7Iq9AOyKvgDsir8A7IuAAOyLgQDsi4IA7IuDAOyLhADsi4UA7IuGAOyLhwDsi4gA7IuJAOyLigDsi4sA7IuMAOyLjQDsi44A7IuPAOyLkADsi5EA7IuSAOyLkwDsi5QA7IuVAOyLlgDsi5cA7IuYAOyLmQDsi5oA7IubAOyLnADsi50A7IueAOyLnwDsi6AA7IuhAOyLogDsi6MA7IukAOyLpQDsi6YA7IunAOyLqADsi6kA7IuqAOyLqwDsi6wA7IutAOyLrgDsi68A7IuwAOyLsQDsi7IA7IuzAOyLtADsi7UA7Iu2AOyLtwDsi7gA7Iu5AOyLugDsi7sA7Iu8AOyLvQDsi74A7Iu/AOyMgADsjIEA7IyCAOyMgwDsjIQA7IyFAOyMhgDsjIcA7IyIAOyMiQDsjIoA7IyLAOyMjADsjI0A7IyOAOyMjwDsjJAA7IyRAOyMkgDsjJMA7IyUAOyMlQDsjJYA7IyXAOyMmADsjJkA7IyaAOyMmwDsjJwA7IydAOyMngDsjJ8A7IygAOyMoQDsjKIA7IyjAOyMpADsjKUA7IymAOyMpwDsjKgA7IypAOyMqgDsjKsA7IysAOyMrQDsjK4A7IyvAOyMsADsjLEA7IyyAOyMswDsjLQA7Iy1AOyMtgDsjLcA7Iy4AOyMuQDsjLoA7Iy7AOyMvADsjL0A7Iy+AOyMvwDsjYAA7I2BAOyNggDsjYMA7I2EAOyNhQDsjYYA7I2HAOyNiADsjYkA7I2KAOyNiwDsjYwA7I2NAOyNjgDsjY8A7I2QAOyNkQDsjZIA7I2TAOyNlADsjZUA7I2WAOyNlwDsjZgA7I2ZAOyNmgDsjZsA7I2cAOyNnQDsjZ4A7I2fAOyNoADsjaEA7I2iAOyNowDsjaQA7I2lAOyNpgDsjacA7I2oAOyNqQDsjaoA7I2rAOyNrADsja0A7I2uAOyNrwDsjbAA7I2xAOyNsgDsjbMA7I20AOyNtQDsjbYA7I23AOyNuADsjbkA7I26AOyNuwDsjbwA7I29AOyNvgDsjb8A7I6AAOyOgQDsjoIA7I6DAOyOhADsjoUA7I6GAOyOhwDsjogA7I6JAOyOigDsjosA7I6MAOyOjQDsjo4A7I6PAOyOkADsjpEA7I6SAOyOkwDsjpQA7I6VAOyOlgDsjpcA7I6YAOyOmQDsjpoA7I6bAOyOnADsjp0A7I6eAOyOnwDsjqAA7I6hAOyOogDsjqMA7I6kAOyOpQDsjqYA7I6nAOyOqADsjqkA7I6qAOyOqwDsjqwA7I6tAOyOrgDsjq8A7I6wAOyOsQDsjrIA7I6zAOyOtADsjrUA7I62AOyOtwDsjrgA7I65AOyOugDsjrsA7I68AOyOvQDsjr4A7I6/AOyPgADsj4EA7I+CAOyPgwDsj4QA7I+FAOyPhgDsj4cA7I+IAOyPiQDsj4oA7I+LAOyPjADsj40A7I+OAOyPjwDsj5AA7I+RAOyPkgDsj5MA7I+UAOyPlQDsj5YA7I+XAOyPmADsj5kA7I+aAOyPmwDsj5wA7I+dAOyPngDsj58A7I+gAOyPoQDsj6IA7I+jAOyPpADsj6UA7I+mAOyPpwDsj6gA7I+pAOyPqgDsj6sA7I+sAOyPrQDsj64A7I+vAOyPsADsj7EA7I+yAOyPswDsj7QA7I+1AOyPtgDsj7cA7I+4AOyPuQDsj7oA7I+7AOyPvADsj70A7I++AOyPvwDskIAA7JCBAOyQggDskIMA7JCEAOyQhQDskIYA7JCHAOyQiADskIkA7JCKAOyQiwDskIwA7JCNAOyQjgDskI8A7JCQAOyQkQDskJIA7JCTAOyQlADskJUA7JCWAOyQlwDskJgA7JCZAOyQmgDskJsA7JCcAOyQnQDskJ4A7JCfAOyQoADskKEA7JCiAOyQowDskKQA7JClAOyQpgDskKcA7JCoAOyQqQDskKoA7JCrAOyQrADskK0A7JCuAOyQrwDskLAA7JCxAOyQsgDskLMA7JC0AOyQtQDskLYA7JC3AOyQuADskLkA7JC6AOyQuwDskLwA7JC9AOyQvgDskL8A7JGAAOyRgQDskYIA7JGDAOyRhADskYUA7JGGAOyRhwDskYgA7JGJAOyRigDskYsA7JGMAOyRjQDskY4A7JGPAOyRkADskZEA7JGSAOyRkwDskZQA7JGVAOyRlgDskZcA7JGYAOyRmQDskZoA7JGbAOyRnADskZ0A7JGeAOyRnwDskaAA7JGhAOyRogDskaMA7JGkAOyRpQDskaYA7JGnAOyRqADskakA7JGqAOyRqwDskawA7JGtAOyRrgDska8A7JGwAOyRsQDskbIA7JGzAOyRtADskbUA7JG2AOyRtwDskbgA7JG5AOyRugDskbsA7JG8AOyRvQDskb4A7JG/AOySgADskoEA7JKCAOySgwDskoQA7JKFAOyShgDskocA7JKIAOySiQDskooA7JKLAOySjADsko0A7JKOAOySjwDskpAA7JKRAOySkgDskpMA7JKUAOySlQDskpYA7JKXAOySmADskpkA7JKaAOySmwDskpwA7JKdAOySngDskp8A7JKgAOySoQDskqIA7JKjAOySpADskqUA7JKmAOySpwDskqgA7JKpAOySqgDskqsA7JKsAOySrQDskq4A7JKvAOySsADskrEA7JKyAOySswDskrQA7JK1AOyStgDskrcA7JK4AOySuQDskroA7JK7AOySvADskr0A7JK+AOySvwDsk4AA7JOBAOyTggDsk4MA7JOEAOyThQDsk4YA7JOHAOyTiADsk4kA7JOKAOyTiwDsk4wA7JONAOyTjgDsk48A7JOQAOyTkQDsk5IA7JOTAOyTlADsk5UA7JOWAOyTlwDsk5gA7JOZAOyTmgDsk5sA7JOcAOyTnQDsk54A7JOfAOyToADsk6EA7JOiAOyTowDsk6QA7JOlAOyTpgDsk6cA7JOoAOyTqQDsk6oA7JOrAOyTrADsk60A7JOuAOyTrwDsk7AA7JOxAOyTsgDsk7MA7JO0AOyTtQDsk7YA7JO3AOyTuADsk7kA7JO6AOyTuwDsk7wA7JO9AOyTvgDsk78A7JSAAOyUgQDslIIA7JSDAOyUhADslIUA7JSGAOyUhwDslIgA7JSJAOyUigDslIsA7JSMAOyUjQDslI4A7JSPAOyUkADslJEA7JSSAOyUkwDslJQA7JSVAOyUlgDslJcA7JSYAOyUmQDslJoA7JSbAOyUnADslJ0A7JSeAOyUnwDslKAA7JShAOyUogDslKMA7JSkAOyUpQDslKYA7JSnAOyUqADslKkA7JSqAOyUqwDslKwA7JStAOyUrgDslK8A7JSwAOyUsQDslLIA7JSzAOyUtADslLUA7JS2AOyUtwDslLgA7JS5AOyUugDslLsA7JS8AOyUvQDslL4A7JS/AOyVgADslYEA7JWCAOyVgwDslYQA7JWFAOyVhgDslYcA7JWIAOyViQDslYoA7JWLAOyVjADslY0A7JWOAOyVjwDslZAA7JWRAOyVkgDslZMA7JWUAOyVlQDslZYA7JWXAOyVmADslZkA7JWaAOyVmwDslZwA7JWdAOyVngDslZ8A7JWgAOyVoQDslaIA7JWjAOyVpADslaUA7JWmAOyVpwDslagA7JWpAOyVqgDslasA7JWsAOyVrQDsla4A7JWvAOyVsADslbEA7JWyAOyVswDslbQA7JW1AOyVtgDslbcA7JW4AOyVuQDslboA7JW7AOyVvADslb0A7JW+AOyVvwDsloAA7JaBAOyWggDsloMA7JaEAOyWhQDsloYA7JaHAOyWiADslokA7JaKAOyWiwDslowA7JaNAOyWjgDslo8A7JaQAOyWkQDslpIA7JaTAOyWlADslpUA7JaWAOyWlwDslpgA7JaZAOyWmgDslpsA7JacAOyWnQDslp4A7JafAOyWoADslqEA7JaiAOyWowDslqQA7JalAOyWpgDslqcA7JaoAOyWqQDslqoA7JarAOyWrADslq0A7JauAOyWrwDslrAA7JaxAOyWsgDslrMA7Ja0AOyWtQDslrYA7Ja3AOyWuADslrkA7Ja6AOyWuwDslrwA7Ja9AOyWvgDslr8A7JeAAOyXgQDsl4IA7JeDAOyXhADsl4UA7JeGAOyXhwDsl4gA7JeJAOyXigDsl4sA7JeMAOyXjQDsl44A7JePAOyXkADsl5EA7JeSAOyXkwDsl5QA7JeVAOyXlgDsl5cA7JeYAOyXmQDsl5oA7JebAOyXnADsl50A7JeeAOyXnwDsl6AA7JehAOyXogDsl6MA7JekAOyXpQDsl6YA7JenAOyXqADsl6kA7JeqAOyXqwDsl6wA7JetAOyXrgDsl68A7JewAOyXsQDsl7IA7JezAOyXtADsl7UA7Je2AOyXtwDsl7gA7Je5AOyXugDsl7sA7Je8AOyXvQDsl74A7Je/AOyYgADsmIEA7JiCAOyYgwDsmIQA7JiFAOyYhgDsmIcA7JiIAOyYiQDsmIoA7JiLAOyYjADsmI0A7JiOAOyYjwDsmJAA7JiRAOyYkgDsmJMA7JiUAOyYlQDsmJYA7JiXAOyYmADsmJkA7JiaAOyYmwDsmJwA7JidAOyYngDsmJ8A7JigAOyYoQDsmKIA7JijAOyYpADsmKUA7JimAOyYpwDsmKgA7JipAOyYqgDsmKsA7JisAOyYrQDsmK4A7JivAOyYsADsmLEA7JiyAOyYswDsmLQA7Ji1AOyYtgDsmLcA7Ji4AOyYuQDsmLoA7Ji7AOyYvADsmL0A7Ji+AOyYvwDsmYAA7JmBAOyZggDsmYMA7JmEAOyZhQDsmYYA7JmHAOyZiADsmYkA7JmKAOyZiwDsmYwA7JmNAOyZjgDsmY8A7JmQAOyZkQDsmZIA7JmTAOyZlADsmZUA7JmWAOyZlwDsmZgA7JmZAOyZmgDsmZsA7JmcAOyZnQDsmZ4A7JmfAOyZoADsmaEA7JmiAOyZowDsmaQA7JmlAOyZpgDsmacA7JmoAOyZqQDsmaoA7JmrAOyZrADsma0A7JmuAOyZrwDsmbAA7JmxAOyZsgDsmbMA7Jm0AOyZtQDsmbYA7Jm3AOyZuADsmbkA7Jm6AOyZuwDsmbwA7Jm9AOyZvgDsmb8A7JqAAOyagQDsmoIA7JqDAOyahADsmoUA7JqGAOyahwDsmogA7JqJAOyaigDsmosA7JqMAOyajQDsmo4A7JqPAOyakADsmpEA7JqSAOyakwDsmpQA7JqVAOyalgDsmpcA7JqYAOyamQDsmpoA7JqbAOyanADsmp0A7JqeAOyanwDsmqAA7JqhAOyaogDsmqMA7JqkAOyapQDsmqYA7JqnAOyaqADsmqkA7JqqAOyaqwDsmqwA7JqtAOyargDsmq8A7JqwAOyasQDsmrIA7JqzAOyatADsmrUA7Jq2AOyatwDsmrgA7Jq5AOyaugDsmrsA7Jq8AOyavQDsmr4A7Jq/AOybgADsm4EA7JuCAOybgwDsm4QA7JuFAOybhgDsm4cA7JuIAOybiQDsm4oA7JuLAOybjADsm40A7JuOAOybjwDsm5AA7JuRAOybkgDsm5MA7JuUAOyblQDsm5YA7JuXAOybmADsm5kA7JuaAOybmwDsm5wA7JudAOybngDsm58A7JugAOyboQDsm6IA7JujAOybpADsm6UA7JumAOybpwDsm6gA7JupAOybqgDsm6sA7JusAOybrQDsm64A7JuvAOybsADsm7EA7JuyAOybswDsm7QA7Ju1AOybtgDsm7cA7Ju4AOybuQDsm7oA7Ju7AOybvADsm70A7Ju+AOybvwDsnIAA7JyBAOycggDsnIMA7JyEAOychQDsnIYA7JyHAOyciADsnIkA7JyKAOyciwDsnIwA7JyNAOycjgDsnI8A7JyQAOyckQDsnJIA7JyTAOyclADsnJUA7JyWAOyclwDsnJgA7JyZAOycmgDsnJsA7JycAOycnQDsnJ4A7JyfAOycoADsnKEA7JyiAOycowDsnKQA7JylAOycpgDsnKcA7JyoAOycqQDsnKoA7JyrAOycrADsnK0A7JyuAOycrwDsnLAA7JyxAOycsgDsnLMA7Jy0AOyctQDsnLYA7Jy3AOycuADsnLkA7Jy6AOycuwDsnLwA7Jy9AOycvgDsnL8A7J2AAOydgQDsnYIA7J2DAOydhADsnYUA7J2GAOydhwDsnYgA7J2JAOydigDsnYsA7J2MAOydjQDsnY4A7J2PAOydkADsnZEA7J2SAOydkwDsnZQA7J2VAOydlgDsnZcA7J2YAOydmQDsnZoA7J2bAOydnADsnZ0A7J2eAOydnwDsnaAA7J2hAOydogDsnaMA7J2kAOydpQDsnaYA7J2nAOydqADsnakA7J2qAOydqwDsnawA7J2tAOydrgDsna8A7J2wAOydsQDsnbIA7J2zAOydtADsnbUA7J22AOydtwDsnbgA7J25AOydugDsnbsA7J28AOydvQDsnb4A7J2/AOyegADsnoEA7J6CAOyegwDsnoQA7J6FAOyehgDsnocA7J6IAOyeiQDsnooA7J6LAOyejADsno0A7J6OAOyejwDsnpAA7J6RAOyekgDsnpMA7J6UAOyelQDsnpYA7J6XAOyemADsnpkA7J6aAOyemwDsnpwA7J6dAOyengDsnp8A7J6gAOyeoQDsnqIA7J6jAOyepADsnqUA7J6mAOyepwDsnqgA7J6pAOyeqgDsnqsA7J6sAOyerQDsnq4A7J6vAOyesADsnrEA7J6yAOyeswDsnrQA7J61AOyetgDsnrcA7J64AOyeuQDsnroA7J67AOyevADsnr0A7J6+AOyevwDsn4AA7J+BAOyfggDsn4MA7J+EAOyfhQDsn4YA7J+HAOyfiADsn4kA7J+KAOyfiwDsn4wA7J+NAOyfjgDsn48A7J+QAOyfkQDsn5IA7J+TAOyflADsn5UA7J+WAOyflwDsn5gA7J+ZAOyfmgDsn5sA7J+cAOyfnQDsn54A7J+fAOyfoADsn6EA7J+iAOyfowDsn6QA7J+lAOyfpgDsn6cA7J+oAOyfqQDsn6oA7J+rAOyfrADsn60A7J+uAOyfrwDsn7AA7J+xAOyfsgDsn7MA7J+0AOyftQDsn7YA7J+3AOyfuADsn7kA7J+6AOyfuwDsn7wA7J+9AOyfvgDsn78A7KCAAOyggQDsoIIA7KCDAOyghADsoIUA7KCGAOyghwDsoIgA7KCJAOygigDsoIsA7KCMAOygjQDsoI4A7KCPAOygkADsoJEA7KCSAOygkwDsoJQA7KCVAOyglgDsoJcA7KCYAOygmQDsoJoA7KCbAOygnADsoJ0A7KCeAOygnwDsoKAA7KChAOygogDsoKMA7KCkAOygpQDsoKYA7KCnAOygqADsoKkA7KCqAOygqwDsoKwA7KCtAOygrgDsoK8A7KCwAOygsQDsoLIA7KCzAOygtADsoLUA7KC2AOygtwDsoLgA7KC5AOygugDsoLsA7KC8AOygvQDsoL4A7KC/AOyhgADsoYEA7KGCAOyhgwDsoYQA7KGFAOyhhgDsoYcA7KGIAOyhiQDsoYoA7KGLAOyhjADsoY0A7KGOAOyhjwDsoZAA7KGRAOyhkgDsoZMA7KGUAOyhlQDsoZYA7KGXAOyhmADsoZkA7KGaAOyhmwDsoZwA7KGdAOyhngDsoZ8A7KGgAOyhoQDsoaIA7KGjAOyhpADsoaUA7KGmAOyhpwDsoagA7KGpAOyhqgDsoasA7KGsAOyhrQDsoa4A7KGvAOyhsADsobEA7KGyAOyhswDsobQA7KG1AOyhtgDsobcA7KG4AOyhuQDsoboA7KG7AOyhvADsob0A7KG+AOyhvwDsooAA7KKBAOyiggDsooMA7KKEAOyihQDsooYA7KKHAOyiiADsookA7KKKAOyiiwDsoowA7KKNAOyijgDsoo8A7KKQAOyikQDsopIA7KKTAOyilADsopUA7KKWAOyilwDsopgA7KKZAOyimgDsopsA7KKcAOyinQDsop4A7KKfAOyioADsoqEA7KKiAOyiowDsoqQA7KKlAOyipgDsoqcA7KKoAOyiqQDsoqoA7KKrAOyirADsoq0A7KKuAOyirwDsorAA7KKxAOyisgDsorMA7KK0AOyitQDsorYA7KK3AOyiuADsorkA7KK6AOyiuwDsorwA7KK9AOyivgDsor8A7KOAAOyjgQDso4IA7KODAOyjhADso4UA7KOGAOyjhwDso4gA7KOJAOyjigDso4sA7KOMAOyjjQDso44A7KOPAOyjkADso5EA7KOSAOyjkwDso5QA7KOVAOyjlgDso5cA7KOYAOyjmQDso5oA7KObAOyjnADso50A7KOeAOyjnwDso6AA7KOhAOyjogDso6MA7KOkAOyjpQDso6YA7KOnAOyjqADso6kA7KOqAOyjqwDso6wA7KOtAOyjrgDso68A7KOwAOyjsQDso7IA7KOzAOyjtADso7UA7KO2AOyjtwDso7gA7KO5AOyjugDso7sA7KO8AOyjvOydmADso70A7KO+AOyjvwDspIAA7KSBAOykggDspIMA7KSEAOykhQDspIYA7KSHAOykiADspIkA7KSKAOykiwDspIwA7KSNAOykjgDspI8A7KSQAOykkQDspJIA7KSTAOyklADspJUA7KSWAOyklwDspJgA7KSZAOykmgDspJsA7KScAOyknQDspJ4A7KSfAOykoADspKEA7KSiAOykowDspKQA7KSlAOykpgDspKcA7KSoAOykqQDspKoA7KSrAOykrADspK0A7KSuAOykrwDspLAA7KSxAOyksgDspLMA7KS0AOyktQDspLYA7KS3AOykuADspLkA7KS6AOykuwDspLwA7KS9AOykvgDspL8A7KWAAOylgQDspYIA7KWDAOylhADspYUA7KWGAOylhwDspYgA7KWJAOyligDspYsA7KWMAOyljQDspY4A7KWPAOylkADspZEA7KWSAOylkwDspZQA7KWVAOyllgDspZcA7KWYAOylmQDspZoA7KWbAOylnADspZ0A7KWeAOylnwDspaAA7KWhAOylogDspaMA7KWkAOylpQDspaYA7KWnAOylqADspakA7KWqAOylqwDspawA7KWtAOylrgDspa8A7KWwAOylsQDspbIA7KWzAOyltADspbUA7KW2AOyltwDspbgA7KW5AOylugDspbsA7KW8AOylvQDspb4A7KW/AOymgADspoEA7KaCAOymgwDspoQA7KaFAOymhgDspocA7KaIAOymiQDspooA7KaLAOymjADspo0A7KaOAOymjwDsppAA7KaRAOymkgDsppMA7KaUAOymlQDsppYA7KaXAOymmADsppkA7KaaAOymmwDsppwA7KadAOymngDspp8A7KagAOymoQDspqIA7KajAOympADspqUA7KamAOympwDspqgA7KapAOymqgDspqsA7KasAOymrQDspq4A7KavAOymsADsprEA7KayAOymswDsprQA7Ka1AOymtgDsprcA7Ka4AOymuQDsproA7Ka7AOymvADspr0A7Ka+AOymvwDsp4AA7KeBAOynggDsp4MA7KeEAOynhQDsp4YA7KeHAOyniADsp4kA7KeKAOyniwDsp4wA7KeNAOynjgDsp48A7KeQAOynkQDsp5IA7KeTAOynlADsp5UA7KeWAOynlwDsp5gA7KeZAOynmgDsp5sA7KecAOynnQDsp54A7KefAOynoADsp6EA7KeiAOynowDsp6QA7KelAOynpgDsp6cA7KeoAOynqQDsp6oA7KerAOynrADsp60A7KeuAOynrwDsp7AA7KexAOynsgDsp7MA7Ke0AOyntQDsp7YA7Ke3AOynuADsp7kA7Ke6AOynuwDsp7wA7Ke9AOynvgDsp78A7KiAAOyogQDsqIIA7KiDAOyohADsqIUA7KiGAOyohwDsqIgA7KiJAOyoigDsqIsA7KiMAOyojQDsqI4A7KiPAOyokADsqJEA7KiSAOyokwDsqJQA7KiVAOyolgDsqJcA7KiYAOyomQDsqJoA7KibAOyonADsqJ0A7KieAOyonwDsqKAA7KihAOyoogDsqKMA7KikAOyopQDsqKYA7KinAOyoqADsqKkA7KiqAOyoqwDsqKwA7KitAOyorgDsqK8A7KiwAOyosQDsqLIA7KizAOyotADsqLUA7Ki2AOyotwDsqLgA7Ki5AOyougDsqLsA7Ki8AOyovQDsqL4A7Ki/AOypgADsqYEA7KmCAOypgwDsqYQA7KmFAOyphgDsqYcA7KmIAOypiQDsqYoA7KmLAOypjADsqY0A7KmOAOypjwDsqZAA7KmRAOypkgDsqZMA7KmUAOyplQDsqZYA7KmXAOypmADsqZkA7KmaAOypmwDsqZwA7KmdAOypngDsqZ8A7KmgAOypoQDsqaIA7KmjAOyppADsqaUA7KmmAOyppwDsqagA7KmpAOypqgDsqasA7KmsAOyprQDsqa4A7KmvAOypsADsqbEA7KmyAOypswDsqbQA7Km1AOyptgDsqbcA7Km4AOypuQDsqboA7Km7AOypvADsqb0A7Km+AOypvwDsqoAA7KqBAOyqggDsqoMA7KqEAOyqhQDsqoYA7KqHAOyqiADsqokA7KqKAOyqiwDsqowA7KqNAOyqjgDsqo8A7KqQAOyqkQDsqpIA7KqTAOyqlADsqpUA7KqWAOyqlwDsqpgA7KqZAOyqmgDsqpsA7KqcAOyqnQDsqp4A7KqfAOyqoADsqqEA7KqiAOyqowDsqqQA7KqlAOyqpgDsqqcA7KqoAOyqqQDsqqoA7KqrAOyqrADsqq0A7KquAOyqrwDsqrAA7KqxAOyqsgDsqrMA7Kq0AOyqtQDsqrYA7Kq3AOyquADsqrkA7Kq6AOyquwDsqrwA7Kq9AOyqvgDsqr8A7KuAAOyrgQDsq4IA7KuDAOyrhADsq4UA7KuGAOyrhwDsq4gA7KuJAOyrigDsq4sA7KuMAOyrjQDsq44A7KuPAOyrkADsq5EA7KuSAOyrkwDsq5QA7KuVAOyrlgDsq5cA7KuYAOyrmQDsq5oA7KubAOyrnADsq50A7KueAOyrnwDsq6AA7KuhAOyrogDsq6MA7KukAOyrpQDsq6YA7KunAOyrqADsq6kA7KuqAOyrqwDsq6wA7KutAOyrrgDsq68A7KuwAOyrsQDsq7IA7KuzAOyrtADsq7UA7Ku2AOyrtwDsq7gA7Ku5AOyrugDsq7sA7Ku8AOyrvQDsq74A7Ku/AOysgADsrIEA7KyCAOysgwDsrIQA7KyFAOyshgDsrIcA7KyIAOysiQDsrIoA7KyLAOysjADsrI0A7KyOAOysjwDsrJAA7KyRAOyskgDsrJMA7KyUAOyslQDsrJYA7KyXAOysmADsrJkA7KyaAOysmwDsrJwA7KydAOysngDsrJ8A7KygAOysoQDsrKIA7KyjAOyspADsrKUA7KymAOyspwDsrKgA7KypAOysqgDsrKsA7KysAOysrQDsrK4A7KyvAOyssADsrLEA7KyyAOysswDsrLQA7Ky1AOystgDsrLcA7Ky4AOysuQDsrLoA7Ky7AOysvADsrL0A7Ky+AOysvwDsrYAA7K2BAOytggDsrYMA7K2EAOythQDsrYYA7K2HAOytiADsrYkA7K2KAOytiwDsrYwA7K2NAOytjgDsrY8A7K2QAOytkQDsrZIA7K2TAOytlADsrZUA7K2WAOytlwDsrZgA7K2ZAOytmgDsrZsA7K2cAOytnQDsrZ4A7K2fAOytoADsraEA7K2iAOytowDsraQA7K2lAOytpgDsracA7K2oAOytqQDsraoA7K2rAOytrADsra0A7K2uAOytrwDsrbAA7K2xAOytsgDsrbMA7K20AOyttQDsrbYA7K23AOytuADsrbkA7K26AOytuwDsrbwA7K29AOytvgDsrb8A7K6AAOyugQDsroIA7K6DAOyuhADsroUA7K6GAOyuhwDsrogA7K6JAOyuigDsrosA7K6MAOyujQDsro4A7K6PAOyukADsrpEA7K6SAOyukwDsrpQA7K6VAOyulgDsrpcA7K6YAOyumQDsrpoA7K6bAOyunADsrp0A7K6eAOyunwDsrqAA7K6hAOyuogDsrqMA7K6kAOyupQDsrqYA7K6nAOyuqADsrqkA7K6qAOyuqwDsrqwA7K6tAOyurgDsrq8A7K6wAOyusQDsrrIA7K6zAOyutADsrrUA7K62AOyutwDsrrgA7K65AOyuugDsrrsA7K68AOyuvQDsrr4A7K6/AOyvgADsr4EA7K+CAOyvgwDsr4QA7K+FAOyvhgDsr4cA7K+IAOyviQDsr4oA7K+LAOyvjADsr40A7K+OAOyvjwDsr5AA7K+RAOyvkgDsr5MA7K+UAOyvlQDsr5YA7K+XAOyvmADsr5kA7K+aAOyvmwDsr5wA7K+dAOyvngDsr58A7K+gAOyvoQDsr6IA7K+jAOyvpADsr6UA7K+mAOyvpwDsr6gA7K+pAOyvqgDsr6sA7K+sAOyvrQDsr64A7K+vAOyvsADsr7EA7K+yAOyvswDsr7QA7K+1AOyvtgDsr7cA7K+4AOyvuQDsr7oA7K+7AOyvvADsr70A7K++AOyvvwDssIAA7LCBAOywggDssIMA7LCEAOywhQDssIYA7LCHAOywiADssIkA7LCKAOywiwDssIwA7LCNAOywjgDssI8A7LCQAOywkQDssJIA7LCTAOywlADssJUA7LCWAOywlwDssJgA7LCZAOywmgDssJsA7LCcAOywnQDssJ4A7LCfAOywoADssKEA7LCiAOywowDssKQA7LClAOywpgDssKcA7LCoAOywqQDssKoA7LCrAOywrADssK0A7LCuAOywrwDssLAA7LCxAOywsgDssLMA7LC0AOywtQDssLYA7LC3AOywuADssLjqs6AA7LC5AOywugDssLsA7LC8AOywvQDssL4A7LC/AOyxgADssYEA7LGCAOyxgwDssYQA7LGFAOyxhgDssYcA7LGIAOyxiQDssYoA7LGLAOyxjADssY0A7LGOAOyxjwDssZAA7LGRAOyxkgDssZMA7LGUAOyxlQDssZYA7LGXAOyxmADssZkA7LGaAOyxmwDssZwA7LGdAOyxngDssZ8A7LGgAOyxoQDssaIA7LGjAOyxpADssaUA7LGmAOyxpwDssagA7LGpAOyxqgDssasA7LGsAOyxrQDssa4A7LGvAOyxsADssbEA7LGyAOyxswDssbQA7LG1AOyxtgDssbcA7LG4AOyxuQDssboA7LG7AOyxvADssb0A7LG+AOyxvwDssoAA7LKBAOyyggDssoMA7LKEAOyyhQDssoYA7LKHAOyyiADssokA7LKKAOyyiwDssowA7LKNAOyyjgDsso8A7LKQAOyykQDsspIA7LKTAOyylADsspUA7LKWAOyylwDsspgA7LKZAOyymgDsspsA7LKcAOyynQDssp4A7LKfAOyyoADssqEA7LKiAOyyowDssqQA7LKlAOyypgDssqcA7LKoAOyyqQDssqoA7LKrAOyyrADssq0A7LKuAOyyrwDssrAA7LKxAOyysgDssrMA7LK0AOyytQDssrYA7LK3AOyyuADssrkA7LK6AOyyuwDssrwA7LK9AOyyvgDssr8A7LOAAOyzgQDss4IA7LODAOyzhADss4UA7LOGAOyzhwDss4gA7LOJAOyzigDss4sA7LOMAOyzjQDss44A7LOPAOyzkADss5EA7LOSAOyzkwDss5QA7LOVAOyzlgDss5cA7LOYAOyzmQDss5oA7LObAOyznADss50A7LOeAOyznwDss6AA7LOhAOyzogDss6MA7LOkAOyzpQDss6YA7LOnAOyzqADss6kA7LOqAOyzqwDss6wA7LOtAOyzrgDss68A7LOwAOyzsQDss7IA7LOzAOyztADss7UA7LO2AOyztwDss7gA7LO5AOyzugDss7sA7LO8AOyzvQDss74A7LO/AOy0gADstIEA7LSCAOy0gwDstIQA7LSFAOy0hgDstIcA7LSIAOy0iQDstIoA7LSLAOy0jADstI0A7LSOAOy0jwDstJAA7LSRAOy0kgDstJMA7LSUAOy0lQDstJYA7LSXAOy0mADstJkA7LSaAOy0mwDstJwA7LSdAOy0ngDstJ8A7LSgAOy0oQDstKIA7LSjAOy0pADstKUA7LSmAOy0pwDstKgA7LSpAOy0qgDstKsA7LSsAOy0rQDstK4A7LSvAOy0sADstLEA7LSyAOy0swDstLQA7LS1AOy0tgDstLcA7LS4AOy0uQDstLoA7LS7AOy0vADstL0A7LS+AOy0vwDstYAA7LWBAOy1ggDstYMA7LWEAOy1hQDstYYA7LWHAOy1iADstYkA7LWKAOy1iwDstYwA7LWNAOy1jgDstY8A7LWQAOy1kQDstZIA7LWTAOy1lADstZUA7LWWAOy1lwDstZgA7LWZAOy1mgDstZsA7LWcAOy1nQDstZ4A7LWfAOy1oADstaEA7LWiAOy1owDstaQA7LWlAOy1pgDstacA7LWoAOy1qQDstaoA7LWrAOy1rADsta0A7LWuAOy1rwDstbAA7LWxAOy1sgDstbMA7LW0AOy1tQDstbYA7LW3AOy1uADstbkA7LW6AOy1uwDstbwA7LW9AOy1vgDstb8A7LaAAOy2gQDstoIA7LaDAOy2hADstoUA7LaGAOy2hwDstogA7LaJAOy2igDstosA7LaMAOy2jQDsto4A7LaPAOy2kADstpEA7LaSAOy2kwDstpQA7LaVAOy2lgDstpcA7LaYAOy2mQDstpoA7LabAOy2nADstp0A7LaeAOy2nwDstqAA7LahAOy2ogDstqMA7LakAOy2pQDstqYA7LanAOy2qADstqkA7LaqAOy2qwDstqwA7LatAOy2rgDstq8A7LawAOy2sQDstrIA7LazAOy2tADstrUA7La2AOy2twDstrgA7La5AOy2ugDstrsA7La8AOy2vQDstr4A7La/AOy3gADst4EA7LeCAOy3gwDst4QA7LeFAOy3hgDst4cA7LeIAOy3iQDst4oA7LeLAOy3jADst40A7LeOAOy3jwDst5AA7LeRAOy3kgDst5MA7LeUAOy3lQDst5YA7LeXAOy3mADst5kA7LeaAOy3mwDst5wA7LedAOy3ngDst58A7LegAOy3oQDst6IA7LejAOy3pADst6UA7LemAOy3pwDst6gA7LepAOy3qgDst6sA7LesAOy3rQDst64A7LevAOy3sADst7EA7LeyAOy3swDst7QA7Le1AOy3tgDst7cA7Le4AOy3uQDst7oA7Le7AOy3vADst70A7Le+AOy3vwDsuIAA7LiBAOy4ggDsuIMA7LiEAOy4hQDsuIYA7LiHAOy4iADsuIkA7LiKAOy4iwDsuIwA7LiNAOy4jgDsuI8A7LiQAOy4kQDsuJIA7LiTAOy4lADsuJUA7LiWAOy4lwDsuJgA7LiZAOy4mgDsuJsA7LicAOy4nQDsuJ4A7LifAOy4oADsuKEA7LiiAOy4owDsuKQA7LilAOy4pgDsuKcA7LioAOy4qQDsuKoA7LirAOy4rADsuK0A7LiuAOy4rwDsuLAA7LixAOy4sgDsuLMA7Li0AOy4tQDsuLYA7Li3AOy4uADsuLkA7Li6AOy4uwDsuLwA7Li9AOy4vgDsuL8A7LmAAOy5gQDsuYIA7LmDAOy5hADsuYUA7LmGAOy5hwDsuYgA7LmJAOy5igDsuYsA7LmMAOy5jQDsuY4A7LmPAOy5kADsuZEA7LmSAOy5kwDsuZQA7LmVAOy5lgDsuZcA7LmYAOy5mQDsuZoA7LmbAOy5nADsuZ0A7LmeAOy5nwDsuaAA7LmhAOy5ogDsuaMA7LmkAOy5pQDsuaYA7LmnAOy5qADsuakA7LmqAOy5qwDsuawA7LmtAOy5rgDsua8A7LmwAOy5sQDsubIA7LmzAOy5tADsubUA7Lm2AOy5twDsubgA7Lm5AOy5ugDsubsA7Lm8AOy5vQDsub4A7Lm/AOy6gADsuoEA7LqCAOy6gwDsuoQA7LqFAOy6hgDsuocA7LqIAOy6iQDsuooA7LqLAOy6jADsuo0A7LqOAOy6jwDsupAA7LqRAOy6kgDsupMA7LqUAOy6lQDsupYA7LqXAOy6mADsupkA7LqaAOy6mwDsupwA7LqdAOy6ngDsup8A7LqgAOy6oQDsuqIA7LqjAOy6pADsuqUA7LqmAOy6pwDsuqgA7LqpAOy6qgDsuqsA7LqsAOy6rQDsuq4A7LqvAOy6sADsurEA7LqyAOy6swDsurQA7Lq1AOy6tgDsurcA7Lq4AOy6uQDsuroA7Lq7AOy6vADsur0A7Lq+AOy6vwDsu4AA7LuBAOy7ggDsu4MA7LuEAOy7hQDsu4YA7LuHAOy7iADsu4kA7LuKAOy7iwDsu4wA7LuNAOy7jgDsu48A7LuQAOy7kQDsu5IA7LuTAOy7lADsu5UA7LuWAOy7lwDsu5gA7LuZAOy7mgDsu5sA7LucAOy7nQDsu54A7LufAOy7oADsu6EA7LuiAOy7owDsu6QA7LulAOy7pgDsu6cA7LuoAOy7qQDsu6oA7LurAOy7rADsu60A7LuuAOy7rwDsu7AA7LuxAOy7sgDsu7MA7Lu0AOy7tQDsu7YA7Lu3AOy7uADsu7kA7Lu6AOy7uwDsu7wA7Lu9AOy7vgDsu78A7LyAAOy8gQDsvIIA7LyDAOy8hADsvIUA7LyGAOy8hwDsvIgA7LyJAOy8igDsvIsA7LyMAOy8jQDsvI4A7LyPAOy8kADsvJEA7LySAOy8kwDsvJQA7LyVAOy8lgDsvJcA7LyYAOy8mQDsvJoA7LybAOy8nADsvJ0A7LyeAOy8nwDsvKAA7LyhAOy8ogDsvKMA7LykAOy8pQDsvKYA7LynAOy8qADsvKkA7LyqAOy8qwDsvKwA7LytAOy8rgDsvK8A7LywAOy8sQDsvLIA7LyzAOy8tADsvLUA7Ly2AOy8twDsvLgA7Ly5AOy8ugDsvLsA7Ly8AOy8vQDsvL4A7Ly/AOy9gADsvYEA7L2CAOy9gwDsvYQA7L2FAOy9hgDsvYcA7L2IAOy9iQDsvYoA7L2LAOy9jADsvY0A7L2OAOy9jwDsvZAA7L2RAOy9kgDsvZMA7L2UAOy9lQDsvZYA7L2XAOy9mADsvZkA7L2aAOy9mwDsvZwA7L2dAOy9ngDsvZ8A7L2gAOy9oQDsvaIA7L2jAOy9pADsvaUA7L2mAOy9pwDsvagA7L2pAOy9qgDsvasA7L2sAOy9rQDsva4A7L2vAOy9sADsvbEA7L2yAOy9swDsvbQA7L21AOy9tgDsvbcA7L24AOy9uQDsvboA7L27AOy9vADsvb0A7L2+AOy9vwDsvoAA7L6BAOy+ggDsvoMA7L6EAOy+hQDsvoYA7L6HAOy+iADsvokA7L6KAOy+iwDsvowA7L6NAOy+jgDsvo8A7L6QAOy+kQDsvpIA7L6TAOy+lADsvpUA7L6WAOy+lwDsvpgA7L6ZAOy+mgDsvpsA7L6cAOy+nQDsvp4A7L6fAOy+oADsvqEA7L6iAOy+owDsvqQA7L6lAOy+pgDsvqcA7L6oAOy+qQDsvqoA7L6rAOy+rADsvq0A7L6uAOy+rwDsvrAA7L6xAOy+sgDsvrMA7L60AOy+tQDsvrYA7L63AOy+uADsvrkA7L66AOy+uwDsvrwA7L69AOy+vgDsvr8A7L+AAOy/gQDsv4IA7L+DAOy/hADsv4UA7L+GAOy/hwDsv4gA7L+JAOy/igDsv4sA7L+MAOy/jQDsv44A7L+PAOy/kADsv5EA7L+SAOy/kwDsv5QA7L+VAOy/lgDsv5cA7L+YAOy/mQDsv5oA7L+bAOy/nADsv50A7L+eAOy/nwDsv6AA7L+hAOy/ogDsv6MA7L+kAOy/pQDsv6YA7L+nAOy/qADsv6kA7L+qAOy/qwDsv6wA7L+tAOy/rgDsv68A7L+wAOy/sQDsv7IA7L+zAOy/tADsv7UA7L+2AOy/twDsv7gA7L+5AOy/ugDsv7sA7L+8AOy/vQDsv74A7L+/AO2AgADtgIEA7YCCAO2AgwDtgIQA7YCFAO2AhgDtgIcA7YCIAO2AiQDtgIoA7YCLAO2AjADtgI0A7YCOAO2AjwDtgJAA7YCRAO2AkgDtgJMA7YCUAO2AlQDtgJYA7YCXAO2AmADtgJkA7YCaAO2AmwDtgJwA7YCdAO2AngDtgJ8A7YCgAO2AoQDtgKIA7YCjAO2ApADtgKUA7YCmAO2ApwDtgKgA7YCpAO2AqgDtgKsA7YCsAO2ArQDtgK4A7YCvAO2AsADtgLEA7YCyAO2AswDtgLQA7YC1AO2AtgDtgLcA7YC4AO2AuQDtgLoA7YC7AO2AvADtgL0A7YC+AO2AvwDtgYAA7YGBAO2BggDtgYMA7YGEAO2BhQDtgYYA7YGHAO2BiADtgYkA7YGKAO2BiwDtgYwA7YGNAO2BjgDtgY8A7YGQAO2BkQDtgZIA7YGTAO2BlADtgZUA7YGWAO2BlwDtgZgA7YGZAO2BmgDtgZsA7YGcAO2BnQDtgZ4A7YGfAO2BoADtgaEA7YGiAO2BowDtgaQA7YGlAO2BpgDtgacA7YGoAO2BqQDtgaoA7YGrAO2BrADtga0A7YGuAO2BrwDtgbAA7YGxAO2BsgDtgbMA7YG0AO2BtQDtgbYA7YG3AO2BuADtgbkA7YG6AO2BuwDtgbwA7YG9AO2BvgDtgb8A7YKAAO2CgQDtgoIA7YKDAO2ChADtgoUA7YKGAO2ChwDtgogA7YKJAO2CigDtgosA7YKMAO2CjQDtgo4A7YKPAO2CkADtgpEA7YKSAO2CkwDtgpQA7YKVAO2ClgDtgpcA7YKYAO2CmQDtgpoA7YKbAO2CnADtgp0A7YKeAO2CnwDtgqAA7YKhAO2CogDtgqMA7YKkAO2CpQDtgqYA7YKnAO2CqADtgqkA7YKqAO2CqwDtgqwA7YKtAO2CrgDtgq8A7YKwAO2CsQDtgrIA7YKzAO2CtADtgrUA7YK2AO2CtwDtgrgA7YK5AO2CugDtgrsA7YK8AO2CvQDtgr4A7YK/AO2DgADtg4EA7YOCAO2DgwDtg4QA7YOFAO2DhgDtg4cA7YOIAO2DiQDtg4oA7YOLAO2DjADtg40A7YOOAO2DjwDtg5AA7YORAO2DkgDtg5MA7YOUAO2DlQDtg5YA7YOXAO2DmADtg5kA7YOaAO2DmwDtg5wA7YOdAO2DngDtg58A7YOgAO2DoQDtg6IA7YOjAO2DpADtg6UA7YOmAO2DpwDtg6gA7YOpAO2DqgDtg6sA7YOsAO2DrQDtg64A7YOvAO2DsADtg7EA7YOyAO2DswDtg7QA7YO1AO2DtgDtg7cA7YO4AO2DuQDtg7oA7YO7AO2DvADtg70A7YO+AO2DvwDthIAA7YSBAO2EggDthIMA7YSEAO2EhQDthIYA7YSHAO2EiADthIkA7YSKAO2EiwDthIwA7YSNAO2EjgDthI8A7YSQAO2EkQDthJIA7YSTAO2ElADthJUA7YSWAO2ElwDthJgA7YSZAO2EmgDthJsA7YScAO2EnQDthJ4A7YSfAO2EoADthKEA7YSiAO2EowDthKQA7YSlAO2EpgDthKcA7YSoAO2EqQDthKoA7YSrAO2ErADthK0A7YSuAO2ErwDthLAA7YSxAO2EsgDthLMA7YS0AO2EtQDthLYA7YS3AO2EuADthLkA7YS6AO2EuwDthLwA7YS9AO2EvgDthL8A7YWAAO2FgQDthYIA7YWDAO2FhADthYUA7YWGAO2FhwDthYgA7YWJAO2FigDthYsA7YWMAO2FjQDthY4A7YWPAO2FkADthZEA7YWSAO2FkwDthZQA7YWVAO2FlgDthZcA7YWYAO2FmQDthZoA7YWbAO2FnADthZ0A7YWeAO2FnwDthaAA7YWhAO2FogDthaMA7YWkAO2FpQDthaYA7YWnAO2FqADthakA7YWqAO2FqwDthawA7YWtAO2FrgDtha8A7YWwAO2FsQDthbIA7YWzAO2FtADthbUA7YW2AO2FtwDthbgA7YW5AO2FugDthbsA7YW8AO2FvQDthb4A7YW/AO2GgADthoEA7YaCAO2GgwDthoQA7YaFAO2GhgDthocA7YaIAO2GiQDthooA7YaLAO2GjADtho0A7YaOAO2GjwDthpAA7YaRAO2GkgDthpMA7YaUAO2GlQDthpYA7YaXAO2GmADthpkA7YaaAO2GmwDthpwA7YadAO2GngDthp8A7YagAO2GoQDthqIA7YajAO2GpADthqUA7YamAO2GpwDthqgA7YapAO2GqgDthqsA7YasAO2GrQDthq4A7YavAO2GsADthrEA7YayAO2GswDthrQA7Ya1AO2GtgDthrcA7Ya4AO2GuQDthroA7Ya7AO2GvADthr0A7Ya+AO2GvwDth4AA7YeBAO2HggDth4MA7YeEAO2HhQDth4YA7YeHAO2HiADth4kA7YeKAO2HiwDth4wA7YeNAO2HjgDth48A7YeQAO2HkQDth5IA7YeTAO2HlADth5UA7YeWAO2HlwDth5gA7YeZAO2HmgDth5sA7YecAO2HnQDth54A7YefAO2HoADth6EA7YeiAO2HowDth6QA7YelAO2HpgDth6cA7YeoAO2HqQDth6oA7YerAO2HrADth60A7YeuAO2HrwDth7AA7YexAO2HsgDth7MA7Ye0AO2HtQDth7YA7Ye3AO2HuADth7kA7Ye6AO2HuwDth7wA7Ye9AO2HvgDth78A7YiAAO2IgQDtiIIA7YiDAO2IhADtiIUA7YiGAO2IhwDtiIgA7YiJAO2IigDtiIsA7YiMAO2IjQDtiI4A7YiPAO2IkADtiJEA7YiSAO2IkwDtiJQA7YiVAO2IlgDtiJcA7YiYAO2ImQDtiJoA7YibAO2InADtiJ0A7YieAO2InwDtiKAA7YihAO2IogDtiKMA7YikAO2IpQDtiKYA7YinAO2IqADtiKkA7YiqAO2IqwDtiKwA7YitAO2IrgDtiK8A7YiwAO2IsQDtiLIA7YizAO2ItADtiLUA7Yi2AO2ItwDtiLgA7Yi5AO2IugDtiLsA7Yi8AO2IvQDtiL4A7Yi/AO2JgADtiYEA7YmCAO2JgwDtiYQA7YmFAO2JhgDtiYcA7YmIAO2JiQDtiYoA7YmLAO2JjADtiY0A7YmOAO2JjwDtiZAA7YmRAO2JkgDtiZMA7YmUAO2JlQDtiZYA7YmXAO2JmADtiZkA7YmaAO2JmwDtiZwA7YmdAO2JngDtiZ8A7YmgAO2JoQDtiaIA7YmjAO2JpADtiaUA7YmmAO2JpwDtiagA7YmpAO2JqgDtiasA7YmsAO2JrQDtia4A7YmvAO2JsADtibEA7YmyAO2JswDtibQA7Ym1AO2JtgDtibcA7Ym4AO2JuQDtiboA7Ym7AO2JvADtib0A7Ym+AO2JvwDtioAA7YqBAO2KggDtioMA7YqEAO2KhQDtioYA7YqHAO2KiADtiokA7YqKAO2KiwDtiowA7YqNAO2KjgDtio8A7YqQAO2KkQDtipIA7YqTAO2KlADtipUA7YqWAO2KlwDtipgA7YqZAO2KmgDtipsA7YqcAO2KnQDtip4A7YqfAO2KoADtiqEA7YqiAO2KowDtiqQA7YqlAO2KpgDtiqcA7YqoAO2KqQDtiqoA7YqrAO2KrADtiq0A7YquAO2KrwDtirAA7YqxAO2KsgDtirMA7Yq0AO2KtQDtirYA7Yq3AO2KuADtirkA7Yq6AO2KuwDtirwA7Yq9AO2KvgDtir8A7YuAAO2LgQDti4IA7YuDAO2LhADti4UA7YuGAO2LhwDti4gA7YuJAO2LigDti4sA7YuMAO2LjQDti44A7YuPAO2LkADti5EA7YuSAO2LkwDti5QA7YuVAO2LlgDti5cA7YuYAO2LmQDti5oA7YubAO2LnADti50A7YueAO2LnwDti6AA7YuhAO2LogDti6MA7YukAO2LpQDti6YA7YunAO2LqADti6kA7YuqAO2LqwDti6wA7YutAO2LrgDti68A7YuwAO2LsQDti7IA7YuzAO2LtADti7UA7Yu2AO2LtwDti7gA7Yu5AO2LugDti7sA7Yu8AO2LvQDti74A7Yu/AO2MgADtjIEA7YyCAO2MgwDtjIQA7YyFAO2MhgDtjIcA7YyIAO2MiQDtjIoA7YyLAO2MjADtjI0A7YyOAO2MjwDtjJAA7YyRAO2MkgDtjJMA7YyUAO2MlQDtjJYA7YyXAO2MmADtjJkA7YyaAO2MmwDtjJwA7YydAO2MngDtjJ8A7YygAO2MoQDtjKIA7YyjAO2MpADtjKUA7YymAO2MpwDtjKgA7YypAO2MqgDtjKsA7YysAO2MrQDtjK4A7YyvAO2MsADtjLEA7YyyAO2MswDtjLQA7Yy1AO2MtgDtjLcA7Yy4AO2MuQDtjLoA7Yy7AO2MvADtjL0A7Yy+AO2MvwDtjYAA7Y2BAO2NggDtjYMA7Y2EAO2NhQDtjYYA7Y2HAO2NiADtjYkA7Y2KAO2NiwDtjYwA7Y2NAO2NjgDtjY8A7Y2QAO2NkQDtjZIA7Y2TAO2NlADtjZUA7Y2WAO2NlwDtjZgA7Y2ZAO2NmgDtjZsA7Y2cAO2NnQDtjZ4A7Y2fAO2NoADtjaEA7Y2iAO2NowDtjaQA7Y2lAO2NpgDtjacA7Y2oAO2NqQDtjaoA7Y2rAO2NrADtja0A7Y2uAO2NrwDtjbAA7Y2xAO2NsgDtjbMA7Y20AO2NtQDtjbYA7Y23AO2NuADtjbkA7Y26AO2NuwDtjbwA7Y29AO2NvgDtjb8A7Y6AAO2OgQDtjoIA7Y6DAO2OhADtjoUA7Y6GAO2OhwDtjogA7Y6JAO2OigDtjosA7Y6MAO2OjQDtjo4A7Y6PAO2OkADtjpEA7Y6SAO2OkwDtjpQA7Y6VAO2OlgDtjpcA7Y6YAO2OmQDtjpoA7Y6bAO2OnADtjp0A7Y6eAO2OnwDtjqAA7Y6hAO2OogDtjqMA7Y6kAO2OpQDtjqYA7Y6nAO2OqADtjqkA7Y6qAO2OqwDtjqwA7Y6tAO2OrgDtjq8A7Y6wAO2OsQDtjrIA7Y6zAO2OtADtjrUA7Y62AO2OtwDtjrgA7Y65AO2OugDtjrsA7Y68AO2OvQDtjr4A7Y6/AO2PgADtj4EA7Y+CAO2PgwDtj4QA7Y+FAO2PhgDtj4cA7Y+IAO2PiQDtj4oA7Y+LAO2PjADtj40A7Y+OAO2PjwDtj5AA7Y+RAO2PkgDtj5MA7Y+UAO2PlQDtj5YA7Y+XAO2PmADtj5kA7Y+aAO2PmwDtj5wA7Y+dAO2PngDtj58A7Y+gAO2PoQDtj6IA7Y+jAO2PpADtj6UA7Y+mAO2PpwDtj6gA7Y+pAO2PqgDtj6sA7Y+sAO2PrQDtj64A7Y+vAO2PsADtj7EA7Y+yAO2PswDtj7QA7Y+1AO2PtgDtj7cA7Y+4AO2PuQDtj7oA7Y+7AO2PvADtj70A7Y++AO2PvwDtkIAA7ZCBAO2QggDtkIMA7ZCEAO2QhQDtkIYA7ZCHAO2QiADtkIkA7ZCKAO2QiwDtkIwA7ZCNAO2QjgDtkI8A7ZCQAO2QkQDtkJIA7ZCTAO2QlADtkJUA7ZCWAO2QlwDtkJgA7ZCZAO2QmgDtkJsA7ZCcAO2QnQDtkJ4A7ZCfAO2QoADtkKEA7ZCiAO2QowDtkKQA7ZClAO2QpgDtkKcA7ZCoAO2QqQDtkKoA7ZCrAO2QrADtkK0A7ZCuAO2QrwDtkLAA7ZCxAO2QsgDtkLMA7ZC0AO2QtQDtkLYA7ZC3AO2QuADtkLkA7ZC6AO2QuwDtkLwA7ZC9AO2QvgDtkL8A7ZGAAO2RgQDtkYIA7ZGDAO2RhADtkYUA7ZGGAO2RhwDtkYgA7ZGJAO2RigDtkYsA7ZGMAO2RjQDtkY4A7ZGPAO2RkADtkZEA7ZGSAO2RkwDtkZQA7ZGVAO2RlgDtkZcA7ZGYAO2RmQDtkZoA7ZGbAO2RnADtkZ0A7ZGeAO2RnwDtkaAA7ZGhAO2RogDtkaMA7ZGkAO2RpQDtkaYA7ZGnAO2RqADtkakA7ZGqAO2RqwDtkawA7ZGtAO2RrgDtka8A7ZGwAO2RsQDtkbIA7ZGzAO2RtADtkbUA7ZG2AO2RtwDtkbgA7ZG5AO2RugDtkbsA7ZG8AO2RvQDtkb4A7ZG/AO2SgADtkoEA7ZKCAO2SgwDtkoQA7ZKFAO2ShgDtkocA7ZKIAO2SiQDtkooA7ZKLAO2SjADtko0A7ZKOAO2SjwDtkpAA7ZKRAO2SkgDtkpMA7ZKUAO2SlQDtkpYA7ZKXAO2SmADtkpkA7ZKaAO2SmwDtkpwA7ZKdAO2SngDtkp8A7ZKgAO2SoQDtkqIA7ZKjAO2SpADtkqUA7ZKmAO2SpwDtkqgA7ZKpAO2SqgDtkqsA7ZKsAO2SrQDtkq4A7ZKvAO2SsADtkrEA7ZKyAO2SswDtkrQA7ZK1AO2StgDtkrcA7ZK4AO2SuQDtkroA7ZK7AO2SvADtkr0A7ZK+AO2SvwDtk4AA7ZOBAO2TggDtk4MA7ZOEAO2ThQDtk4YA7ZOHAO2TiADtk4kA7ZOKAO2TiwDtk4wA7ZONAO2TjgDtk48A7ZOQAO2TkQDtk5IA7ZOTAO2TlADtk5UA7ZOWAO2TlwDtk5gA7ZOZAO2TmgDtk5sA7ZOcAO2TnQDtk54A7ZOfAO2ToADtk6EA7ZOiAO2TowDtk6QA7ZOlAO2TpgDtk6cA7ZOoAO2TqQDtk6oA7ZOrAO2TrADtk60A7ZOuAO2TrwDtk7AA7ZOxAO2TsgDtk7MA7ZO0AO2TtQDtk7YA7ZO3AO2TuADtk7kA7ZO6AO2TuwDtk7wA7ZO9AO2TvgDtk78A7ZSAAO2UgQDtlIIA7ZSDAO2UhADtlIUA7ZSGAO2UhwDtlIgA7ZSJAO2UigDtlIsA7ZSMAO2UjQDtlI4A7ZSPAO2UkADtlJEA7ZSSAO2UkwDtlJQA7ZSVAO2UlgDtlJcA7ZSYAO2UmQDtlJoA7ZSbAO2UnADtlJ0A7ZSeAO2UnwDtlKAA7ZShAO2UogDtlKMA7ZSkAO2UpQDtlKYA7ZSnAO2UqADtlKkA7ZSqAO2UqwDtlKwA7ZStAO2UrgDtlK8A7ZSwAO2UsQDtlLIA7ZSzAO2UtADtlLUA7ZS2AO2UtwDtlLgA7ZS5AO2UugDtlLsA7ZS8AO2UvQDtlL4A7ZS/AO2VgADtlYEA7ZWCAO2VgwDtlYQA7ZWFAO2VhgDtlYcA7ZWIAO2ViQDtlYoA7ZWLAO2VjADtlY0A7ZWOAO2VjwDtlZAA7ZWRAO2VkgDtlZMA7ZWUAO2VlQDtlZYA7ZWXAO2VmADtlZkA7ZWaAO2VmwDtlZwA7ZWdAO2VngDtlZ8A7ZWgAO2VoQDtlaIA7ZWjAO2VpADtlaUA7ZWmAO2VpwDtlagA7ZWpAO2VqgDtlasA7ZWsAO2VrQDtla4A7ZWvAO2VsADtlbEA7ZWyAO2VswDtlbQA7ZW1AO2VtgDtlbcA7ZW4AO2VuQDtlboA7ZW7AO2VvADtlb0A7ZW+AO2VvwDtloAA7ZaBAO2WggDtloMA7ZaEAO2WhQDtloYA7ZaHAO2WiADtlokA7ZaKAO2WiwDtlowA7ZaNAO2WjgDtlo8A7ZaQAO2WkQDtlpIA7ZaTAO2WlADtlpUA7ZaWAO2WlwDtlpgA7ZaZAO2WmgDtlpsA7ZacAO2WnQDtlp4A7ZafAO2WoADtlqEA7ZaiAO2WowDtlqQA7ZalAO2WpgDtlqcA7ZaoAO2WqQDtlqoA7ZarAO2WrADtlq0A7ZauAO2WrwDtlrAA7ZaxAO2WsgDtlrMA7Za0AO2WtQDtlrYA7Za3AO2WuADtlrkA7Za6AO2WuwDtlrwA7Za9AO2WvgDtlr8A7ZeAAO2XgQDtl4IA7ZeDAO2XhADtl4UA7ZeGAO2XhwDtl4gA7ZeJAO2XigDtl4sA7ZeMAO2XjQDtl44A7ZePAO2XkADtl5EA7ZeSAO2XkwDtl5QA7ZeVAO2XlgDtl5cA7ZeYAO2XmQDtl5oA7ZebAO2XnADtl50A7ZeeAO2XnwDtl6AA7ZehAO2XogDtl6MA7ZekAO2XpQDtl6YA7ZenAO2XqADtl6kA7ZeqAO2XqwDtl6wA7ZetAO2XrgDtl68A7ZewAO2XsQDtl7IA7ZezAO2XtADtl7UA7Ze2AO2XtwDtl7gA7Ze5AO2XugDtl7sA7Ze8AO2XvQDtl74A7Ze/AO2YgADtmIEA7ZiCAO2YgwDtmIQA7ZiFAO2YhgDtmIcA7ZiIAO2YiQDtmIoA7ZiLAO2YjADtmI0A7ZiOAO2YjwDtmJAA7ZiRAO2YkgDtmJMA7ZiUAO2YlQDtmJYA7ZiXAO2YmADtmJkA7ZiaAO2YmwDtmJwA7ZidAO2YngDtmJ8A7ZigAO2YoQDtmKIA7ZijAO2YpADtmKUA7ZimAO2YpwDtmKgA7ZipAO2YqgDtmKsA7ZisAO2YrQDtmK4A7ZivAO2YsADtmLEA7ZiyAO2YswDtmLQA7Zi1AO2YtgDtmLcA7Zi4AO2YuQDtmLoA7Zi7AO2YvADtmL0A7Zi+AO2YvwDtmYAA7ZmBAO2ZggDtmYMA7ZmEAO2ZhQDtmYYA7ZmHAO2ZiADtmYkA7ZmKAO2ZiwDtmYwA7ZmNAO2ZjgDtmY8A7ZmQAO2ZkQDtmZIA7ZmTAO2ZlADtmZUA7ZmWAO2ZlwDtmZgA7ZmZAO2ZmgDtmZsA7ZmcAO2ZnQDtmZ4A7ZmfAO2ZoADtmaEA7ZmiAO2ZowDtmaQA7ZmlAO2ZpgDtmacA7ZmoAO2ZqQDtmaoA7ZmrAO2ZrADtma0A7ZmuAO2ZrwDtmbAA7ZmxAO2ZsgDtmbMA7Zm0AO2ZtQDtmbYA7Zm3AO2ZuADtmbkA7Zm6AO2ZuwDtmbwA7Zm9AO2ZvgDtmb8A7ZqAAO2agQDtmoIA7ZqDAO2ahADtmoUA7ZqGAO2ahwDtmogA7ZqJAO2aigDtmosA7ZqMAO2ajQDtmo4A7ZqPAO2akADtmpEA7ZqSAO2akwDtmpQA7ZqVAO2algDtmpcA7ZqYAO2amQDtmpoA7ZqbAO2anADtmp0A7ZqeAO2anwDtmqAA7ZqhAO2aogDtmqMA7ZqkAO2apQDtmqYA7ZqnAO2aqADtmqkA7ZqqAO2aqwDtmqwA7ZqtAO2argDtmq8A7ZqwAO2asQDtmrIA7ZqzAO2atADtmrUA7Zq2AO2atwDtmrgA7Zq5AO2augDtmrsA7Zq8AO2avQDtmr4A7Zq/AO2bgADtm4EA7ZuCAO2bgwDtm4QA7ZuFAO2bhgDtm4cA7ZuIAO2biQDtm4oA7ZuLAO2bjADtm40A7ZuOAO2bjwDtm5AA7ZuRAO2bkgDtm5MA7ZuUAO2blQDtm5YA7ZuXAO2bmADtm5kA7ZuaAO2bmwDtm5wA7ZudAO2bngDtm58A7ZugAO2boQDtm6IA7ZujAO2bpADtm6UA7ZumAO2bpwDtm6gA7ZupAO2bqgDtm6sA7ZusAO2brQDtm64A7ZuvAO2bsADtm7EA7ZuyAO2bswDtm7QA7Zu1AO2btgDtm7cA7Zu4AO2buQDtm7oA7Zu7AO2bvADtm70A7Zu+AO2bvwDtnIAA7ZyBAO2cggDtnIMA7ZyEAO2chQDtnIYA7ZyHAO2ciADtnIkA7ZyKAO2ciwDtnIwA7ZyNAO2cjgDtnI8A7ZyQAO2ckQDtnJIA7ZyTAO2clADtnJUA7ZyWAO2clwDtnJgA7ZyZAO2cmgDtnJsA7ZycAO2cnQDtnJ4A7ZyfAO2coADtnKEA7ZyiAO2cowDtnKQA7ZylAO2cpgDtnKcA7ZyoAO2cqQDtnKoA7ZyrAO2crADtnK0A7ZyuAO2crwDtnLAA7ZyxAO2csgDtnLMA7Zy0AO2ctQDtnLYA7Zy3AO2cuADtnLkA7Zy6AO2cuwDtnLwA7Zy9AO2cvgDtnL8A7Z2AAO2dgQDtnYIA7Z2DAO2dhADtnYUA7Z2GAO2dhwDtnYgA7Z2JAO2digDtnYsA7Z2MAO2djQDtnY4A7Z2PAO2dkADtnZEA7Z2SAO2dkwDtnZQA7Z2VAO2dlgDtnZcA7Z2YAO2dmQDtnZoA7Z2bAO2dnADtnZ0A7Z2eAO2dnwDtnaAA7Z2hAO2dogDtnaMA7Z2kAO2dpQDtnaYA7Z2nAO2dqADtnakA7Z2qAO2dqwDtnawA7Z2tAO2drgDtna8A7Z2wAO2dsQDtnbIA7Z2zAO2dtADtnbUA7Z22AO2dtwDtnbgA7Z25AO2dugDtnbsA7Z28AO2dvQDtnb4A7Z2/AO2egADtnoEA7Z6CAO2egwDtnoQA7Z6FAO2ehgDtnocA7Z6IAO2eiQDtnooA7Z6LAO2ejADtno0A7Z6OAO2ejwDtnpAA7Z6RAO2ekgDtnpMA7Z6UAO2elQDtnpYA7Z6XAO2emADtnpkA7Z6aAO2emwDtnpwA7Z6dAO2engDtnp8A7Z6gAO2eoQDtnqIA7Z6jAPCRgpoA8JGCnADwkYKrAPCRhK4A8JGErwDwkY2LAPCRjYwA8JGSuwDwkZK8APCRkr4A8JGWugDwkZa7APCdhZfwnYWlAPCdhZjwnYWlAPCdhZjwnYWl8J2FrgDwnYWY8J2FpfCdha8A8J2FmPCdhaXwnYWwAPCdhZjwnYWl8J2FsQDwnYWY8J2FpfCdhbIA8J2GufCdhaUA8J2GufCdhaXwnYWuAPCdhrnwnYWl8J2FrwDwnYa68J2FpQDwnYa68J2FpfCdha4A8J2GuvCdhaXwnYWvAPCghKIA8KCUnADwoJSlAPCglYsA8KCYugDwoKCEAPCgo54A8KCorADwoK2jAPChk6QA8KGaqADwoZuqAPChp4gA8KGsmADwobSLAPCht6QA8KG3pgDwooaDAPCihp8A8KKMsQDwopuUAPCioYQA8KKhigDwoqyMAPCir7EA8KOAigDwo4q4APCjjZ8A8KOOkwDwo46cAPCjj4MA8KOPlQDwo5GtAPCjmqMA8KOipwDwo6qNAPCjq7oA8KOyvADwo7SeAPCju5EA8KO9ngDwo76OAPCkiaMA8KSLrgDwpI6rAPCkmIgA8KSctQDwpKCUAPCksLYA8KSykgDwpL6hAPCkvrgA8KWBhADwpYOyAPClg7MA8KWEmQDwpYSzAPCliYkA8KWQnQDwpZimAPClmpoA8KWbhQDwpaW8APClqqcA8KWuqwDwpbKAAPCls5AA8KW+hgDwpoeaAPCmiKgA8KaJhwDwpouZAPCmjL4A8KaTmgDwppSjAPCmlqgA8KaepwDwpp61APCmrLwA8KawtgDwprOVAPCmtasA8Ka8rADwpr6xAPCng5IA8KePigDwp5mnAPCnoq4A8KelpgDwp7KoAPCnu5MA8Ke8rwDwqJeSAPCol60A8KicrgDwqK+6APCotbcA8KmFhQDwqYefAPCpiJoA8KmQigDwqZKWAPCplrYA8KmssADwqoOOAPCqhIUA8KqIjgDwqoqRAPCqjpIA8KqYgAA="
+ },
+ {
+ "type": "Strip",
+ "strip_left": false,
+ "strip_right": true
+ },
+ {
+ "type": "Replace",
+ "pattern": {
+ "Regex": " {2,}"
+ },
+ "content": "▁"
+ }
+ ]
+ },
+ "pre_tokenizer": {
+ "type": "Metaspace",
+ "replacement": "▁",
+ "prepend_scheme": "always",
+ "split": true
+ },
+ "post_processor": {
+ "type": "TemplateProcessing",
+ "single": [
+ {
+ "Sequence": {
+ "id": "A",
+ "type_id": 0
+ }
+ },
+ {
+ "SpecialToken": {
+ "id": "",
+ "type_id": 0
+ }
+ }
+ ],
+ "pair": [
+ {
+ "Sequence": {
+ "id": "A",
+ "type_id": 0
+ }
+ },
+ {
+ "SpecialToken": {
+ "id": "",
+ "type_id": 0
+ }
+ },
+ {
+ "Sequence": {
+ "id": "B",
+ "type_id": 0
+ }
+ },
+ {
+ "SpecialToken": {
+ "id": "",
+ "type_id": 0
+ }
+ }
+ ],
+ "special_tokens": {
+ "": {
+ "id": "",
+ "ids": [
+ 1
+ ],
+ "tokens": [
+ ""
+ ]
+ }
+ }
+ },
+ "decoder": {
+ "type": "Metaspace",
+ "replacement": "▁",
+ "prepend_scheme": "always",
+ "split": true
+ },
+ "model": {
+ "type": "Unigram",
+ "unk_id": 2,
+ "vocab": [
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "▁",
+ -2.0122928619384766
+ ],
+ [
+ "X",
+ -2.486478805541992
+ ],
+ [
+ ".",
+ -3.5449328422546387
+ ],
+ [
+ ",",
+ -3.649247407913208
+ ],
+ [
+ "s",
+ -3.9033992290496826
+ ],
+ [
+ "▁the",
+ -3.9598512649536133
+ ],
+ [
+ "a",
+ -4.097104549407959
+ ],
+ [
+ ":",
+ -4.414328098297119
+ ],
+ [
+ "▁and",
+ -4.420670986175537
+ ],
+ [
+ "▁to",
+ -4.4523234367370605
+ ],
+ [
+ "▁of",
+ -4.572070121765137
+ ],
+ [
+ "▁fill",
+ -4.575019836425781
+ ],
+ [
+ "e",
+ -4.674920082092285
+ ],
+ [
+ "▁in",
+ -4.812063694000244
+ ],
+ [
+ "t",
+ -5.063905715942383
+ ],
+ [
+ "-",
+ -5.129043102264404
+ ],
+ [
+ "▁is",
+ -5.283425331115723
+ ],
+ [
+ "▁de",
+ -5.344141960144043
+ ],
+ [
+ "▁for",
+ -5.3930158615112305
+ ],
+ [
+ "’",
+ -5.4228339195251465
+ ],
+ [
+ "i",
+ -5.469857692718506
+ ],
+ [
+ "▁that",
+ -5.576240539550781
+ ],
+ [
+ "▁you",
+ -5.596375465393066
+ ],
+ [
+ "d",
+ -5.6047282218933105
+ ],
+ [
+ "▁I",
+ -5.6640448570251465
+ ],
+ [
+ "▁with",
+ -5.703730583190918
+ ],
+ [
+ "n",
+ -5.737886905670166
+ ],
+ [
+ "▁on",
+ -5.784142971038818
+ ],
+ [
+ "'",
+ -5.828996181488037
+ ],
+ [
+ "o",
+ -5.925558090209961
+ ],
+ [
+ "▁are",
+ -5.931313991546631
+ ],
+ [
+ "▁it",
+ -5.939518928527832
+ ],
+ [
+ "en",
+ -5.9465556144714355
+ ],
+ [
+ "▁be",
+ -5.9556708335876465
+ ],
+ [
+ "▁The",
+ -5.990020751953125
+ ],
+ [
+ "▁as",
+ -6.057407379150391
+ ],
+ [
+ "▁your",
+ -6.132311820983887
+ ],
+ [
+ "l",
+ -6.139498710632324
+ ],
+ [
+ "▁(",
+ -6.184796333312988
+ ],
+ [
+ "▁or",
+ -6.241950035095215
+ ],
+ [
+ "▁have",
+ -6.27459192276001
+ ],
+ [
+ "▁at",
+ -6.327472686767578
+ ],
+ [
+ "▁from",
+ -6.349645137786865
+ ],
+ [
+ "▁an",
+ -6.350090980529785
+ ],
+ [
+ "▁was",
+ -6.350385665893555
+ ],
+ [
+ "▁this",
+ -6.352563381195068
+ ],
+ [
+ "er",
+ -6.3604278564453125
+ ],
+ [
+ "▁la",
+ -6.3624043464660645
+ ],
+ [
+ "m",
+ -6.375206470489502
+ ],
+ [
+ "r",
+ -6.376530170440674
+ ],
+ [
+ "ing",
+ -6.3778581619262695
+ ],
+ [
+ "▁can",
+ -6.387146472930908
+ ],
+ [
+ "!",
+ -6.421379566192627
+ ],
+ [
+ "▁will",
+ -6.423982620239258
+ ],
+ [
+ "▁by",
+ -6.44155216217041
+ ],
+ [
+ "?",
+ -6.585887432098389
+ ],
+ [
+ "▁not",
+ -6.5959086418151855
+ ],
+ [
+ "re",
+ -6.620072364807129
+ ],
+ [
+ ")",
+ -6.63656759262085
+ ],
+ [
+ "▁we",
+ -6.643022060394287
+ ],
+ [
+ "y",
+ -6.654535293579102
+ ],
+ [
+ "▁und",
+ -6.741473197937012
+ ],
+ [
+ "▁has",
+ -6.7602033615112305
+ ],
+ [
+ "▁all",
+ -6.768176555633545
+ ],
+ [
+ "▁die",
+ -6.8641204833984375
+ ],
+ [
+ "▁but",
+ -6.906830310821533
+ ],
+ [
+ "▁our",
+ -6.909878730773926
+ ],
+ [
+ "▁their",
+ -6.91325044631958
+ ],
+ [
+ "▁A",
+ -6.915814399719238
+ ],
+ [
+ "▁more",
+ -6.918668746948242
+ ],
+ [
+ "▁un",
+ -6.924930572509766
+ ],
+ [
+ "▁der",
+ -6.925402641296387
+ ],
+ [
+ "c",
+ -6.925714015960693
+ ],
+ [
+ "u",
+ -6.932939052581787
+ ],
+ [
+ "in",
+ -6.934063911437988
+ ],
+ [
+ "▁so",
+ -6.947050094604492
+ ],
+ [
+ "▁they",
+ -6.989297866821289
+ ],
+ [
+ "▁one",
+ -7.012735843658447
+ ],
+ [
+ "▁about",
+ -7.071486473083496
+ ],
+ [
+ "▁my",
+ -7.072140693664551
+ ],
+ [
+ "ul",
+ -7.076492786407471
+ ],
+ [
+ "▁which",
+ -7.097039222717285
+ ],
+ [
+ "à",
+ -7.099997520446777
+ ],
+ [
+ "▁In",
+ -7.100254535675049
+ ],
+ [
+ "/",
+ -7.100865840911865
+ ],
+ [
+ "he",
+ -7.104752540588379
+ ],
+ [
+ "f",
+ -7.110044002532959
+ ],
+ [
+ "▁le",
+ -7.112937927246094
+ ],
+ [
+ "▁out",
+ -7.128556728363037
+ ],
+ [
+ "▁also",
+ -7.133583068847656
+ ],
+ [
+ "▁des",
+ -7.156766414642334
+ ],
+ [
+ "▁It",
+ -7.162121295928955
+ ],
+ [
+ "▁up",
+ -7.1723432540893555
+ ],
+ [
+ "▁\"",
+ -7.172809600830078
+ ],
+ [
+ "▁time",
+ -7.178046703338623
+ ],
+ [
+ "ă",
+ -7.183253765106201
+ ],
+ [
+ "if",
+ -7.185171127319336
+ ],
+ [
+ "▁This",
+ -7.191652297973633
+ ],
+ [
+ "▁We",
+ -7.223267078399658
+ ],
+ [
+ "p",
+ -7.224130153656006
+ ],
+ [
+ "▁do",
+ -7.228212356567383
+ ],
+ [
+ "–",
+ -7.235409736633301
+ ],
+ [
+ "▁“",
+ -7.238142013549805
+ ],
+ [
+ "on",
+ -7.240827560424805
+ ],
+ [
+ "h",
+ -7.2543206214904785
+ ],
+ [
+ "▁si",
+ -7.276725769042969
+ ],
+ [
+ "le",
+ -7.2994256019592285
+ ],
+ [
+ "▁les",
+ -7.312957286834717
+ ],
+ [
+ "▁în",
+ -7.314571857452393
+ ],
+ [
+ "▁his",
+ -7.324767112731934
+ ],
+ [
+ "▁who",
+ -7.35105562210083
+ ],
+ [
+ "▁like",
+ -7.371364116668701
+ ],
+ [
+ "b",
+ -7.375369071960449
+ ],
+ [
+ "▁when",
+ -7.380199432373047
+ ],
+ [
+ ";",
+ -7.380846977233887
+ ],
+ [
+ "▁been",
+ -7.38668966293335
+ ],
+ [
+ "▁other",
+ -7.388518333435059
+ ],
+ [
+ "ly",
+ -7.394660949707031
+ ],
+ [
+ "\"",
+ -7.407205104827881
+ ],
+ [
+ "g",
+ -7.407997131347656
+ ],
+ [
+ "▁cu",
+ -7.415276527404785
+ ],
+ [
+ "▁care",
+ -7.432408332824707
+ ],
+ [
+ "▁what",
+ -7.433043003082275
+ ],
+ [
+ "▁new",
+ -7.4370903968811035
+ ],
+ [
+ "or",
+ -7.445409774780273
+ ],
+ [
+ "▁some",
+ -7.461953639984131
+ ],
+ [
+ "▁get",
+ -7.479001998901367
+ ],
+ [
+ "▁were",
+ -7.491549491882324
+ ],
+ [
+ "▁just",
+ -7.492495536804199
+ ],
+ [
+ "▁there",
+ -7.493194103240967
+ ],
+ [
+ "▁would",
+ -7.494382381439209
+ ],
+ [
+ "S",
+ -7.4974141120910645
+ ],
+ [
+ "▁them",
+ -7.513596057891846
+ ],
+ [
+ "▁any",
+ -7.520544052124023
+ ],
+ [
+ ").",
+ -7.521052360534668
+ ],
+ [
+ "al",
+ -7.523056983947754
+ ],
+ [
+ "▁into",
+ -7.527902603149414
+ ],
+ [
+ "▁me",
+ -7.528337001800537
+ ],
+ [
+ "▁had",
+ -7.532425403594971
+ ],
+ [
+ "▁se",
+ -7.5451483726501465
+ ],
+ [
+ "▁make",
+ -7.5827131271362305
+ ],
+ [
+ "at",
+ -7.589433670043945
+ ],
+ [
+ "▁than",
+ -7.592360019683838
+ ],
+ [
+ "▁du",
+ -7.595852375030518
+ ],
+ [
+ "▁over",
+ -7.6078782081604
+ ],
+ [
+ "▁You",
+ -7.626111030578613
+ ],
+ [
+ "▁how",
+ -7.635554313659668
+ ],
+ [
+ "▁no",
+ -7.63729190826416
+ ],
+ [
+ "▁people",
+ -7.639947414398193
+ ],
+ [
+ "an",
+ -7.64084005355835
+ ],
+ [
+ "”",
+ -7.644528865814209
+ ],
+ [
+ "é",
+ -7.646921157836914
+ ],
+ [
+ "it",
+ -7.648641109466553
+ ],
+ [
+ "▁If",
+ -7.648687839508057
+ ],
+ [
+ "k",
+ -7.6605634689331055
+ ],
+ [
+ "▁pe",
+ -7.662139415740967
+ ],
+ [
+ "is",
+ -7.66726016998291
+ ],
+ [
+ "▁her",
+ -7.6733808517456055
+ ],
+ [
+ "▁work",
+ -7.680386543273926
+ ],
+ [
+ "ve",
+ -7.687412738800049
+ ],
+ [
+ "▁only",
+ -7.69785737991333
+ ],
+ [
+ "▁may",
+ -7.702393531799316
+ ],
+ [
+ "▁its",
+ -7.702449798583984
+ ],
+ [
+ "▁first",
+ -7.704373836517334
+ ],
+ [
+ "▁most",
+ -7.708309173583984
+ ],
+ [
+ "▁well",
+ -7.708758354187012
+ ],
+ [
+ "▁use",
+ -7.715085983276367
+ ],
+ [
+ "▁zu",
+ -7.718777656555176
+ ],
+ [
+ "▁pour",
+ -7.736708164215088
+ ],
+ [
+ "z",
+ -7.745654106140137
+ ],
+ [
+ "il",
+ -7.745913982391357
+ ],
+ [
+ "▁need",
+ -7.74778938293457
+ ],
+ [
+ "▁these",
+ -7.763317584991455
+ ],
+ [
+ "▁din",
+ -7.769891262054443
+ ],
+ [
+ "▁den",
+ -7.775663375854492
+ ],
+ [
+ "▁us",
+ -7.778133869171143
+ ],
+ [
+ "able",
+ -7.779712200164795
+ ],
+ [
+ "▁S",
+ -7.781893730163574
+ ],
+ [
+ "▁mit",
+ -7.792516231536865
+ ],
+ [
+ "▁very",
+ -7.79970645904541
+ ],
+ [
+ "▁am",
+ -7.814100742340088
+ ],
+ [
+ "&",
+ -7.829529285430908
+ ],
+ [
+ "▁au",
+ -7.83012056350708
+ ],
+ [
+ "▁many",
+ -7.83834171295166
+ ],
+ [
+ "▁mai",
+ -7.84363317489624
+ ],
+ [
+ "A",
+ -7.849830150604248
+ ],
+ [
+ "th",
+ -7.855541229248047
+ ],
+ [
+ "▁through",
+ -7.859585285186768
+ ],
+ [
+ "▁pentru",
+ -7.86391544342041
+ ],
+ [
+ "▁two",
+ -7.873607158660889
+ ],
+ [
+ "▁von",
+ -7.874959945678711
+ ],
+ [
+ "▁way",
+ -7.887117385864258
+ ],
+ [
+ "ll",
+ -7.887749195098877
+ ],
+ [
+ "I",
+ -7.891303539276123
+ ],
+ [
+ "▁ce",
+ -7.9015631675720215
+ ],
+ [
+ "▁și",
+ -7.904444694519043
+ ],
+ [
+ "▁help",
+ -7.907405853271484
+ ],
+ [
+ "▁best",
+ -7.907911777496338
+ ],
+ [
+ "),",
+ -7.908212184906006
+ ],
+ [
+ "un",
+ -7.925017833709717
+ ],
+ [
+ "▁years",
+ -7.925964832305908
+ ],
+ [
+ "▁2",
+ -7.9282684326171875
+ ],
+ [
+ "▁C",
+ -7.936962604522705
+ ],
+ [
+ "▁nu",
+ -7.939520835876465
+ ],
+ [
+ "▁good",
+ -7.943995952606201
+ ],
+ [
+ "v",
+ -7.94746732711792
+ ],
+ [
+ "▁1",
+ -7.94765567779541
+ ],
+ [
+ "w",
+ -7.947978496551514
+ ],
+ [
+ "▁das",
+ -7.960538864135742
+ ],
+ [
+ "▁ca",
+ -7.962430477142334
+ ],
+ [
+ "▁where",
+ -7.964908123016357
+ ],
+ [
+ "▁know",
+ -7.96622896194458
+ ],
+ [
+ "▁year",
+ -7.971063613891602
+ ],
+ [
+ "▁He",
+ -7.974609375
+ ],
+ [
+ "▁see",
+ -7.980011463165283
+ ],
+ [
+ "▁für",
+ -7.984004497528076
+ ],
+ [
+ "▁auf",
+ -7.984249114990234
+ ],
+ [
+ "▁3",
+ -7.984433650970459
+ ],
+ [
+ "de",
+ -7.985401153564453
+ ],
+ [
+ "est",
+ -8.002091407775879
+ ],
+ [
+ "▁back",
+ -8.007022857666016
+ ],
+ [
+ "▁such",
+ -8.008523941040039
+ ],
+ [
+ "▁should",
+ -8.011754989624023
+ ],
+ [
+ "x",
+ -8.015050888061523
+ ],
+ [
+ "▁after",
+ -8.01761245727539
+ ],
+ [
+ "▁could",
+ -8.019674301147461
+ ],
+ [
+ "▁ist",
+ -8.020784378051758
+ ],
+ [
+ "▁now",
+ -8.022845268249512
+ ],
+ [
+ "▁much",
+ -8.023111343383789
+ ],
+ [
+ "and",
+ -8.02390193939209
+ ],
+ [
+ "...",
+ -8.030110359191895
+ ],
+ [
+ "▁home",
+ -8.036273956298828
+ ],
+ [
+ "to",
+ -8.03821086883545
+ ],
+ [
+ "▁ein",
+ -8.04833984375
+ ],
+ [
+ "▁even",
+ -8.048656463623047
+ ],
+ [
+ "▁que",
+ -8.049829483032227
+ ],
+ [
+ "▁day",
+ -8.051553726196289
+ ],
+ [
+ "▁take",
+ -8.054189682006836
+ ],
+ [
+ "▁want",
+ -8.054435729980469
+ ],
+ [
+ "▁For",
+ -8.06217098236084
+ ],
+ [
+ "▁said",
+ -8.063249588012695
+ ],
+ [
+ "▁sur",
+ -8.073471069335938
+ ],
+ [
+ "▁une",
+ -8.077030181884766
+ ],
+ [
+ "▁să",
+ -8.082921028137207
+ ],
+ [
+ "▁dans",
+ -8.084549903869629
+ ],
+ [
+ "▁great",
+ -8.088057518005371
+ ],
+ [
+ "▁este",
+ -8.08947467803955
+ ],
+ [
+ "▁because",
+ -8.094311714172363
+ ],
+ [
+ "▁information",
+ -8.104085922241211
+ ],
+ [
+ "ului",
+ -8.105451583862305
+ ],
+ [
+ "▁find",
+ -8.112174987792969
+ ],
+ [
+ "C",
+ -8.119946479797363
+ ],
+ [
+ "▁she",
+ -8.125317573547363
+ ],
+ [
+ "▁im",
+ -8.126056671142578
+ ],
+ [
+ "ation",
+ -8.130115509033203
+ ],
+ [
+ "▁then",
+ -8.13021469116211
+ ],
+ [
+ "▁est",
+ -8.13099479675293
+ ],
+ [
+ "▁par",
+ -8.138585090637207
+ ],
+ [
+ "▁used",
+ -8.141871452331543
+ ],
+ [
+ "▁E",
+ -8.146790504455566
+ ],
+ [
+ "▁made",
+ -8.149978637695312
+ ],
+ [
+ "▁So",
+ -8.15785026550293
+ ],
+ [
+ "am",
+ -8.16288948059082
+ ],
+ [
+ "▁eine",
+ -8.165464401245117
+ ],
+ [
+ "▁şi",
+ -8.168368339538574
+ ],
+ [
+ "▁business",
+ -8.17335033416748
+ ],
+ [
+ "▁right",
+ -8.173593521118164
+ ],
+ [
+ "▁here",
+ -8.176125526428223
+ ],
+ [
+ "▁being",
+ -8.184967041015625
+ ],
+ [
+ "▁B",
+ -8.185355186462402
+ ],
+ [
+ "▁those",
+ -8.185736656188965
+ ],
+ [
+ "▁before",
+ -8.194721221923828
+ ],
+ [
+ "▁And",
+ -8.199501037597656
+ ],
+ [
+ "▁P",
+ -8.200712203979492
+ ],
+ [
+ "ers",
+ -8.200922012329102
+ ],
+ [
+ "▁don",
+ -8.204029083251953
+ ],
+ [
+ "B",
+ -8.20487117767334
+ ],
+ [
+ "▁life",
+ -8.206265449523926
+ ],
+ [
+ "▁go",
+ -8.209736824035645
+ ],
+ [
+ "▁As",
+ -8.210551261901855
+ ],
+ [
+ "▁M",
+ -8.221170425415039
+ ],
+ [
+ "▁each",
+ -8.22955322265625
+ ],
+ [
+ "▁qui",
+ -8.23323917388916
+ ],
+ [
+ "▁place",
+ -8.236248970031738
+ ],
+ [
+ "com",
+ -8.237479209899902
+ ],
+ [
+ "ant",
+ -8.252915382385254
+ ],
+ [
+ "▁sich",
+ -8.255932807922363
+ ],
+ [
+ "▁There",
+ -8.261948585510254
+ ],
+ [
+ "ar",
+ -8.264991760253906
+ ],
+ [
+ "▁Sie",
+ -8.273868560791016
+ ],
+ [
+ "▁own",
+ -8.277531623840332
+ ],
+ [
+ "▁part",
+ -8.279440879821777
+ ],
+ [
+ "ent",
+ -8.281047821044922
+ ],
+ [
+ "▁world",
+ -8.28173542022705
+ ],
+ [
+ "ment",
+ -8.282004356384277
+ ],
+ [
+ "▁while",
+ -8.294474601745605
+ ],
+ [
+ "▁But",
+ -8.295366287231445
+ ],
+ [
+ "▁around",
+ -8.300799369812012
+ ],
+ [
+ "▁L",
+ -8.301082611083984
+ ],
+ [
+ "us",
+ -8.304039001464844
+ ],
+ [
+ "▁plus",
+ -8.313054084777832
+ ],
+ [
+ "▁To",
+ -8.313691139221191
+ ],
+ [
+ "▁5",
+ -8.31412410736084
+ ],
+ [
+ "▁high",
+ -8.31862735748291
+ ],
+ [
+ "▁long",
+ -8.319378852844238
+ ],
+ [
+ "D",
+ -8.320075035095215
+ ],
+ [
+ "▁D",
+ -8.320279121398926
+ ],
+ [
+ "▁really",
+ -8.322924613952637
+ ],
+ [
+ "▁nicht",
+ -8.332040786743164
+ ],
+ [
+ "▁Le",
+ -8.335328102111816
+ ],
+ [
+ "▁service",
+ -8.3412504196167
+ ],
+ [
+ "▁4",
+ -8.342093467712402
+ ],
+ [
+ "▁different",
+ -8.342538833618164
+ ],
+ [
+ "▁Die",
+ -8.348092079162598
+ ],
+ [
+ "▁think",
+ -8.353771209716797
+ ],
+ [
+ "—",
+ -8.355998039245605
+ ],
+ [
+ "▁auch",
+ -8.357160568237305
+ ],
+ [
+ "▁look",
+ -8.362202644348145
+ ],
+ [
+ "▁both",
+ -8.366817474365234
+ ],
+ [
+ "lor",
+ -8.36687183380127
+ ],
+ [
+ "▁down",
+ -8.367999076843262
+ ],
+ [
+ "ten",
+ -8.368885040283203
+ ],
+ [
+ "▁La",
+ -8.378066062927246
+ ],
+ [
+ "▁off",
+ -8.380044937133789
+ ],
+ [
+ "▁vous",
+ -8.380541801452637
+ ],
+ [
+ "▁They",
+ -8.381462097167969
+ ],
+ [
+ "M",
+ -8.383248329162598
+ ],
+ [
+ "▁pas",
+ -8.384513854980469
+ ],
+ [
+ "▁data",
+ -8.385709762573242
+ ],
+ [
+ "▁T",
+ -8.386754989624023
+ ],
+ [
+ "▁love",
+ -8.388101577758789
+ ],
+ [
+ "▁every",
+ -8.390009880065918
+ ],
+ [
+ "▁10",
+ -8.391179084777832
+ ],
+ [
+ "▁last",
+ -8.392083168029785
+ ],
+ [
+ "▁same",
+ -8.393481254577637
+ ],
+ [
+ "▁using",
+ -8.395487785339355
+ ],
+ [
+ "▁free",
+ -8.408831596374512
+ ],
+ [
+ "▁dem",
+ -8.40894889831543
+ ],
+ [
+ "▁still",
+ -8.409984588623047
+ ],
+ [
+ "ate",
+ -8.410931587219238
+ ],
+ [
+ "ist",
+ -8.415611267089844
+ ],
+ [
+ "▁between",
+ -8.420283317565918
+ ],
+ [
+ "P",
+ -8.420982360839844
+ ],
+ [
+ "be",
+ -8.428167343139648
+ ],
+ [
+ "▁available",
+ -8.429443359375
+ ],
+ [
+ "man",
+ -8.432978630065918
+ ],
+ [
+ "▁company",
+ -8.439678192138672
+ ],
+ [
+ "▁G",
+ -8.441640853881836
+ ],
+ [
+ "▁experience",
+ -8.444950103759766
+ ],
+ [
+ "▁going",
+ -8.449073791503906
+ ],
+ [
+ "▁site",
+ -8.453832626342773
+ ],
+ [
+ "j",
+ -8.455142974853516
+ ],
+ [
+ "are",
+ -8.456900596618652
+ ],
+ [
+ "▁set",
+ -8.470661163330078
+ ],
+ [
+ "2",
+ -8.473684310913086
+ ],
+ [
+ "▁system",
+ -8.474678039550781
+ ],
+ [
+ "▁important",
+ -8.476791381835938
+ ],
+ [
+ "▁few",
+ -8.482437133789062
+ ],
+ [
+ "▁fi",
+ -8.482551574707031
+ ],
+ [
+ "ich",
+ -8.483301162719727
+ ],
+ [
+ "▁What",
+ -8.488649368286133
+ ],
+ [
+ "▁services",
+ -8.502433776855469
+ ],
+ [
+ "▁under",
+ -8.502569198608398
+ ],
+ [
+ "▁When",
+ -8.50308895111084
+ ],
+ [
+ "▁online",
+ -8.50699520111084
+ ],
+ [
+ "▁New",
+ -8.51494312286377
+ ],
+ [
+ "▁come",
+ -8.524871826171875
+ ],
+ [
+ "▁provide",
+ -8.525650024414062
+ ],
+ [
+ "F",
+ -8.526449203491211
+ ],
+ [
+ "▁team",
+ -8.52782154083252
+ ],
+ [
+ "▁always",
+ -8.529409408569336
+ ],
+ [
+ "▁De",
+ -8.530412673950195
+ ],
+ [
+ "▁că",
+ -8.532517433166504
+ ],
+ [
+ "▁him",
+ -8.53586196899414
+ ],
+ [
+ "▁F",
+ -8.538305282592773
+ ],
+ [
+ "▁things",
+ -8.550079345703125
+ ],
+ [
+ "▁including",
+ -8.550943374633789
+ ],
+ [
+ "▁support",
+ -8.552608489990234
+ ],
+ [
+ "▁number",
+ -8.554113388061523
+ ],
+ [
+ "T",
+ -8.557183265686035
+ ],
+ [
+ "▁during",
+ -8.55886459350586
+ ],
+ [
+ "▁family",
+ -8.560463905334473
+ ],
+ [
+ "▁little",
+ -8.561317443847656
+ ],
+ [
+ "▁three",
+ -8.567726135253906
+ ],
+ [
+ "▁water",
+ -8.56810188293457
+ ],
+ [
+ "▁man",
+ -8.569759368896484
+ ],
+ [
+ "▁An",
+ -8.57192611694336
+ ],
+ [
+ "based",
+ -8.572155952453613
+ ],
+ [
+ "▁R",
+ -8.57442855834961
+ ],
+ [
+ "▁sau",
+ -8.574433326721191
+ ],
+ [
+ "▁avec",
+ -8.576035499572754
+ ],
+ [
+ "▁better",
+ -8.576830863952637
+ ],
+ [
+ "▁„",
+ -8.582253456115723
+ ],
+ [
+ "▁too",
+ -8.58635425567627
+ ],
+ [
+ "ge",
+ -8.586719512939453
+ ],
+ [
+ "▁must",
+ -8.589736938476562
+ ],
+ [
+ "▁per",
+ -8.589916229248047
+ ],
+ [
+ "ele",
+ -8.590399742126465
+ ],
+ [
+ "▁oder",
+ -8.59264850616455
+ ],
+ [
+ "au",
+ -8.59555435180664
+ ],
+ [
+ "▁aus",
+ -8.595727920532227
+ ],
+ [
+ "▁werden",
+ -8.598653793334961
+ ],
+ [
+ "▁does",
+ -8.599140167236328
+ ],
+ [
+ "▁without",
+ -8.599270820617676
+ ],
+ [
+ "▁ou",
+ -8.599929809570312
+ ],
+ [
+ "▁design",
+ -8.60101318359375
+ ],
+ [
+ "▁va",
+ -8.605440139770508
+ ],
+ [
+ "▁did",
+ -8.615679740905762
+ ],
+ [
+ "▁O",
+ -8.619062423706055
+ ],
+ [
+ "▁U",
+ -8.623565673828125
+ ],
+ [
+ "up",
+ -8.62901496887207
+ ],
+ [
+ "▁end",
+ -8.63367748260498
+ ],
+ [
+ "▁local",
+ -8.636231422424316
+ ],
+ [
+ "▁next",
+ -8.638967514038086
+ ],
+ [
+ "▁sure",
+ -8.64098072052002
+ ],
+ [
+ "▁lot",
+ -8.64644718170166
+ ],
+ [
+ "▁Re",
+ -8.647016525268555
+ ],
+ [
+ "▁top",
+ -8.647642135620117
+ ],
+ [
+ "▁Our",
+ -8.656886100769043
+ ],
+ [
+ "▁small",
+ -8.656978607177734
+ ],
+ [
+ "▁full",
+ -8.659418106079102
+ ],
+ [
+ "▁something",
+ -8.662886619567871
+ ],
+ [
+ "ung",
+ -8.666722297668457
+ ],
+ [
+ "▁vor",
+ -8.673250198364258
+ ],
+ [
+ "E",
+ -8.673337936401367
+ ],
+ [
+ "▁give",
+ -8.67603588104248
+ ],
+ [
+ "▁might",
+ -8.67660903930664
+ ],
+ [
+ "▁another",
+ -8.679330825805664
+ ],
+ [
+ "▁6",
+ -8.680779457092285
+ ],
+ [
+ "▁All",
+ -8.681318283081055
+ ],
+ [
+ "▁process",
+ -8.681672096252441
+ ],
+ [
+ "L",
+ -8.682575225830078
+ ],
+ [
+ "▁found",
+ -8.68941593170166
+ ],
+ [
+ "▁sind",
+ -8.690044403076172
+ ],
+ [
+ "▁since",
+ -8.69528865814209
+ ],
+ [
+ "▁With",
+ -8.695560455322266
+ ],
+ [
+ "K",
+ -8.696988105773926
+ ],
+ [
+ "um",
+ -8.701016426086426
+ ],
+ [
+ "▁within",
+ -8.701669692993164
+ ],
+ [
+ "▁post",
+ -8.706608772277832
+ ],
+ [
+ "▁car",
+ -8.709365844726562
+ ],
+ [
+ "une",
+ -8.714099884033203
+ ],
+ [
+ "▁N",
+ -8.715041160583496
+ ],
+ [
+ "▁J",
+ -8.715597152709961
+ ],
+ [
+ "ic",
+ -8.71823787689209
+ ],
+ [
+ "R",
+ -8.722309112548828
+ ],
+ [
+ "ter",
+ -8.727437019348145
+ ],
+ [
+ "ur",
+ -8.728265762329102
+ ],
+ [
+ "▁She",
+ -8.73131275177002
+ ],
+ [
+ "▁public",
+ -8.732009887695312
+ ],
+ [
+ "▁keep",
+ -8.735784530639648
+ ],
+ [
+ "▁H",
+ -8.736178398132324
+ ],
+ [
+ "▁order",
+ -8.740762710571289
+ ],
+ [
+ "▁start",
+ -8.742195129394531
+ ],
+ [
+ "ez",
+ -8.74746322631836
+ ],
+ [
+ "▁‘",
+ -8.749832153320312
+ ],
+ [
+ "uri",
+ -8.751104354858398
+ ],
+ [
+ "▁20",
+ -8.752482414245605
+ ],
+ [
+ "▁On",
+ -8.753515243530273
+ ],
+ [
+ "▁offer",
+ -8.763005256652832
+ ],
+ [
+ "▁quality",
+ -8.764988899230957
+ ],
+ [
+ "▁working",
+ -8.769987106323242
+ ],
+ [
+ "▁No",
+ -8.770307540893555
+ ],
+ [
+ "▁That",
+ -8.775156021118164
+ ],
+ [
+ "▁game",
+ -8.7863187789917
+ ],
+ [
+ "▁bei",
+ -8.786642074584961
+ ],
+ [
+ "▁today",
+ -8.788661003112793
+ ],
+ [
+ "▁never",
+ -8.794586181640625
+ ],
+ [
+ "▁week",
+ -8.79587173461914
+ ],
+ [
+ "▁St",
+ -8.797786712646484
+ ],
+ [
+ "▁feel",
+ -8.799317359924316
+ ],
+ [
+ "▁put",
+ -8.801899909973145
+ ],
+ [
+ "▁website",
+ -8.80322265625
+ ],
+ [
+ "Y",
+ -8.804483413696289
+ ],
+ [
+ "▁days",
+ -8.804709434509277
+ ],
+ [
+ "▁program",
+ -8.805448532104492
+ ],
+ [
+ "▁looking",
+ -8.810463905334473
+ ],
+ [
+ "▁K",
+ -8.810808181762695
+ ],
+ [
+ "▁students",
+ -8.811436653137207
+ ],
+ [
+ "▁create",
+ -8.811800956726074
+ ],
+ [
+ "▁change",
+ -8.812616348266602
+ ],
+ [
+ "▁book",
+ -8.812932014465332
+ ],
+ [
+ "ity",
+ -8.813761711120605
+ ],
+ [
+ "▁At",
+ -8.815207481384277
+ ],
+ [
+ "▁possible",
+ -8.815670013427734
+ ],
+ [
+ "▁sunt",
+ -8.81651496887207
+ ],
+ [
+ "▁7",
+ -8.818120002746582
+ ],
+ [
+ "▁real",
+ -8.823369026184082
+ ],
+ [
+ "▁al",
+ -8.824172019958496
+ ],
+ [
+ "▁making",
+ -8.825371742248535
+ ],
+ [
+ "▁Be",
+ -8.825761795043945
+ ],
+ [
+ "▁products",
+ -8.82592487335205
+ ],
+ [
+ "▁case",
+ -8.82653522491455
+ ],
+ [
+ "▁school",
+ -8.8272066116333
+ ],
+ [
+ "▁say",
+ -8.830352783203125
+ ],
+ [
+ "area",
+ -8.832084655761719
+ ],
+ [
+ "▁My",
+ -8.833836555480957
+ ],
+ [
+ "▁point",
+ -8.834731101989746
+ ],
+ [
+ "▁als",
+ -8.83560848236084
+ ],
+ [
+ "▁children",
+ -8.836194038391113
+ ],
+ [
+ "▁course",
+ -8.844061851501465
+ ],
+ [
+ "▁show",
+ -8.847993850708008
+ ],
+ [
+ "▁8",
+ -8.849273681640625
+ ],
+ [
+ "▁These",
+ -8.849345207214355
+ ],
+ [
+ "▁18",
+ -8.851140975952148
+ ],
+ [
+ "▁large",
+ -8.851323127746582
+ ],
+ [
+ "co",
+ -8.854362487792969
+ ],
+ [
+ "▁über",
+ -8.854788780212402
+ ],
+ [
+ "▁second",
+ -8.856559753417969
+ ],
+ [
+ "▁market",
+ -8.859807014465332
+ ],
+ [
+ "▁fost",
+ -8.86048698425293
+ ],
+ [
+ "▁easy",
+ -8.863983154296875
+ ],
+ [
+ "▁plan",
+ -8.864302635192871
+ ],
+ [
+ "▁project",
+ -8.864927291870117
+ ],
+ [
+ "G",
+ -8.865178108215332
+ ],
+ [
+ "W",
+ -8.869574546813965
+ ],
+ [
+ "3",
+ -8.871939659118652
+ ],
+ [
+ "▁son",
+ -8.873332023620605
+ ],
+ [
+ "la",
+ -8.879053115844727
+ ],
+ [
+ "▁face",
+ -8.88137435913086
+ ],
+ [
+ "▁needs",
+ -8.88148021697998
+ ],
+ [
+ "ch",
+ -8.883138656616211
+ ],
+ [
+ "▁personal",
+ -8.88343620300293
+ ],
+ [
+ "me",
+ -8.886031150817871
+ ],
+ [
+ "▁sont",
+ -8.887377738952637
+ ],
+ [
+ "▁je",
+ -8.894930839538574
+ ],
+ [
+ "▁non",
+ -8.895471572875977
+ ],
+ [
+ "▁got",
+ -8.896591186523438
+ ],
+ [
+ "▁Do",
+ -8.897382736206055
+ ],
+ [
+ "the",
+ -8.89765453338623
+ ],
+ [
+ "▁health",
+ -8.89908504486084
+ ],
+ [
+ "▁special",
+ -8.90555477142334
+ ],
+ [
+ ".\"",
+ -8.907710075378418
+ ],
+ [
+ "1",
+ -8.907852172851562
+ ],
+ [
+ "den",
+ -8.908616065979004
+ ],
+ [
+ "▁state",
+ -8.909355163574219
+ ],
+ [
+ "▁open",
+ -8.91019058227539
+ ],
+ [
+ "▁money",
+ -8.91053581237793
+ ],
+ [
+ "▁again",
+ -8.913084983825684
+ ],
+ [
+ "▁food",
+ -8.913167953491211
+ ],
+ [
+ "▁page",
+ -8.914595603942871
+ ],
+ [
+ "▁together",
+ -8.91628360748291
+ ],
+ [
+ "age",
+ -8.919108390808105
+ ],
+ [
+ "▁qu",
+ -8.921928405761719
+ ],
+ [
+ "hat",
+ -8.922386169433594
+ ],
+ [
+ "▁ver",
+ -8.926993370056152
+ ],
+ [
+ "▁W",
+ -8.927785873413086
+ ],
+ [
+ "▁away",
+ -8.928759574890137
+ ],
+ [
+ "▁wird",
+ -8.931641578674316
+ ],
+ [
+ "▁until",
+ -8.934249877929688
+ ],
+ [
+ "V",
+ -8.934935569763184
+ ],
+ [
+ "▁pre",
+ -8.935851097106934
+ ],
+ [
+ "▁One",
+ -8.936429977416992
+ ],
+ [
+ "▁product",
+ -8.936561584472656
+ ],
+ [
+ "▁often",
+ -8.939326286315918
+ ],
+ [
+ "▁wir",
+ -8.944111824035645
+ ],
+ [
+ "▁nach",
+ -8.945127487182617
+ ],
+ [
+ "▁include",
+ -8.946555137634277
+ ],
+ [
+ "▁um",
+ -8.948204040527344
+ ],
+ [
+ "▁room",
+ -8.953709602355957
+ ],
+ [
+ "▁group",
+ -8.953767776489258
+ ],
+ [
+ "▁name",
+ -8.954949378967285
+ ],
+ [
+ "ce",
+ -8.955448150634766
+ ],
+ [
+ "H",
+ -8.956180572509766
+ ],
+ [
+ "N",
+ -8.958139419555664
+ ],
+ [
+ "▁person",
+ -8.958183288574219
+ ],
+ [
+ "▁social",
+ -8.958606719970703
+ ],
+ [
+ "▁list",
+ -8.963666915893555
+ ],
+ [
+ "▁How",
+ -8.964127540588379
+ ],
+ [
+ "▁why",
+ -8.96571159362793
+ ],
+ [
+ "▁community",
+ -8.965995788574219
+ ],
+ [
+ "▁contact",
+ -8.973031044006348
+ ],
+ [
+ "",
+ -8.9755859375
+ ],
+ [
+ "▁co",
+ -8.979683876037598
+ ],
+ [
+ "▁play",
+ -8.983960151672363
+ ],
+ [
+ "▁having",
+ -8.984169960021973
+ ],
+ [
+ "▁power",
+ -8.986917495727539
+ ],
+ [
+ "▁call",
+ -8.991690635681152
+ ],
+ [
+ "▁against",
+ -8.991816520690918
+ ],
+ [
+ "▁become",
+ -8.997780799865723
+ ],
+ [
+ "▁cost",
+ -9.003793716430664
+ ],
+ [
+ "▁V",
+ -9.004593849182129
+ ],
+ [
+ "▁research",
+ -9.006913185119629
+ ],
+ [
+ "▁12",
+ -9.007307052612305
+ ],
+ [
+ "▁wie",
+ -9.008277893066406
+ ],
+ [
+ "der",
+ -9.008386611938477
+ ],
+ [
+ "▁thing",
+ -9.014028549194336
+ ],
+ [
+ "▁along",
+ -9.017301559448242
+ ],
+ [
+ "4",
+ -9.017330169677734
+ ],
+ [
+ "▁access",
+ -9.020391464233398
+ ],
+ [
+ "▁level",
+ -9.020505905151367
+ ],
+ [
+ "▁price",
+ -9.022817611694336
+ ],
+ [
+ "▁einen",
+ -9.023714065551758
+ ],
+ [
+ "▁side",
+ -9.026359558105469
+ ],
+ [
+ "▁Un",
+ -9.026851654052734
+ ],
+ [
+ "▁means",
+ -9.030416488647461
+ ],
+ [
+ "(",
+ -9.032341957092285
+ ],
+ [
+ "▁big",
+ -9.034374237060547
+ ],
+ [
+ "▁God",
+ -9.036499977111816
+ ],
+ [
+ "▁dass",
+ -9.037314414978027
+ ],
+ [
+ "im",
+ -9.037374496459961
+ ],
+ [
+ "▁30",
+ -9.037432670593262
+ ],
+ [
+ "▁event",
+ -9.041665077209473
+ ],
+ [
+ "▁development",
+ -9.042060852050781
+ ],
+ [
+ "▁form",
+ -9.04226303100586
+ ],
+ [
+ "▁read",
+ -9.042579650878906
+ ],
+ [
+ "▁hand",
+ -9.043194770812988
+ ],
+ [
+ "▁control",
+ -9.04446792602539
+ ],
+ [
+ "▁However",
+ -9.046320915222168
+ ],
+ [
+ "▁done",
+ -9.048060417175293
+ ],
+ [
+ "▁job",
+ -9.051692008972168
+ ],
+ [
+ "▁hard",
+ -9.056619644165039
+ ],
+ [
+ "▁war",
+ -9.057538032531738
+ ],
+ [
+ "▁area",
+ -9.0584135055542
+ ],
+ [
+ "▁add",
+ -9.0586576461792
+ ],
+ [
+ "▁votre",
+ -9.0593900680542
+ ],
+ [
+ "▁live",
+ -9.059494018554688
+ ],
+ [
+ "▁range",
+ -9.060099601745605
+ ],
+ [
+ "▁After",
+ -9.060164451599121
+ ],
+ [
+ "▁Les",
+ -9.060513496398926
+ ],
+ [
+ "▁far",
+ -9.064413070678711
+ ],
+ [
+ "ver",
+ -9.064727783203125
+ ],
+ [
+ "▁old",
+ -9.069576263427734
+ ],
+ [
+ "▁perfect",
+ -9.06976318359375
+ ],
+ [
+ "▁15",
+ -9.070429801940918
+ ],
+ [
+ "▁space",
+ -9.073654174804688
+ ],
+ [
+ "▁house",
+ -9.074068069458008
+ ],
+ [
+ "ine",
+ -9.07408618927002
+ ],
+ [
+ "▁enough",
+ -9.074334144592285
+ ],
+ [
+ "0",
+ -9.075824737548828
+ ],
+ [
+ "▁several",
+ -9.077119827270508
+ ],
+ [
+ "The",
+ -9.081155776977539
+ ],
+ [
+ "mm",
+ -9.085619926452637
+ ],
+ [
+ "▁University",
+ -9.08637523651123
+ ],
+ [
+ "▁diese",
+ -9.087566375732422
+ ],
+ [
+ "▁Co",
+ -9.088335990905762
+ ],
+ [
+ "▁comes",
+ -9.088497161865234
+ ],
+ [
+ "▁across",
+ -9.088857650756836
+ ],
+ [
+ "▁already",
+ -9.090097427368164
+ ],
+ [
+ ",”",
+ -9.090341567993164
+ ],
+ [
+ "▁body",
+ -9.09276294708252
+ ],
+ [
+ "▁Das",
+ -9.094594955444336
+ ],
+ [
+ "▁einer",
+ -9.095956802368164
+ ],
+ [
+ "▁left",
+ -9.09921646118164
+ ],
+ [
+ "▁future",
+ -9.105711936950684
+ ],
+ [
+ "▁times",
+ -9.106670379638672
+ ],
+ [
+ "▁dar",
+ -9.109651565551758
+ ],
+ [
+ "▁simple",
+ -9.110408782958984
+ ],
+ [
+ "ry",
+ -9.112407684326172
+ ],
+ [
+ "▁getting",
+ -9.113155364990234
+ ],
+ [
+ "▁try",
+ -9.115362167358398
+ ],
+ [
+ "ți",
+ -9.116897583007812
+ ],
+ [
+ "ness",
+ -9.120043754577637
+ ],
+ [
+ "▁makes",
+ -9.120377540588379
+ ],
+ [
+ "▁past",
+ -9.120619773864746
+ ],
+ [
+ "ca",
+ -9.12130069732666
+ ],
+ [
+ "▁light",
+ -9.122207641601562
+ ],
+ [
+ "▁Der",
+ -9.122997283935547
+ ],
+ [
+ "▁run",
+ -9.125843048095703
+ ],
+ [
+ "▁four",
+ -9.126943588256836
+ ],
+ [
+ "ance",
+ -9.130500793457031
+ ],
+ [
+ "▁ever",
+ -9.131503105163574
+ ],
+ [
+ "▁einem",
+ -9.131816864013672
+ ],
+ [
+ "▁below",
+ -9.133723258972168
+ ],
+ [
+ "O",
+ -9.134073257446289
+ ],
+ [
+ "▁9",
+ -9.137282371520996
+ ],
+ [
+ "▁learn",
+ -9.14004135131836
+ ],
+ [
+ "out",
+ -9.140358924865723
+ ],
+ [
+ "▁video",
+ -9.143178939819336
+ ],
+ [
+ "▁etc",
+ -9.146929740905762
+ ],
+ [
+ "▁«",
+ -9.148795127868652
+ ],
+ [
+ "▁zum",
+ -9.149712562561035
+ ],
+ [
+ "▁kann",
+ -9.1504487991333
+ ],
+ [
+ "▁minutes",
+ -9.151180267333984
+ ],
+ [
+ "▁example",
+ -9.154194831848145
+ ],
+ [
+ "▁nous",
+ -9.154619216918945
+ ],
+ [
+ "▁Se",
+ -9.157441139221191
+ ],
+ [
+ "▁sie",
+ -9.159955024719238
+ ],
+ [
+ "▁industry",
+ -9.161614418029785
+ ],
+ [
+ "▁problem",
+ -9.162016868591309
+ ],
+ [
+ "J",
+ -9.162480354309082
+ ],
+ [
+ "▁country",
+ -9.163366317749023
+ ],
+ [
+ "▁fact",
+ -9.164189338684082
+ ],
+ [
+ "▁type",
+ -9.164190292358398
+ ],
+ [
+ "ner",
+ -9.164238929748535
+ ],
+ [
+ "▁companies",
+ -9.165864944458008
+ ],
+ [
+ "▁line",
+ -9.169849395751953
+ ],
+ [
+ "▁city",
+ -9.172713279724121
+ ],
+ [
+ "▁check",
+ -9.173710823059082
+ ],
+ [
+ "▁doing",
+ -9.174406051635742
+ ],
+ [
+ "elle",
+ -9.175037384033203
+ ],
+ [
+ "▁fun",
+ -9.176549911499023
+ ],
+ [
+ "▁En",
+ -9.177546501159668
+ ],
+ [
+ "▁Your",
+ -9.178601264953613
+ ],
+ [
+ "ling",
+ -9.181450843811035
+ ],
+ [
+ "▁share",
+ -9.18185806274414
+ ],
+ [
+ "ile",
+ -9.182005882263184
+ ],
+ [
+ "▁actually",
+ -9.187544822692871
+ ],
+ [
+ "▁value",
+ -9.187751770019531
+ ],
+ [
+ "zi",
+ -9.188661575317383
+ ],
+ [
+ "▁ab",
+ -9.1898832321167
+ ],
+ [
+ "▁offers",
+ -9.1905517578125
+ ],
+ [
+ "▁less",
+ -9.190573692321777
+ ],
+ [
+ "▁night",
+ -9.193560600280762
+ ],
+ [
+ "▁Dr",
+ -9.19518756866455
+ ],
+ [
+ "▁started",
+ -9.195454597473145
+ ],
+ [
+ "▁least",
+ -9.198020935058594
+ ],
+ [
+ "▁short",
+ -9.198562622070312
+ ],
+ [
+ "▁main",
+ -9.201143264770508
+ ],
+ [
+ "▁single",
+ -9.202939987182617
+ ],
+ [
+ "▁though",
+ -9.203780174255371
+ ],
+ [
+ "▁prin",
+ -9.203930854797363
+ ],
+ [
+ "time",
+ -9.20531177520752
+ ],
+ [
+ "▁hours",
+ -9.206608772277832
+ ],
+ [
+ "▁others",
+ -9.206849098205566
+ ],
+ [
+ "▁called",
+ -9.20730209350586
+ ],
+ [
+ "▁visit",
+ -9.208869934082031
+ ],
+ [
+ "▁bit",
+ -9.209009170532227
+ ],
+ [
+ "ée",
+ -9.210821151733398
+ ],
+ [
+ "▁customers",
+ -9.211383819580078
+ ],
+ [
+ "▁music",
+ -9.212000846862793
+ ],
+ [
+ "▁members",
+ -9.217191696166992
+ ],
+ [
+ "ies",
+ -9.21743392944336
+ ],
+ [
+ "▁pay",
+ -9.219176292419434
+ ],
+ [
+ "nd",
+ -9.219744682312012
+ ],
+ [
+ "▁once",
+ -9.221125602722168
+ ],
+ [
+ "gen",
+ -9.2217378616333
+ ],
+ [
+ "▁können",
+ -9.222976684570312
+ ],
+ [
+ "▁low",
+ -9.223771095275879
+ ],
+ [
+ "▁durch",
+ -9.227394104003906
+ ],
+ [
+ "▁story",
+ -9.228075981140137
+ ],
+ [
+ "▁understand",
+ -9.22953987121582
+ ],
+ [
+ "“",
+ -9.229856491088867
+ ],
+ [
+ "▁Am",
+ -9.231831550598145
+ ],
+ [
+ "▁didn",
+ -9.234603881835938
+ ],
+ [
+ "▁content",
+ -9.237217903137207
+ ],
+ [
+ "son",
+ -9.24180793762207
+ ],
+ [
+ "▁building",
+ -9.242242813110352
+ ],
+ [
+ "▁result",
+ -9.242605209350586
+ ],
+ [
+ "▁aux",
+ -9.243107795715332
+ ],
+ [
+ "▁complete",
+ -9.244999885559082
+ ],
+ [
+ "▁doesn",
+ -9.24510669708252
+ ],
+ [
+ "▁haben",
+ -9.246070861816406
+ ],
+ [
+ "▁questions",
+ -9.24661636352539
+ ],
+ [
+ "line",
+ -9.247077941894531
+ ],
+ [
+ "▁technology",
+ -9.247429847717285
+ ],
+ [
+ "▁Pro",
+ -9.247976303100586
+ ],
+ [
+ "▁current",
+ -9.248504638671875
+ ],
+ [
+ "▁won",
+ -9.248883247375488
+ ],
+ [
+ "▁let",
+ -9.250710487365723
+ ],
+ [
+ "▁features",
+ -9.251978874206543
+ ],
+ [
+ "▁please",
+ -9.258262634277344
+ ],
+ [
+ "5",
+ -9.258519172668457
+ ],
+ [
+ "▁above",
+ -9.259394645690918
+ ],
+ [
+ "ive",
+ -9.262128829956055
+ ],
+ [
+ "▁management",
+ -9.262394905090332
+ ],
+ [
+ "▁lui",
+ -9.262539863586426
+ ],
+ [
+ "her",
+ -9.263057708740234
+ ],
+ [
+ "▁training",
+ -9.265711784362793
+ ],
+ [
+ "▁everything",
+ -9.2665433883667
+ ],
+ [
+ "▁noch",
+ -9.266846656799316
+ ],
+ [
+ "▁came",
+ -9.267708778381348
+ ],
+ [
+ "▁web",
+ -9.272823333740234
+ ],
+ [
+ "▁ensure",
+ -9.272987365722656
+ ],
+ [
+ "▁months",
+ -9.273130416870117
+ ],
+ [
+ "▁art",
+ -9.27313232421875
+ ],
+ [
+ "▁sub",
+ -9.274359703063965
+ ],
+ [
+ "▁million",
+ -9.274559020996094
+ ],
+ [
+ "▁professional",
+ -9.275035858154297
+ ],
+ [
+ "▁results",
+ -9.278368949890137
+ ],
+ [
+ "▁kind",
+ -9.278395652770996
+ ],
+ [
+ "▁season",
+ -9.279285430908203
+ ],
+ [
+ "▁unique",
+ -9.281067848205566
+ ],
+ [
+ "ze",
+ -9.284360885620117
+ ],
+ [
+ "▁enjoy",
+ -9.28487777709961
+ ],
+ [
+ "▁early",
+ -9.287765502929688
+ ],
+ [
+ "▁major",
+ -9.288202285766602
+ ],
+ [
+ "▁yet",
+ -9.29152774810791
+ ],
+ [
+ "▁Ver",
+ -9.293331146240234
+ ],
+ [
+ "one",
+ -9.296777725219727
+ ],
+ [
+ "▁media",
+ -9.29719352722168
+ ],
+ [
+ "▁[",
+ -9.30095100402832
+ ],
+ [
+ "▁property",
+ -9.302969932556152
+ ],
+ [
+ "▁beautiful",
+ -9.304466247558594
+ ],
+ [
+ "▁given",
+ -9.305286407470703
+ ],
+ [
+ "▁due",
+ -9.306716918945312
+ ],
+ [
+ "▁government",
+ -9.307181358337402
+ ],
+ [
+ "▁nur",
+ -9.30881404876709
+ ],
+ [
+ "▁email",
+ -9.309103012084961
+ ],
+ [
+ "▁total",
+ -9.311080932617188
+ ],
+ [
+ "▁natural",
+ -9.311264038085938
+ ],
+ [
+ "▁test",
+ -9.311450004577637
+ ],
+ [
+ "▁provides",
+ -9.311640739440918
+ ],
+ [
+ "▁various",
+ -9.312631607055664
+ ],
+ [
+ "▁American",
+ -9.315605163574219
+ ],
+ [
+ "▁moment",
+ -9.318109512329102
+ ],
+ [
+ "▁air",
+ -9.318952560424805
+ ],
+ [
+ "▁idea",
+ -9.319236755371094
+ ],
+ [
+ "▁known",
+ -9.319981575012207
+ ],
+ [
+ "▁Il",
+ -9.320504188537598
+ ],
+ [
+ "▁friends",
+ -9.320576667785645
+ ],
+ [
+ "▁final",
+ -9.320919036865234
+ ],
+ [
+ "▁buy",
+ -9.32139778137207
+ ],
+ [
+ "▁specific",
+ -9.322234153747559
+ ],
+ [
+ "▁issues",
+ -9.32454776763916
+ ],
+ [
+ "▁took",
+ -9.325233459472656
+ ],
+ [
+ "▁mind",
+ -9.326258659362793
+ ],
+ [
+ "▁study",
+ -9.32675838470459
+ ],
+ [
+ "▁addition",
+ -9.328418731689453
+ ],
+ [
+ "▁size",
+ -9.332446098327637
+ ],
+ [
+ "▁pro",
+ -9.334047317504883
+ ],
+ [
+ "▁film",
+ -9.33545970916748
+ ],
+ [
+ "▁pot",
+ -9.335636138916016
+ ],
+ [
+ "▁thought",
+ -9.338120460510254
+ ],
+ [
+ "▁tell",
+ -9.33890438079834
+ ],
+ [
+ "▁While",
+ -9.339675903320312
+ ],
+ [
+ "▁head",
+ -9.339983940124512
+ ],
+ [
+ "▁clients",
+ -9.340429306030273
+ ],
+ [
+ "▁performance",
+ -9.346199989318848
+ ],
+ [
+ "▁question",
+ -9.346835136413574
+ ],
+ [
+ "▁whether",
+ -9.347925186157227
+ ],
+ [
+ "▁certain",
+ -9.34826946258545
+ ],
+ [
+ "▁model",
+ -9.348764419555664
+ ],
+ [
+ "▁following",
+ -9.350926399230957
+ ],
+ [
+ "▁energy",
+ -9.354207992553711
+ ],
+ [
+ "▁office",
+ -9.354207992553711
+ ],
+ [
+ "▁whole",
+ -9.356687545776367
+ ],
+ [
+ "▁bring",
+ -9.356956481933594
+ ],
+ [
+ "▁required",
+ -9.35726261138916
+ ],
+ [
+ "ţi",
+ -9.358223915100098
+ ],
+ [
+ "▁date",
+ -9.358695030212402
+ ],
+ [
+ "_",
+ -9.358983039855957
+ ],
+ [
+ "que",
+ -9.359789848327637
+ ],
+ [
+ "▁da",
+ -9.360264778137207
+ ],
+ [
+ "▁US",
+ -9.36120319366455
+ ],
+ [
+ "▁taking",
+ -9.36143684387207
+ ],
+ [
+ "go",
+ -9.362788200378418
+ ],
+ [
+ "▁living",
+ -9.36341667175293
+ ],
+ [
+ "▁someone",
+ -9.363489151000977
+ ],
+ [
+ "▁heart",
+ -9.365120887756348
+ ],
+ [
+ "▁key",
+ -9.365775108337402
+ ],
+ [
+ "▁areas",
+ -9.366238594055176
+ ],
+ [
+ "▁says",
+ -9.367013931274414
+ ],
+ [
+ "▁2018",
+ -9.369132041931152
+ ],
+ [
+ "▁month",
+ -9.37012767791748
+ ],
+ [
+ "▁Er",
+ -9.371354103088379
+ ],
+ [
+ "ste",
+ -9.375077247619629
+ ],
+ [
+ "▁11",
+ -9.375179290771484
+ ],
+ [
+ "▁front",
+ -9.37528133392334
+ ],
+ [
+ "▁Now",
+ -9.37669563293457
+ ],
+ [
+ "▁class",
+ -9.376946449279785
+ ],
+ [
+ "▁choose",
+ -9.377082824707031
+ ],
+ [
+ "pe",
+ -9.37808609008789
+ ],
+ [
+ "▁further",
+ -9.379021644592285
+ ],
+ [
+ "▁believe",
+ -9.37936019897461
+ ],
+ [
+ "of",
+ -9.379590034484863
+ ],
+ [
+ "▁among",
+ -9.380990982055664
+ ],
+ [
+ "sch",
+ -9.381686210632324
+ ],
+ [
+ "▁child",
+ -9.382609367370605
+ ],
+ [
+ "▁aber",
+ -9.38376235961914
+ ],
+ [
+ "▁Please",
+ -9.386269569396973
+ ],
+ [
+ "rea",
+ -9.387248992919922
+ ],
+ [
+ "▁later",
+ -9.387272834777832
+ ],
+ [
+ "▁amount",
+ -9.388760566711426
+ ],
+ [
+ "ice",
+ -9.390128135681152
+ ],
+ [
+ "▁National",
+ -9.390177726745605
+ ],
+ [
+ "▁style",
+ -9.390748977661133
+ ],
+ [
+ "▁tout",
+ -9.391490936279297
+ ],
+ [
+ "▁staff",
+ -9.392939567565918
+ ],
+ [
+ "▁white",
+ -9.397933959960938
+ ],
+ [
+ "▁ge",
+ -9.399179458618164
+ ],
+ [
+ "▁five",
+ -9.400984764099121
+ ],
+ [
+ "▁blog",
+ -9.40109920501709
+ ],
+ [
+ "▁designed",
+ -9.40125846862793
+ ],
+ [
+ "▁went",
+ -9.402216911315918
+ ],
+ [
+ "▁Da",
+ -9.40268611907959
+ ],
+ [
+ "▁general",
+ -9.403801918029785
+ ],
+ [
+ "▁rest",
+ -9.403874397277832
+ ],
+ [
+ "▁zur",
+ -9.40579891204834
+ ],
+ [
+ "▁quite",
+ -9.405948638916016
+ ],
+ [
+ "per",
+ -9.40687084197998
+ ],
+ [
+ "▁customer",
+ -9.408379554748535
+ ],
+ [
+ "▁close",
+ -9.408747673034668
+ ],
+ [
+ "▁Some",
+ -9.41054630279541
+ ],
+ [
+ "▁women",
+ -9.41075611114502
+ ],
+ [
+ "▁move",
+ -9.410761833190918
+ ],
+ [
+ "▁software",
+ -9.411357879638672
+ ],
+ [
+ "▁Ein",
+ -9.413651466369629
+ ],
+ [
+ "▁Ab",
+ -9.413823127746582
+ ],
+ [
+ "▁history",
+ -9.413864135742188
+ ],
+ [
+ "▁either",
+ -9.41564655303955
+ ],
+ [
+ "▁seen",
+ -9.417396545410156
+ ],
+ [
+ "▁card",
+ -9.419726371765137
+ ],
+ [
+ "▁City",
+ -9.421541213989258
+ ],
+ [
+ "▁hope",
+ -9.421769142150879
+ ],
+ [
+ "▁16",
+ -9.422072410583496
+ ],
+ [
+ "és",
+ -9.422825813293457
+ ],
+ [
+ "va",
+ -9.423294067382812
+ ],
+ [
+ "▁Al",
+ -9.423827171325684
+ ],
+ [
+ "▁especially",
+ -9.424827575683594
+ ],
+ [
+ "▁view",
+ -9.426136016845703
+ ],
+ [
+ "men",
+ -9.427363395690918
+ ],
+ [
+ "▁account",
+ -9.427489280700684
+ ],
+ [
+ "▁needed",
+ -9.429777145385742
+ ],
+ [
+ "▁United",
+ -9.429789543151855
+ ],
+ [
+ "]",
+ -9.432387351989746
+ ],
+ [
+ "▁yourself",
+ -9.432788848876953
+ ],
+ [
+ "▁100",
+ -9.433059692382812
+ ],
+ [
+ "▁receive",
+ -9.433417320251465
+ ],
+ [
+ "▁ideas",
+ -9.43369197845459
+ ],
+ [
+ "▁writing",
+ -9.434585571289062
+ ],
+ [
+ "▁simply",
+ -9.434741973876953
+ ],
+ [
+ "▁present",
+ -9.435087203979492
+ ],
+ [
+ "▁continue",
+ -9.436107635498047
+ ],
+ [
+ "▁application",
+ -9.44115161895752
+ ],
+ [
+ "▁build",
+ -9.44187068939209
+ ],
+ [
+ "▁turn",
+ -9.44249439239502
+ ],
+ [
+ "ated",
+ -9.442923545837402
+ ],
+ [
+ "▁everyone",
+ -9.443060874938965
+ ],
+ [
+ "cette",
+ -9.443114280700684
+ ],
+ [
+ "▁bien",
+ -9.444964408874512
+ ],
+ [
+ "less",
+ -9.445222854614258
+ ],
+ [
+ "▁Si",
+ -9.445359230041504
+ ],
+ [
+ "▁original",
+ -9.446867942810059
+ ],
+ [
+ "8",
+ -9.44794750213623
+ ],
+ [
+ "▁individual",
+ -9.448895454406738
+ ],
+ [
+ "tre",
+ -9.449433326721191
+ ],
+ [
+ "▁works",
+ -9.45171070098877
+ ],
+ [
+ "▁options",
+ -9.451821327209473
+ ],
+ [
+ "▁May",
+ -9.454456329345703
+ ],
+ [
+ "▁Not",
+ -9.454940795898438
+ ],
+ [
+ "▁report",
+ -9.455467224121094
+ ],
+ [
+ "mer",
+ -9.457239151000977
+ ],
+ [
+ "▁human",
+ -9.459118843078613
+ ],
+ [
+ "▁provided",
+ -9.459603309631348
+ ],
+ [
+ "▁By",
+ -9.460925102233887
+ ],
+ [
+ "▁series",
+ -9.462006568908691
+ ],
+ [
+ "7",
+ -9.46226692199707
+ ],
+ [
+ "▁modern",
+ -9.463875770568848
+ ],
+ [
+ "▁meet",
+ -9.463921546936035
+ ],
+ [
+ "▁50",
+ -9.464119911193848
+ ],
+ [
+ "▁25",
+ -9.46969985961914
+ ],
+ [
+ "▁color",
+ -9.470091819763184
+ ],
+ [
+ "▁download",
+ -9.470109939575195
+ ],
+ [
+ "▁Here",
+ -9.471144676208496
+ ],
+ [
+ "6",
+ -9.471323013305664
+ ],
+ [
+ "▁poate",
+ -9.471449851989746
+ ],
+ [
+ "▁În",
+ -9.472321510314941
+ ],
+ [
+ "▁phone",
+ -9.473695755004883
+ ],
+ [
+ "▁likely",
+ -9.474374771118164
+ ],
+ [
+ "▁table",
+ -9.476469993591309
+ ],
+ [
+ "▁ma",
+ -9.476551055908203
+ ],
+ [
+ "▁Or",
+ -9.479181289672852
+ ],
+ [
+ "Z",
+ -9.48026180267334
+ ],
+ [
+ "▁19",
+ -9.482215881347656
+ ],
+ [
+ "▁insurance",
+ -9.482544898986816
+ ],
+ [
+ "▁anything",
+ -9.483808517456055
+ ],
+ [
+ "▁search",
+ -9.485033988952637
+ ],
+ [
+ "▁Ge",
+ -9.48520565032959
+ ],
+ [
+ "▁issue",
+ -9.485564231872559
+ ],
+ [
+ "▁includes",
+ -9.485688209533691
+ ],
+ [
+ "▁clear",
+ -9.487342834472656
+ ],
+ [
+ "les",
+ -9.488021850585938
+ ],
+ [
+ "▁almost",
+ -9.488259315490723
+ ],
+ [
+ "ilor",
+ -9.48935317993164
+ ],
+ [
+ "▁14",
+ -9.490717887878418
+ ],
+ [
+ "by",
+ -9.494056701660156
+ ],
+ [
+ "▁Du",
+ -9.49624252319336
+ ],
+ [
+ "▁mais",
+ -9.497303009033203
+ ],
+ [
+ "ier",
+ -9.499163627624512
+ ],
+ [
+ "▁law",
+ -9.49924087524414
+ ],
+ [
+ "▁added",
+ -9.500134468078613
+ ],
+ [
+ "▁con",
+ -9.500962257385254
+ ],
+ [
+ ",\"",
+ -9.501530647277832
+ ],
+ [
+ "▁ago",
+ -9.502127647399902
+ ],
+ [
+ "▁His",
+ -9.504697799682617
+ ],
+ [
+ "▁points",
+ -9.504981994628906
+ ],
+ [
+ "▁mult",
+ -9.505581855773926
+ ],
+ [
+ "▁financial",
+ -9.506216049194336
+ ],
+ [
+ "▁problems",
+ -9.506428718566895
+ ],
+ [
+ "▁however",
+ -9.50648307800293
+ ],
+ [
+ "▁events",
+ -9.50675106048584
+ ],
+ [
+ "▁half",
+ -9.507889747619629
+ ],
+ [
+ "ard",
+ -9.511183738708496
+ ],
+ [
+ "▁ask",
+ -9.51156997680664
+ ],
+ [
+ "▁version",
+ -9.511631965637207
+ ],
+ [
+ "end",
+ -9.512478828430176
+ ],
+ [
+ "▁created",
+ -9.512639999389648
+ ],
+ [
+ "▁lead",
+ -9.512917518615723
+ ],
+ [
+ "▁focus",
+ -9.513853073120117
+ ],
+ [
+ "▁increase",
+ -9.515096664428711
+ ],
+ [
+ "ex",
+ -9.515118598937988
+ ],
+ [
+ "▁allow",
+ -9.515798568725586
+ ],
+ [
+ "▁extra",
+ -9.516464233398438
+ ],
+ [
+ "▁24",
+ -9.516692161560059
+ ],
+ [
+ "▁credit",
+ -9.516772270202637
+ ],
+ [
+ "▁production",
+ -9.516801834106445
+ ],
+ [
+ "zu",
+ -9.517256736755371
+ ],
+ [
+ "▁black",
+ -9.51754093170166
+ ],
+ [
+ "▁systems",
+ -9.518040657043457
+ ],
+ [
+ "▁17",
+ -9.518178939819336
+ ],
+ [
+ "▁opportunity",
+ -9.518531799316406
+ ],
+ [
+ "▁bis",
+ -9.519219398498535
+ ],
+ [
+ "▁fast",
+ -9.519807815551758
+ ],
+ [
+ "ring",
+ -9.521166801452637
+ ],
+ [
+ "▁Don",
+ -9.522114753723145
+ ],
+ [
+ "▁via",
+ -9.52242660522461
+ ],
+ [
+ "fer",
+ -9.5225248336792
+ ],
+ [
+ "▁comme",
+ -9.522799491882324
+ ],
+ [
+ "▁popular",
+ -9.523722648620605
+ ],
+ [
+ "▁South",
+ -9.524491310119629
+ ],
+ [
+ "ating",
+ -9.525003433227539
+ ],
+ [
+ "▁State",
+ -9.525198936462402
+ ],
+ [
+ "ator",
+ -9.525679588317871
+ ],
+ [
+ "▁common",
+ -9.525968551635742
+ ],
+ [
+ "con",
+ -9.526727676391602
+ ],
+ [
+ "▁throughout",
+ -9.527557373046875
+ ],
+ [
+ "▁risk",
+ -9.52774715423584
+ ],
+ [
+ "▁young",
+ -9.528532028198242
+ ],
+ [
+ "▁Je",
+ -9.528688430786133
+ ],
+ [
+ "▁image",
+ -9.52928352355957
+ ],
+ [
+ "ha",
+ -9.529376983642578
+ ],
+ [
+ "▁third",
+ -9.529587745666504
+ ],
+ [
+ "▁taken",
+ -9.530049324035645
+ ],
+ [
+ "▁Z",
+ -9.5314302444458
+ ],
+ [
+ "▁dis",
+ -9.5316162109375
+ ],
+ [
+ "▁From",
+ -9.533575057983398
+ ],
+ [
+ "▁details",
+ -9.534862518310547
+ ],
+ [
+ "▁games",
+ -9.53516674041748
+ ],
+ [
+ "▁practice",
+ -9.536040306091309
+ ],
+ [
+ "che",
+ -9.536151885986328
+ ],
+ [
+ "▁security",
+ -9.537364959716797
+ ],
+ [
+ "▁medical",
+ -9.537653923034668
+ ],
+ [
+ "▁learning",
+ -9.537806510925293
+ ],
+ [
+ "▁material",
+ -9.538509368896484
+ ],
+ [
+ "▁international",
+ -9.540703773498535
+ ],
+ [
+ "▁forward",
+ -9.541245460510254
+ ],
+ [
+ "▁paper",
+ -9.541247367858887
+ ],
+ [
+ "▁action",
+ -9.541348457336426
+ ],
+ [
+ "▁file",
+ -9.542378425598145
+ ],
+ [
+ "▁oil",
+ -9.543096542358398
+ ],
+ [
+ "▁self",
+ -9.54377555847168
+ ],
+ [
+ "▁private",
+ -9.545247077941895
+ ],
+ [
+ "▁interest",
+ -9.545559883117676
+ ],
+ [
+ "bar",
+ -9.546065330505371
+ ],
+ [
+ "▁sale",
+ -9.547115325927734
+ ],
+ [
+ "▁stay",
+ -9.547348976135254
+ ],
+ [
+ "ke",
+ -9.548089981079102
+ ],
+ [
+ "▁San",
+ -9.549053192138672
+ ],
+ [
+ "▁matter",
+ -9.549870491027832
+ ],
+ [
+ "▁reason",
+ -9.550254821777344
+ ],
+ [
+ "ted",
+ -9.55147647857666
+ ],
+ [
+ "▁potential",
+ -9.551742553710938
+ ],
+ [
+ "▁brand",
+ -9.552441596984863
+ ],
+ [
+ "▁field",
+ -9.55315113067627
+ ],
+ [
+ "▁treatment",
+ -9.553420066833496
+ ],
+ [
+ "▁period",
+ -9.553516387939453
+ ],
+ [
+ "▁York",
+ -9.553890228271484
+ ],
+ [
+ "▁Park",
+ -9.554738998413086
+ ],
+ [
+ "▁acest",
+ -9.556009292602539
+ ],
+ [
+ "ou",
+ -9.556926727294922
+ ],
+ [
+ "▁Ce",
+ -9.557014465332031
+ ],
+ [
+ "▁ready",
+ -9.558111190795898
+ ],
+ [
+ "▁rather",
+ -9.55860424041748
+ ],
+ [
+ "▁outside",
+ -9.560086250305176
+ ],
+ [
+ "▁standard",
+ -9.560121536254883
+ ],
+ [
+ "▁located",
+ -9.560770034790039
+ ],
+ [
+ "▁marketing",
+ -9.562313079833984
+ ],
+ [
+ "cu",
+ -9.564041137695312
+ ],
+ [
+ "▁Can",
+ -9.564562797546387
+ ],
+ [
+ "▁education",
+ -9.566105842590332
+ ],
+ [
+ "use",
+ -9.566640853881836
+ ],
+ [
+ "▁role",
+ -9.566828727722168
+ ],
+ [
+ "▁men",
+ -9.571505546569824
+ ],
+ [
+ "▁probably",
+ -9.571550369262695
+ ],
+ [
+ "▁store",
+ -9.57221508026123
+ ],
+ [
+ "▁John",
+ -9.572355270385742
+ ],
+ [
+ "▁rate",
+ -9.573956489562988
+ ],
+ [
+ "▁code",
+ -9.573994636535645
+ ],
+ [
+ "▁kids",
+ -9.574408531188965
+ ],
+ [
+ "▁currently",
+ -9.57552719116211
+ ],
+ [
+ "▁near",
+ -9.576475143432617
+ ],
+ [
+ "▁sales",
+ -9.576716423034668
+ ],
+ [
+ "▁usually",
+ -9.577012062072754
+ ],
+ [
+ "▁activities",
+ -9.577242851257324
+ ],
+ [
+ "▁party",
+ -9.577371597290039
+ ],
+ [
+ "▁leur",
+ -9.577434539794922
+ ],
+ [
+ "▁particular",
+ -9.577627182006836
+ ],
+ [
+ "▁mehr",
+ -9.577707290649414
+ ],
+ [
+ "ill",
+ -9.578757286071777
+ ],
+ [
+ "▁percent",
+ -9.579113006591797
+ ],
+ [
+ "▁fait",
+ -9.579537391662598
+ ],
+ [
+ "▁happy",
+ -9.579904556274414
+ ],
+ [
+ "▁inside",
+ -9.58005428314209
+ ],
+ [
+ "▁save",
+ -9.580510139465332
+ ],
+ [
+ "▁skills",
+ -9.580765724182129
+ ],
+ [
+ "▁consider",
+ -9.581025123596191
+ ],
+ [
+ "▁recent",
+ -9.58161735534668
+ ],
+ [
+ "▁strong",
+ -9.581781387329102
+ ],
+ [
+ "▁position",
+ -9.582076072692871
+ ],
+ [
+ "▁knowledge",
+ -9.582303047180176
+ ],
+ [
+ "▁tax",
+ -9.583868980407715
+ ],
+ [
+ "▁users",
+ -9.584261894226074
+ ],
+ [
+ "und",
+ -9.585564613342285
+ ],
+ [
+ "▁coming",
+ -9.585904121398926
+ ],
+ [
+ "▁article",
+ -9.585923194885254
+ ],
+ [
+ "min",
+ -9.586345672607422
+ ],
+ [
+ "▁sein",
+ -9.586555480957031
+ ],
+ [
+ "▁travel",
+ -9.586871147155762
+ ],
+ [
+ "▁changes",
+ -9.58765983581543
+ ],
+ [
+ "▁impact",
+ -9.588181495666504
+ ],
+ [
+ "▁wanted",
+ -9.588460922241211
+ ],
+ [
+ "▁address",
+ -9.5885591506958
+ ],
+ [
+ "▁soon",
+ -9.58873462677002
+ ],
+ [
+ "▁North",
+ -9.588915824890137
+ ],
+ [
+ "ată",
+ -9.589237213134766
+ ],
+ [
+ "▁trying",
+ -9.58985424041748
+ ],
+ [
+ "▁app",
+ -9.590612411499023
+ ],
+ [
+ "▁School",
+ -9.592510223388672
+ ],
+ [
+ "▁Es",
+ -9.592548370361328
+ ],
+ [
+ "we",
+ -9.59261703491211
+ ],
+ [
+ "▁conditions",
+ -9.59292984008789
+ ],
+ [
+ "▁digital",
+ -9.593293190002441
+ ],
+ [
+ "▁similar",
+ -9.594805717468262
+ ],
+ [
+ "▁solution",
+ -9.59514331817627
+ ],
+ [
+ "▁location",
+ -9.595183372497559
+ ],
+ [
+ "▁Of",
+ -9.595418930053711
+ ],
+ [
+ "▁follow",
+ -9.595842361450195
+ ],
+ [
+ "▁red",
+ -9.597526550292969
+ ],
+ [
+ "▁review",
+ -9.599202156066895
+ ],
+ [
+ "▁skin",
+ -9.599575996398926
+ ],
+ [
+ "▁pretty",
+ -9.600369453430176
+ ],
+ [
+ "day",
+ -9.600558280944824
+ ],
+ [
+ "▁dé",
+ -9.602072715759277
+ ],
+ [
+ "▁cause",
+ -9.602169036865234
+ ],
+ [
+ "▁Sa",
+ -9.602463722229004
+ ],
+ [
+ "▁user",
+ -9.602520942687988
+ ],
+ [
+ "▁Man",
+ -9.603377342224121
+ ],
+ [
+ "”.",
+ -9.604146003723145
+ ],
+ [
+ "▁Just",
+ -9.604366302490234
+ ],
+ [
+ "▁faire",
+ -9.604475021362305
+ ],
+ [
+ "▁member",
+ -9.605619430541992
+ ],
+ [
+ "▁iar",
+ -9.606892585754395
+ ],
+ [
+ "▁higher",
+ -9.607715606689453
+ ],
+ [
+ "▁step",
+ -9.607887268066406
+ ],
+ [
+ "▁wide",
+ -9.608185768127441
+ ],
+ [
+ "▁uns",
+ -9.608920097351074
+ ],
+ [
+ "▁World",
+ -9.609135627746582
+ ],
+ [
+ "▁additional",
+ -9.61176586151123
+ ],
+ [
+ "ber",
+ -9.613197326660156
+ ],
+ [
+ "▁easily",
+ -9.613990783691406
+ ],
+ [
+ "▁deal",
+ -9.615070343017578
+ ],
+ [
+ "▁ways",
+ -9.615514755249023
+ ],
+ [
+ "▁mobile",
+ -9.616837501525879
+ ],
+ [
+ "▁national",
+ -9.616913795471191
+ ],
+ [
+ "▁couple",
+ -9.617389678955078
+ ],
+ [
+ "▁ihre",
+ -9.61939811706543
+ ],
+ [
+ "▁choice",
+ -9.619612693786621
+ ],
+ [
+ "for",
+ -9.619686126708984
+ ],
+ [
+ "ous",
+ -9.62070083618164
+ ],
+ [
+ "▁Google",
+ -9.620855331420898
+ ],
+ [
+ "▁environment",
+ -9.622426986694336
+ ],
+ [
+ "urile",
+ -9.623322486877441
+ ],
+ [
+ "▁Center",
+ -9.626680374145508
+ ],
+ [
+ "mp",
+ -9.628592491149902
+ ],
+ [
+ "▁»",
+ -9.629727363586426
+ ],
+ [
+ "qui",
+ -9.630680084228516
+ ],
+ [
+ "▁growth",
+ -9.631048202514648
+ ],
+ [
+ "ler",
+ -9.633174896240234
+ ],
+ [
+ "▁improve",
+ -9.63360595703125
+ ],
+ [
+ "▁items",
+ -9.6336669921875
+ ],
+ [
+ "▁Nu",
+ -9.63393783569336
+ ],
+ [
+ "▁leave",
+ -9.634074211120605
+ ],
+ [
+ "▁true",
+ -9.634805679321289
+ ],
+ [
+ "▁wurde",
+ -9.63487434387207
+ ],
+ [
+ "▁cannot",
+ -9.635004043579102
+ ],
+ [
+ "▁13",
+ -9.635096549987793
+ ],
+ [
+ "▁running",
+ -9.636015892028809
+ ],
+ [
+ "▁anti",
+ -9.636177062988281
+ ],
+ [
+ "▁option",
+ -9.636306762695312
+ ],
+ [
+ "▁reading",
+ -9.63657283782959
+ ],
+ [
+ "▁Car",
+ -9.636698722839355
+ ],
+ [
+ "▁Wir",
+ -9.638110160827637
+ ],
+ [
+ "▁April",
+ -9.63975715637207
+ ],
+ [
+ "▁behind",
+ -9.640642166137695
+ ],
+ [
+ "▁client",
+ -9.640750885009766
+ ],
+ [
+ "▁cover",
+ -9.641012191772461
+ ],
+ [
+ "▁stop",
+ -9.641090393066406
+ ],
+ [
+ "ja",
+ -9.641277313232422
+ ],
+ [
+ "▁built",
+ -9.641307830810547
+ ],
+ [
+ "▁Con",
+ -9.641313552856445
+ ],
+ [
+ "ement",
+ -9.641366004943848
+ ],
+ [
+ "▁projects",
+ -9.641828536987305
+ ],
+ [
+ "▁variety",
+ -9.641840934753418
+ ],
+ [
+ "▁Ihre",
+ -9.642666816711426
+ ],
+ [
+ "ș",
+ -9.64302921295166
+ ],
+ [
+ "▁unter",
+ -9.64385986328125
+ ],
+ [
+ "▁longer",
+ -9.646577835083008
+ ],
+ [
+ "year",
+ -9.647161483764648
+ ],
+ [
+ "▁photo",
+ -9.648370742797852
+ ],
+ [
+ "▁Also",
+ -9.64933967590332
+ ],
+ [
+ "▁received",
+ -9.651098251342773
+ ],
+ [
+ "▁return",
+ -9.652676582336426
+ ],
+ [
+ "00",
+ -9.653081893920898
+ ],
+ [
+ "▁bar",
+ -9.653343200683594
+ ],
+ [
+ "ary",
+ -9.654427528381348
+ ],
+ [
+ "elor",
+ -9.655137062072754
+ ],
+ [
+ "▁Home",
+ -9.656189918518066
+ ],
+ [
+ "our",
+ -9.656298637390137
+ ],
+ [
+ "▁Me",
+ -9.65771198272705
+ ],
+ [
+ "▁held",
+ -9.659111022949219
+ ],
+ [
+ "▁click",
+ -9.66014289855957
+ ],
+ [
+ "▁ex",
+ -9.660178184509277
+ ],
+ [
+ "▁cum",
+ -9.661561965942383
+ ],
+ [
+ "▁takes",
+ -9.66395378112793
+ ],
+ [
+ "▁computer",
+ -9.665796279907227
+ ],
+ [
+ "▁told",
+ -9.668192863464355
+ ],
+ [
+ "+",
+ -9.670648574829102
+ ],
+ [
+ "▁patients",
+ -9.670809745788574
+ ],
+ [
+ "ting",
+ -9.672165870666504
+ ],
+ [
+ "▁direct",
+ -9.672248840332031
+ ],
+ [
+ "▁quickly",
+ -9.672410011291504
+ ],
+ [
+ "tic",
+ -9.672877311706543
+ ],
+ [
+ "▁vom",
+ -9.673723220825195
+ ],
+ [
+ "▁di",
+ -9.67381477355957
+ ],
+ [
+ "▁kitchen",
+ -9.674022674560547
+ ],
+ [
+ "▁network",
+ -9.675640106201172
+ ],
+ [
+ "▁2015",
+ -9.676688194274902
+ ],
+ [
+ "▁effective",
+ -9.677227020263672
+ ],
+ [
+ "▁collection",
+ -9.677703857421875
+ ],
+ [
+ "▁2017",
+ -9.677751541137695
+ ],
+ [
+ "▁words",
+ -9.678145408630371
+ ],
+ [
+ "▁cele",
+ -9.678857803344727
+ ],
+ [
+ "▁student",
+ -9.678862571716309
+ ],
+ [
+ "▁amazing",
+ -9.678932189941406
+ ],
+ [
+ "eur",
+ -9.680419921875
+ ],
+ [
+ ".”",
+ -9.68227481842041
+ ],
+ [
+ "▁ale",
+ -9.682716369628906
+ ],
+ [
+ "”,",
+ -9.68414306640625
+ ],
+ [
+ "▁purchase",
+ -9.684350967407227
+ ],
+ [
+ "▁mean",
+ -9.68477725982666
+ ],
+ [
+ "▁West",
+ -9.686846733093262
+ ],
+ [
+ "▁nice",
+ -9.6889066696167
+ ],
+ [
+ "▁age",
+ -9.689131736755371
+ ],
+ [
+ "▁base",
+ -9.68923568725586
+ ],
+ [
+ "▁summer",
+ -9.68928337097168
+ ],
+ [
+ "▁multi",
+ -9.689496994018555
+ ],
+ [
+ "▁allows",
+ -9.689573287963867
+ ],
+ [
+ "▁latest",
+ -9.689604759216309
+ ],
+ [
+ "▁global",
+ -9.68992805480957
+ ],
+ [
+ "▁chance",
+ -9.690792083740234
+ ],
+ [
+ "▁sense",
+ -9.690872192382812
+ ],
+ [
+ "ieren",
+ -9.692789077758789
+ ],
+ [
+ "▁difficult",
+ -9.693133354187012
+ ],
+ [
+ "ité",
+ -9.694750785827637
+ ],
+ [
+ "ka",
+ -9.694792747497559
+ ],
+ [
+ "du",
+ -9.69483757019043
+ ],
+ [
+ "▁providing",
+ -9.695744514465332
+ ],
+ [
+ "▁Art",
+ -9.696940422058105
+ ],
+ [
+ "▁drive",
+ -9.698554992675781
+ ],
+ [
+ "▁Go",
+ -9.698877334594727
+ ],
+ [
+ "▁très",
+ -9.699414253234863
+ ],
+ [
+ "U",
+ -9.699579238891602
+ ],
+ [
+ "▁Pre",
+ -9.699846267700195
+ ],
+ [
+ "▁shows",
+ -9.700040817260742
+ ],
+ [
+ "▁hair",
+ -9.701324462890625
+ ],
+ [
+ "▁success",
+ -9.701513290405273
+ ],
+ [
+ "▁UK",
+ -9.703169822692871
+ ],
+ [
+ "red",
+ -9.703241348266602
+ ],
+ [
+ "ü",
+ -9.703370094299316
+ ],
+ [
+ "ish",
+ -9.703631401062012
+ ],
+ [
+ "▁weeks",
+ -9.704839706420898
+ ],
+ [
+ "▁solutions",
+ -9.7055025100708
+ ],
+ [
+ "▁Pe",
+ -9.7057523727417
+ ],
+ [
+ "▁equipment",
+ -9.706141471862793
+ ],
+ [
+ "și",
+ -9.706482887268066
+ ],
+ [
+ "▁worked",
+ -9.707073211669922
+ ],
+ [
+ "\".",
+ -9.708627700805664
+ ],
+ [
+ "▁legal",
+ -9.708720207214355
+ ],
+ [
+ "▁bad",
+ -9.70892333984375
+ ],
+ [
+ "▁40",
+ -9.709561347961426
+ ],
+ [
+ "▁Internet",
+ -9.709798812866211
+ ],
+ [
+ "▁included",
+ -9.709976196289062
+ ],
+ [
+ "▁upon",
+ -9.710977554321289
+ ],
+ [
+ "▁excellent",
+ -9.71106243133545
+ ],
+ [
+ "▁goal",
+ -9.71130084991455
+ ],
+ [
+ "▁El",
+ -9.711408615112305
+ ],
+ [
+ "▁Mo",
+ -9.711703300476074
+ ],
+ [
+ "▁policy",
+ -9.71319580078125
+ ],
+ [
+ "▁aussi",
+ -9.713537216186523
+ ],
+ [
+ "▁weight",
+ -9.713687896728516
+ ],
+ [
+ "ici",
+ -9.715133666992188
+ ],
+ [
+ "▁approach",
+ -9.715584754943848
+ ],
+ [
+ "▁six",
+ -9.71579647064209
+ ],
+ [
+ "▁entire",
+ -9.715911865234375
+ ],
+ [
+ "9",
+ -9.71633529663086
+ ],
+ [
+ "▁send",
+ -9.716832160949707
+ ],
+ [
+ "▁1.",
+ -9.718971252441406
+ ],
+ [
+ "▁wenn",
+ -9.719056129455566
+ ],
+ [
+ "▁photos",
+ -9.71993637084961
+ ],
+ [
+ "://",
+ -9.721014022827148
+ ],
+ [
+ "ger",
+ -9.72281551361084
+ ],
+ [
+ "▁favorite",
+ -9.723104476928711
+ ],
+ [
+ "ley",
+ -9.723477363586426
+ ],
+ [
+ "▁else",
+ -9.72463321685791
+ ],
+ [
+ "▁types",
+ -9.72468376159668
+ ],
+ [
+ "▁link",
+ -9.725333213806152
+ ],
+ [
+ "▁recently",
+ -9.72584056854248
+ ],
+ [
+ "▁Mit",
+ -9.72631549835205
+ ],
+ [
+ "▁hot",
+ -9.726548194885254
+ ],
+ [
+ "tra",
+ -9.726597785949707
+ ],
+ [
+ "ş",
+ -9.727307319641113
+ ],
+ [
+ "▁according",
+ -9.728511810302734
+ ],
+ [
+ "▁necessary",
+ -9.728511810302734
+ ],
+ [
+ "▁multiple",
+ -9.729269027709961
+ ],
+ [
+ "▁Im",
+ -9.729510307312012
+ ],
+ [
+ "▁sehr",
+ -9.729660034179688
+ ],
+ [
+ "▁sign",
+ -9.732263565063477
+ ],
+ [
+ "▁anyone",
+ -9.73283576965332
+ ],
+ [
+ "▁land",
+ -9.733613014221191
+ ],
+ [
+ "▁States",
+ -9.734037399291992
+ ],
+ [
+ "▁unsere",
+ -9.734119415283203
+ ],
+ [
+ "ées",
+ -9.734639167785645
+ ],
+ [
+ "We",
+ -9.735671043395996
+ ],
+ [
+ "▁nothing",
+ -9.735845565795898
+ ],
+ [
+ "▁commercial",
+ -9.736858367919922
+ ],
+ [
+ "ful",
+ -9.737265586853027
+ ],
+ [
+ "▁seems",
+ -9.739325523376465
+ ],
+ [
+ "▁International",
+ -9.740097045898438
+ ],
+ [
+ "▁March",
+ -9.74163818359375
+ ],
+ [
+ "▁Thanks",
+ -9.743307113647461
+ ],
+ [
+ "▁County",
+ -9.74365234375
+ ],
+ [
+ "▁books",
+ -9.744638442993164
+ ],
+ [
+ "▁Ca",
+ -9.7451753616333
+ ],
+ [
+ "▁mi",
+ -9.746304512023926
+ ],
+ [
+ "▁meeting",
+ -9.746662139892578
+ ],
+ [
+ "▁tools",
+ -9.747593879699707
+ ],
+ [
+ "▁cut",
+ -9.747650146484375
+ ],
+ [
+ "▁related",
+ -9.74765682220459
+ ],
+ [
+ "▁lives",
+ -9.748003005981445
+ ],
+ [
+ "way",
+ -9.748501777648926
+ ],
+ [
+ "▁develop",
+ -9.748651504516602
+ ],
+ [
+ "▁sound",
+ -9.748723983764648
+ ],
+ [
+ "▁safe",
+ -9.748950958251953
+ ],
+ [
+ "▁Her",
+ -9.74937629699707
+ ],
+ [
+ "▁average",
+ -9.751277923583984
+ ],
+ [
+ "▁clean",
+ -9.75174331665039
+ ],
+ [
+ "▁talk",
+ -9.752362251281738
+ ],
+ [
+ "▁peut",
+ -9.75241756439209
+ ],
+ [
+ "▁dann",
+ -9.752546310424805
+ ],
+ [
+ "▁terms",
+ -9.753265380859375
+ ],
+ [
+ "▁foarte",
+ -9.753512382507324
+ ],
+ [
+ "▁super",
+ -9.754284858703613
+ ],
+ [
+ "▁programs",
+ -9.754853248596191
+ ],
+ [
+ "▁decision",
+ -9.75540828704834
+ ],
+ [
+ "▁costs",
+ -9.756058692932129
+ ],
+ [
+ "▁être",
+ -9.756291389465332
+ ],
+ [
+ "▁2019",
+ -9.757674217224121
+ ],
+ [
+ "led",
+ -9.759482383728027
+ ],
+ [
+ "▁parents",
+ -9.759617805480957
+ ],
+ [
+ "▁Mr",
+ -9.761702537536621
+ ],
+ [
+ "▁lower",
+ -9.762362480163574
+ ],
+ [
+ "▁door",
+ -9.762978553771973
+ ],
+ [
+ "▁été",
+ -9.763933181762695
+ ],
+ [
+ "▁box",
+ -9.764954566955566
+ ],
+ [
+ "▁record",
+ -9.765517234802246
+ ],
+ [
+ "▁win",
+ -9.765650749206543
+ ],
+ [
+ "ster",
+ -9.766402244567871
+ ],
+ [
+ "▁America",
+ -9.766748428344727
+ ],
+ [
+ "▁immer",
+ -9.768763542175293
+ ],
+ [
+ "▁road",
+ -9.76996898651123
+ ],
+ [
+ "▁leading",
+ -9.772759437561035
+ ],
+ [
+ "▁section",
+ -9.772838592529297
+ ],
+ [
+ "▁Facebook",
+ -9.772990226745605
+ ],
+ [
+ "▁Most",
+ -9.7738676071167
+ ],
+ [
+ "iert",
+ -9.77435302734375
+ ],
+ [
+ "▁morning",
+ -9.774497032165527
+ ],
+ [
+ "▁asked",
+ -9.775190353393555
+ ],
+ [
+ "▁involved",
+ -9.77551555633545
+ ],
+ [
+ "▁hier",
+ -9.777607917785645
+ ],
+ [
+ "▁images",
+ -9.77821159362793
+ ],
+ [
+ "▁House",
+ -9.778263092041016
+ ],
+ [
+ "▁highly",
+ -9.780763626098633
+ ],
+ [
+ "▁Bar",
+ -9.781620979309082
+ ],
+ [
+ "▁Service",
+ -9.782510757446289
+ ],
+ [
+ "▁attention",
+ -9.784318923950195
+ ],
+ [
+ "▁normal",
+ -9.784571647644043
+ ],
+ [
+ "▁plans",
+ -9.785883903503418
+ ],
+ [
+ "▁source",
+ -9.785930633544922
+ ],
+ [
+ "▁Aus",
+ -9.788092613220215
+ ],
+ [
+ "▁benefits",
+ -9.788655281066895
+ ],
+ [
+ "▁ses",
+ -9.789348602294922
+ ],
+ [
+ "des",
+ -9.789867401123047
+ ],
+ [
+ "▁internet",
+ -9.789949417114258
+ ],
+ [
+ "▁materials",
+ -9.790080070495605
+ ],
+ [
+ "▁même",
+ -9.791318893432617
+ ],
+ [
+ "▁fine",
+ -9.791522026062012
+ ],
+ [
+ "▁fit",
+ -9.792226791381836
+ ],
+ [
+ "▁21",
+ -9.792612075805664
+ ],
+ [
+ "▁itself",
+ -9.793739318847656
+ ],
+ [
+ "▁wieder",
+ -9.793972969055176
+ ],
+ [
+ "▁Many",
+ -9.795313835144043
+ ],
+ [
+ "▁nature",
+ -9.795402526855469
+ ],
+ [
+ "▁pain",
+ -9.795467376708984
+ ],
+ [
+ "▁device",
+ -9.796183586120605
+ ],
+ [
+ "art",
+ -9.796989440917969
+ ],
+ [
+ "pro",
+ -9.7971830368042
+ ],
+ [
+ "▁France",
+ -9.797271728515625
+ ],
+ [
+ "lich",
+ -9.797314643859863
+ ],
+ [
+ "▁2014",
+ -9.799542427062988
+ ],
+ [
+ "▁inter",
+ -9.799964904785156
+ ],
+ [
+ "▁Li",
+ -9.800453186035156
+ ],
+ [
+ "▁career",
+ -9.801136016845703
+ ],
+ [
+ "▁looks",
+ -9.80145263671875
+ ],
+ [
+ "▁ré",
+ -9.802245140075684
+ ],
+ [
+ "▁ability",
+ -9.802556991577148
+ ],
+ [
+ "▁situation",
+ -9.803154945373535
+ ],
+ [
+ "ville",
+ -9.803157806396484
+ ],
+ [
+ "▁2016",
+ -9.80319595336914
+ ],
+ [
+ "tes",
+ -9.803462982177734
+ ],
+ [
+ "▁remember",
+ -9.803879737854004
+ ],
+ [
+ "▁TV",
+ -9.803998947143555
+ ],
+ [
+ "▁levels",
+ -9.805853843688965
+ ],
+ [
+ "▁subject",
+ -9.807723999023438
+ ],
+ [
+ "ally",
+ -9.80844497680664
+ ],
+ [
+ "▁reduce",
+ -9.810232162475586
+ ],
+ [
+ "▁*",
+ -9.8108491897583
+ ],
+ [
+ "▁Day",
+ -9.810867309570312
+ ],
+ [
+ "▁write",
+ -9.812152862548828
+ ],
+ [
+ "▁pick",
+ -9.814252853393555
+ ],
+ [
+ "ence",
+ -9.815399169921875
+ ],
+ [
+ "▁fresh",
+ -9.816520690917969
+ ],
+ [
+ "▁traditional",
+ -9.816662788391113
+ ],
+ [
+ "chi",
+ -9.817692756652832
+ ],
+ [
+ "▁machine",
+ -9.818047523498535
+ ],
+ [
+ "▁resources",
+ -9.819125175476074
+ ],
+ [
+ "â",
+ -9.819502830505371
+ ],
+ [
+ "▁countries",
+ -9.820009231567383
+ ],
+ [
+ "▁Even",
+ -9.820342063903809
+ ],
+ [
+ "▁green",
+ -9.821283340454102
+ ],
+ [
+ "▁Free",
+ -9.821910858154297
+ ],
+ [
+ "▁daily",
+ -9.822112083435059
+ ],
+ [
+ "▁respect",
+ -9.823013305664062
+ ],
+ [
+ "▁instead",
+ -9.823714256286621
+ ],
+ [
+ "▁Once",
+ -9.82418155670166
+ ],
+ [
+ "▁word",
+ -9.824407577514648
+ ],
+ [
+ "▁construction",
+ -9.82489013671875
+ ],
+ [
+ "▁huge",
+ -9.825064659118652
+ ],
+ [
+ "▁feature",
+ -9.825220108032227
+ ],
+ [
+ "▁themselves",
+ -9.826369285583496
+ ],
+ [
+ "▁loss",
+ -9.82919692993164
+ ],
+ [
+ "%",
+ -9.830063819885254
+ ],
+ [
+ "▁safety",
+ -9.830256462097168
+ ],
+ [
+ "▁economic",
+ -9.831406593322754
+ ],
+ [
+ "▁require",
+ -9.831945419311523
+ ],
+ [
+ "30",
+ -9.83255386352539
+ ],
+ [
+ "▁planning",
+ -9.833393096923828
+ ],
+ [
+ "▁mal",
+ -9.834482192993164
+ ],
+ [
+ "▁directly",
+ -9.835214614868164
+ ],
+ [
+ "ure",
+ -9.835719108581543
+ ],
+ [
+ "▁track",
+ -9.835734367370605
+ ],
+ [
+ "▁tool",
+ -9.836135864257812
+ ],
+ [
+ "▁positive",
+ -9.836392402648926
+ ],
+ [
+ "▁piece",
+ -9.837076187133789
+ ],
+ [
+ "▁parts",
+ -9.837140083312988
+ ],
+ [
+ "ang",
+ -9.83740520477295
+ ],
+ [
+ "▁trip",
+ -9.837453842163086
+ ],
+ [
+ "▁organization",
+ -9.837935447692871
+ ],
+ [
+ "▁sites",
+ -9.838274002075195
+ ],
+ [
+ "▁fire",
+ -9.83831787109375
+ ],
+ [
+ "▁China",
+ -9.838876724243164
+ ],
+ [
+ "▁Pour",
+ -9.839289665222168
+ ],
+ [
+ "▁plant",
+ -9.84011459350586
+ ],
+ [
+ "▁board",
+ -9.840341567993164
+ ],
+ [
+ "▁interesting",
+ -9.841227531433105
+ ],
+ [
+ "gar",
+ -9.841713905334473
+ ],
+ [
+ "▁fie",
+ -9.841752052307129
+ ],
+ [
+ "▁late",
+ -9.842166900634766
+ ],
+ [
+ "▁wall",
+ -9.842294692993164
+ ],
+ [
+ "▁walk",
+ -9.842741966247559
+ ],
+ [
+ "ham",
+ -9.843868255615234
+ ],
+ [
+ "▁Ne",
+ -9.845427513122559
+ ],
+ [
+ "▁First",
+ -9.845462799072266
+ ],
+ [
+ "▁double",
+ -9.845701217651367
+ ],
+ [
+ "▁budget",
+ -9.847657203674316
+ ],
+ [
+ "▁cases",
+ -9.847670555114746
+ ],
+ [
+ "cal",
+ -9.849738121032715
+ ],
+ [
+ "old",
+ -9.849796295166016
+ ],
+ [
+ "▁Bo",
+ -9.849822998046875
+ ],
+ [
+ "▁spend",
+ -9.850439071655273
+ ],
+ [
+ "port",
+ -9.850828170776367
+ ],
+ [
+ "▁worth",
+ -9.850934028625488
+ ],
+ [
+ "ique",
+ -9.851308822631836
+ ],
+ [
+ "nes",
+ -9.85190486907959
+ ],
+ [
+ "cul",
+ -9.852272033691406
+ ],
+ [
+ "era",
+ -9.85296630859375
+ ],
+ [
+ "▁text",
+ -9.853032112121582
+ ],
+ [
+ "▁decided",
+ -9.854948997497559
+ ],
+ [
+ "▁floor",
+ -9.855036735534668
+ ],
+ [
+ "▁requirements",
+ -9.85529899597168
+ ],
+ [
+ "▁cel",
+ -9.855361938476562
+ ],
+ [
+ "▁effect",
+ -9.855412483215332
+ ],
+ [
+ "▁gibt",
+ -9.856159210205078
+ ],
+ [
+ "▁news",
+ -9.859238624572754
+ ],
+ [
+ "▁vos",
+ -9.859931945800781
+ ],
+ [
+ "▁players",
+ -9.86057186126709
+ ],
+ [
+ "▁saw",
+ -9.862728118896484
+ ],
+ [
+ "▁auto",
+ -9.863056182861328
+ ],
+ [
+ "▁town",
+ -9.863207817077637
+ ],
+ [
+ "▁myself",
+ -9.864106178283691
+ ],
+ [
+ "▁lost",
+ -9.864988327026367
+ ],
+ [
+ "▁$",
+ -9.865124702453613
+ ],
+ [
+ "▁June",
+ -9.86609172821045
+ ],
+ [
+ "▁significant",
+ -9.866196632385254
+ ],
+ [
+ "▁giving",
+ -9.866230010986328
+ ],
+ [
+ "▁stand",
+ -9.866744041442871
+ ],
+ [
+ "▁stock",
+ -9.867657661437988
+ ],
+ [
+ "▁hold",
+ -9.867766380310059
+ ],
+ [
+ "▁Are",
+ -9.869078636169434
+ ],
+ [
+ "▁shall",
+ -9.86923599243164
+ ],
+ [
+ "▁ideal",
+ -9.869279861450195
+ ],
+ [
+ "▁London",
+ -9.87080192565918
+ ],
+ [
+ "▁answer",
+ -9.870853424072266
+ ],
+ [
+ "▁Vor",
+ -9.87157917022705
+ ],
+ [
+ "▁gives",
+ -9.873115539550781
+ ],
+ [
+ "ative",
+ -9.87316608428955
+ ],
+ [
+ "▁timp",
+ -9.873167991638184
+ ],
+ [
+ "▁center",
+ -9.87362289428711
+ ],
+ [
+ "▁Group",
+ -9.874580383300781
+ ],
+ [
+ "▁sans",
+ -9.875143051147461
+ ],
+ [
+ "▁Ar",
+ -9.875466346740723
+ ],
+ [
+ "▁Ma",
+ -9.875568389892578
+ ],
+ [
+ "▁reach",
+ -9.876279830932617
+ ],
+ [
+ "ren",
+ -9.876652717590332
+ ],
+ [
+ "▁More",
+ -9.877446174621582
+ ],
+ [
+ "mit",
+ -9.878068923950195
+ ],
+ [
+ "▁guide",
+ -9.87833309173584
+ ],
+ [
+ "▁fully",
+ -9.878828048706055
+ ],
+ [
+ "▁Since",
+ -9.878952980041504
+ ],
+ [
+ "▁Inc",
+ -9.87923812866211
+ ],
+ [
+ "▁culture",
+ -9.879780769348145
+ ],
+ [
+ "eat",
+ -9.880531311035156
+ ],
+ [
+ "▁written",
+ -9.880722999572754
+ ],
+ [
+ "▁Ho",
+ -9.881338119506836
+ ],
+ [
+ "▁India",
+ -9.881625175476074
+ ],
+ [
+ "▁Well",
+ -9.881708145141602
+ ],
+ [
+ "back",
+ -9.881752967834473
+ ],
+ [
+ "▁goes",
+ -9.882170677185059
+ ],
+ [
+ "▁completely",
+ -9.88217544555664
+ ],
+ [
+ "▁tour",
+ -9.883081436157227
+ ],
+ [
+ "▁began",
+ -9.883196830749512
+ ],
+ [
+ "▁picture",
+ -9.883255958557129
+ ],
+ [
+ "▁mare",
+ -9.88353157043457
+ ],
+ [
+ "▁playing",
+ -9.884223937988281
+ ],
+ [
+ "▁trebuie",
+ -9.884926795959473
+ ],
+ [
+ "ils",
+ -9.884940147399902
+ ],
+ [
+ "chen",
+ -9.885220527648926
+ ],
+ [
+ "▁hit",
+ -9.885416984558105
+ ],
+ [
+ "▁complex",
+ -9.88591480255127
+ ],
+ [
+ "▁Thank",
+ -9.886140823364258
+ ],
+ [
+ "▁Let",
+ -9.886350631713867
+ ],
+ [
+ "▁applications",
+ -9.887116432189941
+ ],
+ [
+ "▁friend",
+ -9.888312339782715
+ ],
+ [
+ "▁English",
+ -9.889549255371094
+ ],
+ [
+ "▁charge",
+ -9.890040397644043
+ ],
+ [
+ "▁recommend",
+ -9.893453598022461
+ ],
+ [
+ "▁message",
+ -9.893672943115234
+ ],
+ [
+ "In",
+ -9.893722534179688
+ ],
+ [
+ "▁Mar",
+ -9.894762992858887
+ ],
+ [
+ "pp",
+ -9.895845413208008
+ ],
+ [
+ "▁method",
+ -9.89692497253418
+ ],
+ [
+ "▁successful",
+ -9.897004127502441
+ ],
+ [
+ "tion",
+ -9.898880958557129
+ ],
+ [
+ "▁release",
+ -9.899920463562012
+ ],
+ [
+ "▁creating",
+ -9.900403022766113
+ ],
+ [
+ "▁despre",
+ -9.90141773223877
+ ],
+ [
+ "esc",
+ -9.902434349060059
+ ],
+ [
+ "▁eye",
+ -9.902752876281738
+ ],
+ [
+ "▁apply",
+ -9.905945777893066
+ ],
+ [
+ "net",
+ -9.906000137329102
+ ],
+ [
+ "side",
+ -9.906539916992188
+ ],
+ [
+ "▁ar",
+ -9.906949996948242
+ ],
+ [
+ "▁platform",
+ -9.90713882446289
+ ],
+ [
+ "▁touch",
+ -9.907329559326172
+ ],
+ [
+ "▁towards",
+ -9.90785026550293
+ ],
+ [
+ "▁match",
+ -9.908224105834961
+ ],
+ [
+ "▁Black",
+ -9.909344673156738
+ ],
+ [
+ "▁fall",
+ -9.90961742401123
+ ],
+ [
+ "▁ground",
+ -9.910234451293945
+ ],
+ [
+ "▁High",
+ -9.910740852355957
+ ],
+ [
+ "▁Q",
+ -9.911155700683594
+ ],
+ [
+ "▁schon",
+ -9.911709785461426
+ ],
+ [
+ "▁hotel",
+ -9.911751747131348
+ ],
+ [
+ "▁prices",
+ -9.912031173706055
+ ],
+ [
+ "▁developed",
+ -9.913411140441895
+ ],
+ [
+ "uk",
+ -9.913476943969727
+ ],
+ [
+ "ide",
+ -9.91367244720459
+ ],
+ [
+ "▁September",
+ -9.91370964050293
+ ],
+ [
+ "ized",
+ -9.914202690124512
+ ],
+ [
+ "▁War",
+ -9.914704322814941
+ ],
+ [
+ "!!",
+ -9.916285514831543
+ ],
+ [
+ "▁grow",
+ -9.916997909545898
+ ],
+ [
+ "▁watch",
+ -9.917067527770996
+ ],
+ [
+ "▁storage",
+ -9.917412757873535
+ ],
+ [
+ "eau",
+ -9.917513847351074
+ ],
+ [
+ "can",
+ -9.918373107910156
+ ],
+ [
+ "▁Get",
+ -9.919524192810059
+ ],
+ [
+ "▁See",
+ -9.91953182220459
+ ],
+ [
+ "▁European",
+ -9.919703483581543
+ ],
+ [
+ "▁language",
+ -9.91982650756836
+ ],
+ [
+ "ează",
+ -9.920175552368164
+ ],
+ [
+ "▁court",
+ -9.920334815979004
+ ],
+ [
+ "▁Why",
+ -9.921106338500977
+ ],
+ [
+ "▁hear",
+ -9.921342849731445
+ ],
+ [
+ "▁doar",
+ -9.921804428100586
+ ],
+ [
+ "lan",
+ -9.92330265045166
+ ],
+ [
+ "▁Christmas",
+ -9.923810958862305
+ ],
+ [
+ "▁Web",
+ -9.923871994018555
+ ],
+ [
+ "vo",
+ -9.92405891418457
+ ],
+ [
+ "▁sent",
+ -9.924983024597168
+ ],
+ [
+ "▁businesses",
+ -9.925868034362793
+ ],
+ [
+ "▁Red",
+ -9.926278114318848
+ ],
+ [
+ "tel",
+ -9.926375389099121
+ ],
+ [
+ "▁Ha",
+ -9.926508903503418
+ ],
+ [
+ "▁wonderful",
+ -9.926653861999512
+ ],
+ [
+ "ations",
+ -9.926738739013672
+ ],
+ [
+ "za",
+ -9.92748737335205
+ ],
+ [
+ "▁22",
+ -9.928659439086914
+ ],
+ [
+ "▁thinking",
+ -9.92941665649414
+ ],
+ [
+ "▁became",
+ -9.929733276367188
+ ],
+ [
+ "▁cool",
+ -9.929835319519043
+ ],
+ [
+ "▁speed",
+ -9.930370330810547
+ ],
+ [
+ "mar",
+ -9.930426597595215
+ ],
+ [
+ "▁--",
+ -9.931743621826172
+ ],
+ [
+ "▁groups",
+ -9.931920051574707
+ ],
+ [
+ "▁interested",
+ -9.93198299407959
+ ],
+ [
+ "ak",
+ -9.93218994140625
+ ],
+ [
+ "▁60",
+ -9.932672500610352
+ ],
+ [
+ "▁screen",
+ -9.93370246887207
+ ],
+ [
+ "▁Design",
+ -9.933789253234863
+ ],
+ [
+ "▁limited",
+ -9.935648918151855
+ ],
+ [
+ "▁expected",
+ -9.935959815979004
+ ],
+ [
+ "▁opportunities",
+ -9.936376571655273
+ ],
+ [
+ "▁regular",
+ -9.936870574951172
+ ],
+ [
+ "off",
+ -9.93702220916748
+ ],
+ [
+ "▁Best",
+ -9.937298774719238
+ ],
+ [
+ "Re",
+ -9.938436508178711
+ ],
+ [
+ "▁ihr",
+ -9.938719749450684
+ ],
+ [
+ "▁Great",
+ -9.938907623291016
+ ],
+ [
+ "▁employees",
+ -9.93924617767334
+ ],
+ [
+ "▁custom",
+ -9.939679145812988
+ ],
+ [
+ "▁multe",
+ -9.940123558044434
+ ],
+ [
+ "let",
+ -9.940876007080078
+ ],
+ [
+ "▁benefit",
+ -9.942487716674805
+ ],
+ [
+ "▁term",
+ -9.942623138427734
+ ],
+ [
+ "▁bine",
+ -9.942869186401367
+ ],
+ [
+ "▁deep",
+ -9.944526672363281
+ ],
+ [
+ "▁August",
+ -9.94526481628418
+ ],
+ [
+ "▁President",
+ -9.945381164550781
+ ],
+ [
+ "▁Auf",
+ -9.945854187011719
+ ],
+ [
+ "▁wish",
+ -9.946924209594727
+ ],
+ [
+ "▁sometimes",
+ -9.947274208068848
+ ],
+ [
+ "ari",
+ -9.947793960571289
+ ],
+ [
+ "▁pressure",
+ -9.948184967041016
+ ],
+ [
+ "▁ani",
+ -9.94859504699707
+ ],
+ [
+ "▁trade",
+ -9.949930191040039
+ ],
+ [
+ "▁firm",
+ -9.950027465820312
+ ],
+ [
+ "▁comment",
+ -9.95003604888916
+ ],
+ [
+ "▁November",
+ -9.950242042541504
+ ],
+ [
+ "▁expect",
+ -9.951102256774902
+ ],
+ [
+ "▁2012",
+ -9.952491760253906
+ ],
+ [
+ "▁Ich",
+ -9.95328140258789
+ ],
+ [
+ "▁relationship",
+ -9.95363998413086
+ ],
+ [
+ "▁active",
+ -9.954682350158691
+ ],
+ [
+ "org",
+ -9.954710960388184
+ ],
+ [
+ "▁heat",
+ -9.956732749938965
+ ],
+ [
+ "▁wood",
+ -9.95678997039795
+ ],
+ [
+ "▁notre",
+ -9.957921028137207
+ ],
+ [
+ "▁function",
+ -9.958330154418945
+ ],
+ [
+ "▁2.",
+ -9.95909309387207
+ ],
+ [
+ "▁wedding",
+ -9.960049629211426
+ ],
+ [
+ "▁starting",
+ -9.961235046386719
+ ],
+ [
+ "▁Health",
+ -9.961249351501465
+ ],
+ [
+ "\",",
+ -9.961713790893555
+ ],
+ [
+ "▁death",
+ -9.962173461914062
+ ],
+ [
+ "▁pages",
+ -9.962764739990234
+ ],
+ [
+ "▁vehicle",
+ -9.96293830871582
+ ],
+ [
+ "▁request",
+ -9.963874816894531
+ ],
+ [
+ "▁helps",
+ -9.963916778564453
+ ],
+ [
+ "▁blue",
+ -9.964017868041992
+ ],
+ [
+ "▁analysis",
+ -9.964414596557617
+ ],
+ [
+ "▁posted",
+ -9.964544296264648
+ ],
+ [
+ "▁healthy",
+ -9.964814186096191
+ ],
+ [
+ "▁contract",
+ -9.964988708496094
+ ],
+ [
+ "▁•",
+ -9.965263366699219
+ ],
+ [
+ "▁Each",
+ -9.965293884277344
+ ],
+ [
+ "▁Fa",
+ -9.966179847717285
+ ],
+ [
+ "▁dintre",
+ -9.966221809387207
+ ],
+ [
+ "▁Friday",
+ -9.967202186584473
+ ],
+ [
+ "▁considered",
+ -9.967992782592773
+ ],
+ [
+ "cher",
+ -9.96826457977295
+ ],
+ [
+ "▁quick",
+ -9.968731880187988
+ ],
+ [
+ "▁understanding",
+ -9.96916389465332
+ ],
+ [
+ "▁condition",
+ -9.969378471374512
+ ],
+ [
+ "ization",
+ -9.971049308776855
+ ],
+ [
+ "▁document",
+ -9.971664428710938
+ ],
+ [
+ "▁prevent",
+ -9.971890449523926
+ ],
+ [
+ "▁growing",
+ -9.9725341796875
+ ],
+ [
+ "▁protection",
+ -9.972620964050293
+ ],
+ [
+ "▁cat",
+ -9.974002838134766
+ ],
+ [
+ "▁#",
+ -9.975058555603027
+ ],
+ [
+ "10",
+ -9.975275039672852
+ ],
+ [
+ "▁join",
+ -9.9759521484375
+ ],
+ [
+ "▁serve",
+ -9.976580619812012
+ ],
+ [
+ "▁blood",
+ -9.977095603942871
+ ],
+ [
+ "▁July",
+ -9.977341651916504
+ ],
+ [
+ "▁region",
+ -9.977787971496582
+ ],
+ [
+ "car",
+ -9.97933578491211
+ ],
+ [
+ "▁entre",
+ -9.979788780212402
+ ],
+ [
+ "▁physical",
+ -9.981287002563477
+ ],
+ [
+ "▁cash",
+ -9.9813232421875
+ ],
+ [
+ "aux",
+ -9.981823921203613
+ ],
+ [
+ "ng",
+ -9.982654571533203
+ ],
+ [
+ "▁stage",
+ -9.98281478881836
+ ],
+ [
+ "▁seem",
+ -9.983034133911133
+ ],
+ [
+ "▁definitely",
+ -9.983795166015625
+ ],
+ [
+ "▁investment",
+ -9.983827590942383
+ ],
+ [
+ "▁purpose",
+ -9.985441207885742
+ ],
+ [
+ "▁begin",
+ -9.985486030578613
+ ],
+ [
+ "®",
+ -9.985495567321777
+ ],
+ [
+ "▁break",
+ -9.985701560974121
+ ],
+ [
+ "itate",
+ -9.987293243408203
+ ],
+ [
+ "▁moving",
+ -9.989288330078125
+ ],
+ [
+ "▁met",
+ -9.990678787231445
+ ],
+ [
+ "ize",
+ -9.990833282470703
+ ],
+ [
+ "▁select",
+ -9.991165161132812
+ ],
+ [
+ "▁tous",
+ -9.991310119628906
+ ],
+ [
+ "▁Europe",
+ -9.991639137268066
+ ],
+ [
+ "@",
+ -9.992724418640137
+ ],
+ [
+ "▁individuals",
+ -9.993392944335938
+ ],
+ [
+ "▁Zeit",
+ -9.993524551391602
+ ],
+ [
+ "gu",
+ -9.995670318603516
+ ],
+ [
+ "▁unit",
+ -9.995753288269043
+ ],
+ [
+ "▁noi",
+ -9.996089935302734
+ ],
+ [
+ "▁places",
+ -9.996171951293945
+ ],
+ [
+ "all",
+ -9.99632453918457
+ ],
+ [
+ "▁wait",
+ -9.996755599975586
+ ],
+ [
+ "▁difference",
+ -9.997234344482422
+ ],
+ [
+ "▁round",
+ -9.998015403747559
+ ],
+ [
+ "50",
+ -9.99953842163086
+ ],
+ [
+ "rie",
+ -9.999545097351074
+ ],
+ [
+ "▁Et",
+ -9.999933242797852
+ ],
+ [
+ "20",
+ -10.000725746154785
+ ],
+ [
+ "▁activity",
+ -10.000792503356934
+ ],
+ [
+ "е",
+ -10.000866889953613
+ ],
+ [
+ "▁Windows",
+ -10.001087188720703
+ ],
+ [
+ "▁produce",
+ -10.001385688781738
+ ],
+ [
+ "▁keine",
+ -10.00212574005127
+ ],
+ [
+ "▁Air",
+ -10.002567291259766
+ ],
+ [
+ "▁January",
+ -10.004890441894531
+ ],
+ [
+ "▁deux",
+ -10.005081176757812
+ ],
+ [
+ "▁entry",
+ -10.005208015441895
+ ],
+ [
+ "king",
+ -10.006500244140625
+ ],
+ [
+ "▁goals",
+ -10.006736755371094
+ ],
+ [
+ "▁previous",
+ -10.0077543258667
+ ],
+ [
+ "▁+",
+ -10.008035659790039
+ ],
+ [
+ "▁Business",
+ -10.008259773254395
+ ],
+ [
+ "ont",
+ -10.008552551269531
+ ],
+ [
+ "▁Sunday",
+ -10.008694648742676
+ ],
+ [
+ "▁offering",
+ -10.010359764099121
+ ],
+ [
+ "▁response",
+ -10.011018753051758
+ ],
+ [
+ "▁surface",
+ -10.011393547058105
+ ],
+ [
+ "▁Department",
+ -10.01212215423584
+ ],
+ [
+ "▁exactly",
+ -10.012190818786621
+ ],
+ [
+ "▁Online",
+ -10.012577056884766
+ ],
+ [
+ "dem",
+ -10.013803482055664
+ ],
+ [
+ "ischen",
+ -10.014006614685059
+ ],
+ [
+ "▁hands",
+ -10.015100479125977
+ ],
+ [
+ "▁hour",
+ -10.016197204589844
+ ],
+ [
+ "▁dog",
+ -10.016946792602539
+ ],
+ [
+ "▁damage",
+ -10.017006874084473
+ ],
+ [
+ "▁capital",
+ -10.018792152404785
+ ],
+ [
+ "▁toate",
+ -10.020488739013672
+ ],
+ [
+ "▁wrong",
+ -10.020674705505371
+ ],
+ [
+ "unui",
+ -10.022201538085938
+ ],
+ [
+ "tri",
+ -10.023979187011719
+ ],
+ [
+ "▁sell",
+ -10.023999214172363
+ ],
+ [
+ "▁published",
+ -10.024175643920898
+ ],
+ [
+ "▁families",
+ -10.024675369262695
+ ],
+ [
+ "▁avoid",
+ -10.025490760803223
+ ],
+ [
+ "▁Ko",
+ -10.025506019592285
+ ],
+ [
+ "▁mod",
+ -10.026697158813477
+ ],
+ [
+ "rat",
+ -10.027653694152832
+ ],
+ [
+ "▁Make",
+ -10.0299654006958
+ ],
+ [
+ "▁October",
+ -10.030153274536133
+ ],
+ [
+ "▁former",
+ -10.031285285949707
+ ],
+ [
+ "▁Services",
+ -10.03281021118164
+ ],
+ [
+ "▁felt",
+ -10.033045768737793
+ ],
+ [
+ "▁selection",
+ -10.033309936523438
+ ],
+ [
+ "eaza",
+ -10.034177780151367
+ ],
+ [
+ "gel",
+ -10.034422874450684
+ ],
+ [
+ "▁Good",
+ -10.035792350769043
+ ],
+ [
+ "▁actual",
+ -10.0364351272583
+ ],
+ [
+ "▁gut",
+ -10.036853790283203
+ ],
+ [
+ "▁gas",
+ -10.03708553314209
+ ],
+ [
+ "15",
+ -10.038182258605957
+ ],
+ [
+ "▁structure",
+ -10.038285255432129
+ ],
+ [
+ "▁act",
+ -10.0386381149292
+ ],
+ [
+ "▁Zu",
+ -10.038654327392578
+ ],
+ [
+ "▁creative",
+ -10.039134979248047
+ ],
+ [
+ "▁Vi",
+ -10.039159774780273
+ ],
+ [
+ "▁shop",
+ -10.04066276550293
+ ],
+ [
+ "▁Lo",
+ -10.040735244750977
+ ],
+ [
+ "şi",
+ -10.042192459106445
+ ],
+ [
+ "▁mis",
+ -10.042224884033203
+ ],
+ [
+ "ungen",
+ -10.042301177978516
+ ],
+ [
+ "▁fan",
+ -10.04240608215332
+ ],
+ [
+ "▁|",
+ -10.043391227722168
+ ],
+ [
+ "▁Bei",
+ -10.044037818908691
+ ],
+ [
+ "▁protect",
+ -10.04454517364502
+ ],
+ [
+ "▁Na",
+ -10.0447998046875
+ ],
+ [
+ "q",
+ -10.045693397521973
+ ],
+ [
+ "ok",
+ -10.04710578918457
+ ],
+ [
+ "▁California",
+ -10.047263145446777
+ ],
+ [
+ "▁political",
+ -10.047301292419434
+ ],
+ [
+ "25",
+ -10.047530174255371
+ ],
+ [
+ "▁feeling",
+ -10.047913551330566
+ ],
+ [
+ "▁ces",
+ -10.048321723937988
+ ],
+ [
+ "▁display",
+ -10.048857688903809
+ ],
+ [
+ "▁essential",
+ -10.04964542388916
+ ],
+ [
+ "ând",
+ -10.049971580505371
+ ],
+ [
+ "▁seine",
+ -10.050551414489746
+ ],
+ [
+ "▁soft",
+ -10.050915718078613
+ ],
+ [
+ "ach",
+ -10.05102252960205
+ ],
+ [
+ "▁happen",
+ -10.051118850708008
+ ],
+ [
+ "▁Paul",
+ -10.053346633911133
+ ],
+ [
+ "▁Cu",
+ -10.054024696350098
+ ],
+ [
+ "house",
+ -10.055376052856445
+ ],
+ [
+ "ante",
+ -10.05582046508789
+ ],
+ [
+ "▁easier",
+ -10.056551933288574
+ ],
+ [
+ "▁sort",
+ -10.0567045211792
+ ],
+ [
+ "▁Post",
+ -10.057138442993164
+ ],
+ [
+ "▁accept",
+ -10.05730152130127
+ ],
+ [
+ "field",
+ -10.057648658752441
+ ],
+ [
+ "zen",
+ -10.057741165161133
+ ],
+ [
+ "▁character",
+ -10.057848930358887
+ ],
+ [
+ "▁beginning",
+ -10.058433532714844
+ ],
+ [
+ "▁Jesus",
+ -10.058760643005371
+ ],
+ [
+ "▁weekend",
+ -10.059663772583008
+ ],
+ [
+ "▁certainly",
+ -10.06114387512207
+ ],
+ [
+ "▁THE",
+ -10.061254501342773
+ ],
+ [
+ "▁alle",
+ -10.06189250946045
+ ],
+ [
+ "▁transport",
+ -10.062220573425293
+ ],
+ [
+ "▁Saturday",
+ -10.063043594360352
+ ],
+ [
+ "▁basic",
+ -10.064136505126953
+ ],
+ [
+ "▁loved",
+ -10.06431770324707
+ ],
+ [
+ "ros",
+ -10.065333366394043
+ ],
+ [
+ "▁offered",
+ -10.065996170043945
+ ],
+ [
+ "▁camera",
+ -10.067024230957031
+ ],
+ [
+ "▁Green",
+ -10.06789779663086
+ ],
+ [
+ "ology",
+ -10.069480895996094
+ ],
+ [
+ "ä",
+ -10.069646835327148
+ ],
+ [
+ "▁manage",
+ -10.070416450500488
+ ],
+ [
+ "▁paid",
+ -10.070881843566895
+ ],
+ [
+ "▁advice",
+ -10.071617126464844
+ ],
+ [
+ "▁patient",
+ -10.072234153747559
+ ],
+ [
+ "▁spent",
+ -10.072272300720215
+ ],
+ [
+ "▁mir",
+ -10.072366714477539
+ ],
+ [
+ "▁baby",
+ -10.072400093078613
+ ],
+ [
+ "ö",
+ -10.073193550109863
+ ],
+ [
+ "▁basis",
+ -10.073338508605957
+ ],
+ [
+ "▁cancer",
+ -10.073765754699707
+ ],
+ [
+ "▁Although",
+ -10.07400894165039
+ ],
+ [
+ "▁gift",
+ -10.074336051940918
+ ],
+ [
+ "▁3.",
+ -10.074871063232422
+ ],
+ [
+ "dieser",
+ -10.075157165527344
+ ],
+ [
+ "▁overall",
+ -10.07520580291748
+ ],
+ [
+ "▁Sch",
+ -10.075265884399414
+ ],
+ [
+ "▁Ex",
+ -10.076258659362793
+ ],
+ [
+ "▁December",
+ -10.07689094543457
+ ],
+ [
+ "▁released",
+ -10.078214645385742
+ ],
+ [
+ "▁prior",
+ -10.07900333404541
+ ],
+ [
+ "▁sowie",
+ -10.081072807312012
+ ],
+ [
+ "▁club",
+ -10.081326484680176
+ ],
+ [
+ "▁Street",
+ -10.081535339355469
+ ],
+ [
+ "▁College",
+ -10.08254623413086
+ ],
+ [
+ "▁î",
+ -10.083059310913086
+ ],
+ [
+ "over",
+ -10.083159446716309
+ ],
+ [
+ "▁gave",
+ -10.08454704284668
+ ],
+ [
+ "▁truly",
+ -10.084784507751465
+ ],
+ [
+ "par",
+ -10.084806442260742
+ ],
+ [
+ "▁Canada",
+ -10.084888458251953
+ ],
+ [
+ "▁existing",
+ -10.085420608520508
+ ],
+ [
+ "lie",
+ -10.086335182189941
+ ],
+ [
+ "▁ganz",
+ -10.086658477783203
+ ],
+ [
+ "▁setting",
+ -10.087109565734863
+ ],
+ [
+ "▁supply",
+ -10.08739185333252
+ ],
+ [
+ "▁college",
+ -10.087540626525879
+ ],
+ [
+ "▁communication",
+ -10.088407516479492
+ ],
+ [
+ "▁23",
+ -10.088834762573242
+ ],
+ [
+ "▁pass",
+ -10.091546058654785
+ ],
+ [
+ "▁devices",
+ -10.091872215270996
+ ],
+ [
+ "▁glass",
+ -10.092083930969238
+ ],
+ [
+ "▁experienced",
+ -10.092395782470703
+ ],
+ [
+ "▁grand",
+ -10.093363761901855
+ ],
+ [
+ "▁Po",
+ -10.093396186828613
+ ],
+ [
+ "▁beyond",
+ -10.094029426574707
+ ],
+ [
+ "▁format",
+ -10.094165802001953
+ ],
+ [
+ "▁mon",
+ -10.09461498260498
+ ],
+ [
+ "▁perform",
+ -10.094635009765625
+ ],
+ [
+ "sten",
+ -10.095130920410156
+ ],
+ [
+ "▁1,",
+ -10.096270561218262
+ ],
+ [
+ "▁Per",
+ -10.096640586853027
+ ],
+ [
+ "▁sold",
+ -10.097247123718262
+ ],
+ [
+ "▁rates",
+ -10.0972900390625
+ ],
+ [
+ "▁regarding",
+ -10.097782135009766
+ ],
+ [
+ "▁Paris",
+ -10.098291397094727
+ ],
+ [
+ "▁Dar",
+ -10.099579811096191
+ ],
+ [
+ "▁challenge",
+ -10.099649429321289
+ ],
+ [
+ "▁feet",
+ -10.100564002990723
+ ],
+ [
+ "▁Su",
+ -10.102017402648926
+ ],
+ [
+ "je",
+ -10.102593421936035
+ ],
+ [
+ "▁Bank",
+ -10.102627754211426
+ ],
+ [
+ "ven",
+ -10.103126525878906
+ ],
+ [
+ "jo",
+ -10.103290557861328
+ ],
+ [
+ "▁band",
+ -10.10348892211914
+ ],
+ [
+ "▁delivery",
+ -10.104915618896484
+ ],
+ [
+ "Vous",
+ -10.104924201965332
+ ],
+ [
+ "tele",
+ -10.10495376586914
+ ],
+ [
+ "▁East",
+ -10.105379104614258
+ ],
+ [
+ "▁pictures",
+ -10.106067657470703
+ ],
+ [
+ "▁useful",
+ -10.106481552124023
+ ],
+ [
+ "*",
+ -10.107648849487305
+ ],
+ [
+ "▁increased",
+ -10.107746124267578
+ ],
+ [
+ "▁stories",
+ -10.108119010925293
+ ],
+ [
+ "sion",
+ -10.108280181884766
+ ],
+ [
+ "bra",
+ -10.108345985412598
+ ],
+ [
+ "▁brought",
+ -10.108466148376465
+ ],
+ [
+ "▁effort",
+ -10.109898567199707
+ ],
+ [
+ "▁payment",
+ -10.11058235168457
+ ],
+ [
+ "▁heard",
+ -10.110925674438477
+ ],
+ [
+ "▁played",
+ -10.111245155334473
+ ],
+ [
+ "▁White",
+ -10.111417770385742
+ ],
+ [
+ "▁metal",
+ -10.111721992492676
+ ],
+ [
+ "tal",
+ -10.111754417419434
+ ],
+ [
+ "▁engine",
+ -10.112006187438965
+ ],
+ [
+ "▁Club",
+ -10.11218547821045
+ ],
+ [
+ "ical",
+ -10.114581108093262
+ ],
+ [
+ "▁effects",
+ -10.115421295166016
+ ],
+ [
+ "▁degree",
+ -10.115763664245605
+ ],
+ [
+ "▁bed",
+ -10.1159086227417
+ ],
+ [
+ "ette",
+ -10.115991592407227
+ ],
+ [
+ "▁David",
+ -10.116386413574219
+ ],
+ [
+ "°",
+ -10.117666244506836
+ ],
+ [
+ "▁Au",
+ -10.117938041687012
+ ],
+ [
+ "▁Company",
+ -10.11845874786377
+ ],
+ [
+ "▁player",
+ -10.11938190460205
+ ],
+ [
+ "▁Today",
+ -10.120569229125977
+ ],
+ [
+ "▁maintain",
+ -10.12093448638916
+ ],
+ [
+ "▁minute",
+ -10.121193885803223
+ ],
+ [
+ "mail",
+ -10.122172355651855
+ ],
+ [
+ "▁race",
+ -10.122366905212402
+ ],
+ [
+ "▁comfortable",
+ -10.123887062072754
+ ],
+ [
+ "▁responsible",
+ -10.124085426330566
+ ],
+ [
+ "vor",
+ -10.124622344970703
+ ],
+ [
+ "▁associated",
+ -10.124695777893066
+ ],
+ [
+ "▁weather",
+ -10.124701499938965
+ ],
+ [
+ "▁$1",
+ -10.125639915466309
+ ],
+ [
+ "▁tried",
+ -10.126176834106445
+ ],
+ [
+ "▁Check",
+ -10.127649307250977
+ ],
+ [
+ "▁solid",
+ -10.127864837646484
+ ],
+ [
+ "▁movie",
+ -10.128364562988281
+ ],
+ [
+ "▁coffee",
+ -10.12874698638916
+ ],
+ [
+ "board",
+ -10.129073143005371
+ ],
+ [
+ "▁po",
+ -10.12946605682373
+ ],
+ [
+ "▁warm",
+ -10.129583358764648
+ ],
+ [
+ "▁connect",
+ -10.131733894348145
+ ],
+ [
+ "▁Ad",
+ -10.133807182312012
+ ],
+ [
+ "work",
+ -10.133859634399414
+ ],
+ [
+ "mal",
+ -10.13397216796875
+ ],
+ [
+ "▁Act",
+ -10.134634971618652
+ ],
+ [
+ "▁achieve",
+ -10.134769439697266
+ ],
+ [
+ "▁Nach",
+ -10.136604309082031
+ ],
+ [
+ "www",
+ -10.136669158935547
+ ],
+ [
+ "term",
+ -10.13672161102295
+ ],
+ [
+ "▁claim",
+ -10.137251853942871
+ ],
+ [
+ "▁particularly",
+ -10.138245582580566
+ ],
+ [
+ "▁cas",
+ -10.138396263122559
+ ],
+ [
+ "▁furniture",
+ -10.138461112976074
+ ],
+ [
+ "▁finish",
+ -10.13896369934082
+ ],
+ [
+ "▁temps",
+ -10.139026641845703
+ ],
+ [
+ "▁disease",
+ -10.139115333557129
+ ],
+ [
+ "▁lots",
+ -10.139196395874023
+ ],
+ [
+ "▁ball",
+ -10.139307975769043
+ ],
+ [
+ "▁sun",
+ -10.14010238647461
+ ],
+ [
+ "▁strategy",
+ -10.140498161315918
+ ],
+ [
+ "bre",
+ -10.140518188476562
+ ],
+ [
+ "▁mine",
+ -10.141541481018066
+ ],
+ [
+ "▁Click",
+ -10.141743659973145
+ ],
+ [
+ "ran",
+ -10.141983032226562
+ ],
+ [
+ "▁Will",
+ -10.142234802246094
+ ],
+ [
+ "▁garden",
+ -10.142974853515625
+ ],
+ [
+ "▁stuff",
+ -10.14359188079834
+ ],
+ [
+ "▁limit",
+ -10.144641876220703
+ ],
+ [
+ "▁bottom",
+ -10.14494800567627
+ ],
+ [
+ "▁shown",
+ -10.144962310791016
+ ],
+ [
+ "ship",
+ -10.145271301269531
+ ],
+ [
+ "▁habe",
+ -10.145858764648438
+ ],
+ [
+ "▁Super",
+ -10.146219253540039
+ ],
+ [
+ "▁completed",
+ -10.146971702575684
+ ],
+ [
+ "▁wine",
+ -10.146979331970215
+ ],
+ [
+ "ische",
+ -10.147262573242188
+ ],
+ [
+ "▁largest",
+ -10.147466659545898
+ ],
+ [
+ "▁appropriate",
+ -10.148261070251465
+ ],
+ [
+ "▁immediately",
+ -10.150248527526855
+ ],
+ [
+ "▁Hi",
+ -10.152358055114746
+ ],
+ [
+ "▁trust",
+ -10.152767181396484
+ ],
+ [
+ "ability",
+ -10.154254913330078
+ ],
+ [
+ "▁powerful",
+ -10.155101776123047
+ ],
+ [
+ "▁helping",
+ -10.155620574951172
+ ],
+ [
+ "▁schedule",
+ -10.155688285827637
+ ],
+ [
+ "▁correct",
+ -10.155707359313965
+ ],
+ [
+ "▁transfer",
+ -10.156496047973633
+ ],
+ [
+ "pre",
+ -10.15665340423584
+ ],
+ [
+ "▁journey",
+ -10.15688419342041
+ ],
+ [
+ "pm",
+ -10.157002449035645
+ ],
+ [
+ "don",
+ -10.158435821533203
+ ],
+ [
+ "▁highest",
+ -10.159249305725098
+ ],
+ [
+ "▁finally",
+ -10.15999698638916
+ ],
+ [
+ "form",
+ -10.160258293151855
+ ],
+ [
+ "▁extremely",
+ -10.160404205322266
+ ],
+ [
+ "▁window",
+ -10.160501480102539
+ ],
+ [
+ "▁Over",
+ -10.162222862243652
+ ],
+ [
+ "▁remove",
+ -10.162469863891602
+ ],
+ [
+ "wood",
+ -10.162479400634766
+ ],
+ [
+ "▁2013",
+ -10.163631439208984
+ ],
+ [
+ "▁mother",
+ -10.164072036743164
+ ],
+ [
+ "▁Auto",
+ -10.16436767578125
+ ],
+ [
+ "▁annual",
+ -10.164615631103516
+ ],
+ [
+ "▁Star",
+ -10.164834976196289
+ ],
+ [
+ "▁Di",
+ -10.166138648986816
+ ],
+ [
+ "о",
+ -10.16711139678955
+ ],
+ [
+ "▁gold",
+ -10.167129516601562
+ ],
+ [
+ "tar",
+ -10.167352676391602
+ ],
+ [
+ "ju",
+ -10.167750358581543
+ ],
+ [
+ "▁Use",
+ -10.169474601745605
+ ],
+ [
+ "▁thanks",
+ -10.16960334777832
+ ],
+ [
+ "▁centre",
+ -10.170127868652344
+ ],
+ [
+ "▁Australia",
+ -10.170358657836914
+ ],
+ [
+ "▁estate",
+ -10.170504570007324
+ ],
+ [
+ "▁eyes",
+ -10.1714448928833
+ ],
+ [
+ "▁force",
+ -10.171592712402344
+ ],
+ [
+ "▁income",
+ -10.17395305633545
+ ],
+ [
+ "▁science",
+ -10.174036026000977
+ ],
+ [
+ "ori",
+ -10.174230575561523
+ ],
+ [
+ "▁enter",
+ -10.174851417541504
+ ],
+ [
+ "▁28",
+ -10.175408363342285
+ ],
+ [
+ "ire",
+ -10.17568302154541
+ ],
+ [
+ "▁schools",
+ -10.175797462463379
+ ],
+ [
+ "▁restaurant",
+ -10.176088333129883
+ ],
+ [
+ "▁Council",
+ -10.177032470703125
+ ],
+ [
+ "aus",
+ -10.177885055541992
+ ],
+ [
+ "▁agree",
+ -10.17905330657959
+ ],
+ [
+ "▁campaign",
+ -10.179192543029785
+ ],
+ [
+ "▁Ta",
+ -10.179428100585938
+ ],
+ [
+ "▁letter",
+ -10.179814338684082
+ ],
+ [
+ "▁central",
+ -10.179931640625
+ ],
+ [
+ "▁Because",
+ -10.180054664611816
+ ],
+ [
+ "▁path",
+ -10.180349349975586
+ ],
+ [
+ "▁loc",
+ -10.180882453918457
+ ],
+ [
+ "▁files",
+ -10.182587623596191
+ ],
+ [
+ "▁population",
+ -10.182705879211426
+ ],
+ [
+ "▁explore",
+ -10.182723999023438
+ ],
+ [
+ "▁mid",
+ -10.182734489440918
+ ],
+ [
+ "▁concept",
+ -10.182748794555664
+ ],
+ [
+ "▁church",
+ -10.183015823364258
+ ],
+ [
+ "80",
+ -10.183026313781738
+ ],
+ [
+ "▁einfach",
+ -10.185834884643555
+ ],
+ [
+ "▁reasons",
+ -10.186690330505371
+ ],
+ [
+ "▁determine",
+ -10.186755180358887
+ ],
+ [
+ "▁February",
+ -10.187095642089844
+ ],
+ [
+ "▁evidence",
+ -10.18797779083252
+ ],
+ [
+ "▁sleep",
+ -10.188036918640137
+ ],
+ [
+ "▁Board",
+ -10.188652992248535
+ ],
+ [
+ "▁maybe",
+ -10.189635276794434
+ ],
+ [
+ "▁wasn",
+ -10.189701080322266
+ ],
+ [
+ "▁Monday",
+ -10.190101623535156
+ ],
+ [
+ "▁director",
+ -10.190481185913086
+ ],
+ [
+ "well",
+ -10.190974235534668
+ ],
+ [
+ "During",
+ -10.191001892089844
+ ],
+ [
+ "▁sweet",
+ -10.191061973571777
+ ],
+ [
+ "▁assist",
+ -10.19124984741211
+ ],
+ [
+ "▁police",
+ -10.191511154174805
+ ],
+ [
+ "▁repair",
+ -10.191729545593262
+ ],
+ [
+ "▁techniques",
+ -10.191733360290527
+ ],
+ [
+ "▁served",
+ -10.191808700561523
+ ],
+ [
+ "vi",
+ -10.192037582397461
+ ],
+ [
+ "▁sports",
+ -10.192331314086914
+ ],
+ [
+ "▁opening",
+ -10.192401885986328
+ ],
+ [
+ "▁ones",
+ -10.192731857299805
+ ],
+ [
+ "▁notice",
+ -10.193460464477539
+ ],
+ [
+ "▁PC",
+ -10.193547248840332
+ ],
+ [
+ "▁alte",
+ -10.194242477416992
+ ],
+ [
+ "▁Bi",
+ -10.194340705871582
+ ],
+ [
+ "▁cold",
+ -10.195606231689453
+ ],
+ [
+ "▁billion",
+ -10.195794105529785
+ ],
+ [
+ "▁balance",
+ -10.196361541748047
+ ],
+ [
+ "cer",
+ -10.196417808532715
+ ],
+ [
+ "▁nearly",
+ -10.196725845336914
+ ],
+ [
+ "▁wear",
+ -10.197259902954102
+ ],
+ [
+ "free",
+ -10.19760799407959
+ ],
+ [
+ "▁Have",
+ -10.197748184204102
+ ],
+ [
+ "▁comfort",
+ -10.199211120605469
+ ],
+ [
+ "▁studies",
+ -10.199225425720215
+ ],
+ [
+ "▁traffic",
+ -10.199540138244629
+ ],
+ [
+ "▁item",
+ -10.200214385986328
+ ],
+ [
+ "▁teaching",
+ -10.200467109680176
+ ],
+ [
+ "▁turned",
+ -10.201326370239258
+ ],
+ [
+ "isation",
+ -10.201354026794434
+ ],
+ [
+ "12",
+ -10.202038764953613
+ ],
+ [
+ "▁greater",
+ -10.202167510986328
+ ],
+ [
+ "▁knew",
+ -10.20233154296875
+ ],
+ [
+ "▁Association",
+ -10.203333854675293
+ ],
+ [
+ "▁Office",
+ -10.203802108764648
+ ],
+ [
+ "▁established",
+ -10.204085350036621
+ ],
+ [
+ "45",
+ -10.204170227050781
+ ],
+ [
+ "▁Love",
+ -10.204318046569824
+ ],
+ [
+ "▁changed",
+ -10.204882621765137
+ ],
+ [
+ "▁pan",
+ -10.205184936523438
+ ],
+ [
+ "van",
+ -10.20565414428711
+ ],
+ [
+ "▁Mi",
+ -10.205663681030273
+ ],
+ [
+ "▁tend",
+ -10.20637321472168
+ ],
+ [
+ "▁connection",
+ -10.206522941589355
+ ],
+ [
+ "▁lack",
+ -10.206954002380371
+ ],
+ [
+ "▁bank",
+ -10.208464622497559
+ ],
+ [
+ "cat",
+ -10.208720207214355
+ ],
+ [
+ "▁helped",
+ -10.209071159362793
+ ],
+ [
+ "▁spot",
+ -10.209417343139648
+ ],
+ [
+ "▁spring",
+ -10.20974063873291
+ ],
+ [
+ "▁Wi",
+ -10.210912704467773
+ ],
+ [
+ "▁Mac",
+ -10.211682319641113
+ ],
+ [
+ "▁Christ",
+ -10.212015151977539
+ ],
+ [
+ "▁saying",
+ -10.212835311889648
+ ],
+ [
+ "▁General",
+ -10.213062286376953
+ ],
+ [
+ "▁port",
+ -10.213099479675293
+ ],
+ [
+ "▁Mal",
+ -10.213156700134277
+ ],
+ [
+ "▁System",
+ -10.213486671447754
+ ],
+ [
+ "▁According",
+ -10.2152738571167
+ ],
+ [
+ "▁chiar",
+ -10.21568489074707
+ ],
+ [
+ "log",
+ -10.21576976776123
+ ],
+ [
+ "▁mix",
+ -10.215974807739258
+ ],
+ [
+ "▁Lake",
+ -10.216042518615723
+ ],
+ [
+ "▁intr",
+ -10.216590881347656
+ ],
+ [
+ "▁deliver",
+ -10.216793060302734
+ ],
+ [
+ "mon",
+ -10.216931343078613
+ ],
+ [
+ "▁Ro",
+ -10.217060089111328
+ ],
+ [
+ "▁Management",
+ -10.217504501342773
+ ],
+ [
+ "bri",
+ -10.218718528747559
+ ],
+ [
+ "▁pieces",
+ -10.218774795532227
+ ],
+ [
+ "▁announced",
+ -10.218926429748535
+ ],
+ [
+ "▁Yes",
+ -10.219268798828125
+ ],
+ [
+ "▁dark",
+ -10.220884323120117
+ ],
+ [
+ "val",
+ -10.221765518188477
+ ],
+ [
+ "▁rights",
+ -10.22309684753418
+ ],
+ [
+ "▁Diese",
+ -10.223100662231445
+ ],
+ [
+ "ki",
+ -10.223350524902344
+ ],
+ [
+ "vent",
+ -10.22375774383545
+ ],
+ [
+ "▁born",
+ -10.22380542755127
+ ],
+ [
+ "▁muss",
+ -10.224031448364258
+ ],
+ [
+ "compared",
+ -10.224660873413086
+ ],
+ [
+ "▁demand",
+ -10.224669456481934
+ ],
+ [
+ "▁handle",
+ -10.225493431091309
+ ],
+ [
+ "▁mode",
+ -10.226058006286621
+ ],
+ [
+ "lic",
+ -10.226137161254883
+ ],
+ [
+ "▁ahead",
+ -10.226436614990234
+ ],
+ [
+ "▁sharing",
+ -10.227599143981934
+ ],
+ [
+ "▁micro",
+ -10.227779388427734
+ ],
+ [
+ "▁Par",
+ -10.228626251220703
+ ],
+ [
+ "▁Every",
+ -10.22950553894043
+ ],
+ [
+ "▁bag",
+ -10.229736328125
+ ],
+ [
+ "▁daca",
+ -10.22974967956543
+ ],
+ [
+ "▁Apple",
+ -10.23022174835205
+ ],
+ [
+ "▁Mark",
+ -10.230239868164062
+ ],
+ [
+ "▁larger",
+ -10.231284141540527
+ ],
+ [
+ "eze",
+ -10.231978416442871
+ ],
+ [
+ "▁progress",
+ -10.232234001159668
+ ],
+ [
+ "▁stress",
+ -10.232929229736328
+ ],
+ [
+ "▁cards",
+ -10.233663558959961
+ ],
+ [
+ "▁driving",
+ -10.233738899230957
+ ],
+ [
+ "▁dry",
+ -10.233970642089844
+ ],
+ [
+ "▁relevant",
+ -10.234556198120117
+ ],
+ [
+ "▁Jo",
+ -10.234825134277344
+ ],
+ [
+ "▁tree",
+ -10.235036849975586
+ ],
+ [
+ "▁reported",
+ -10.235770225524902
+ ],
+ [
+ "ities",
+ -10.23577880859375
+ ],
+ [
+ "▁tea",
+ -10.235806465148926
+ ],
+ [
+ "▁although",
+ -10.236145973205566
+ ],
+ [
+ "▁Research",
+ -10.236261367797852
+ ],
+ [
+ "▁pool",
+ -10.23691463470459
+ ],
+ [
+ "▁fin",
+ -10.237163543701172
+ ],
+ [
+ "▁Und",
+ -10.238130569458008
+ ],
+ [
+ "▁decide",
+ -10.239217758178711
+ ],
+ [
+ "▁expert",
+ -10.239344596862793
+ ],
+ [
+ "rate",
+ -10.239428520202637
+ ],
+ [
+ "zeit",
+ -10.239971160888672
+ ],
+ [
+ "▁26",
+ -10.24040412902832
+ ],
+ [
+ "▁Ka",
+ -10.24056339263916
+ ],
+ [
+ "▁fix",
+ -10.240666389465332
+ ],
+ [
+ "igen",
+ -10.240713119506836
+ ],
+ [
+ "▁direction",
+ -10.241188049316406
+ ],
+ [
+ "▁star",
+ -10.241661071777344
+ ],
+ [
+ "▁middle",
+ -10.241889953613281
+ ],
+ [
+ "▁Ja",
+ -10.241962432861328
+ ],
+ [
+ "▁Land",
+ -10.24207878112793
+ ],
+ [
+ "ken",
+ -10.242605209350586
+ ],
+ [
+ "▁button",
+ -10.242630004882812
+ ],
+ [
+ "▁rules",
+ -10.242656707763672
+ ],
+ [
+ "▁également",
+ -10.242706298828125
+ ],
+ [
+ "▁viel",
+ -10.243158340454102
+ ],
+ [
+ "▁welcome",
+ -10.243682861328125
+ ],
+ [
+ "că",
+ -10.243932723999023
+ ],
+ [
+ "▁Top",
+ -10.245308876037598
+ ],
+ [
+ "▁allowed",
+ -10.245487213134766
+ ],
+ [
+ "▁tip",
+ -10.245584487915039
+ ],
+ [
+ "▁cei",
+ -10.245768547058105
+ ],
+ [
+ "▁Nous",
+ -10.246004104614258
+ ],
+ [
+ "té",
+ -10.246850967407227
+ ],
+ [
+ "▁unei",
+ -10.246903419494629
+ ],
+ [
+ "▁efforts",
+ -10.247260093688965
+ ],
+ [
+ "▁note",
+ -10.247719764709473
+ ],
+ [
+ "▁title",
+ -10.247977256774902
+ ],
+ [
+ "ric",
+ -10.248047828674316
+ ],
+ [
+ "berg",
+ -10.248252868652344
+ ],
+ [
+ "▁ainsi",
+ -10.248576164245605
+ ],
+ [
+ "▁led",
+ -10.248713493347168
+ ],
+ [
+ "▁alone",
+ -10.248786926269531
+ ],
+ [
+ "ward",
+ -10.249215126037598
+ ],
+ [
+ "▁vie",
+ -10.249323844909668
+ ],
+ [
+ "▁brain",
+ -10.249427795410156
+ ],
+ [
+ "light",
+ -10.250100135803223
+ ],
+ [
+ "▁Court",
+ -10.250598907470703
+ ],
+ [
+ "set",
+ -10.250869750976562
+ ],
+ [
+ "▁steps",
+ -10.251251220703125
+ ],
+ [
+ "pri",
+ -10.251391410827637
+ ],
+ [
+ "Q",
+ -10.251654624938965
+ ],
+ [
+ "sti",
+ -10.251938819885254
+ ],
+ [
+ "▁voice",
+ -10.252121925354004
+ ],
+ [
+ "▁models",
+ -10.252705574035645
+ ],
+ [
+ "▁parties",
+ -10.25442886352539
+ ],
+ [
+ "▁radio",
+ -10.255270957946777
+ ],
+ [
+ "▁mission",
+ -10.25545883178711
+ ],
+ [
+ "▁methods",
+ -10.255658149719238
+ ],
+ [
+ "▁Te",
+ -10.256019592285156
+ ],
+ [
+ "air",
+ -10.256489753723145
+ ],
+ [
+ "▁essay",
+ -10.256719589233398
+ ],
+ [
+ "my",
+ -10.256826400756836
+ ],
+ [
+ "▁competition",
+ -10.257049560546875
+ ],
+ [
+ "ses",
+ -10.257447242736816
+ ],
+ [
+ "▁serious",
+ -10.258724212646484
+ ],
+ [
+ "▁Ti",
+ -10.258733749389648
+ ],
+ [
+ "▁Hand",
+ -10.259561538696289
+ ],
+ [
+ "not",
+ -10.25958251953125
+ ],
+ [
+ "▁winter",
+ -10.261277198791504
+ ],
+ [
+ "24",
+ -10.261724472045898
+ ],
+ [
+ "▁vision",
+ -10.26174545288086
+ ],
+ [
+ "▁technical",
+ -10.262110710144043
+ ],
+ [
+ "▁cross",
+ -10.262799263000488
+ ],
+ [
+ "▁update",
+ -10.262947082519531
+ ],
+ [
+ "▁Team",
+ -10.263564109802246
+ ],
+ [
+ "▁evening",
+ -10.264286041259766
+ ],
+ [
+ "▁experts",
+ -10.26435661315918
+ ],
+ [
+ "part",
+ -10.264640808105469
+ ],
+ [
+ "▁wo",
+ -10.265190124511719
+ ],
+ [
+ "▁App",
+ -10.265729904174805
+ ],
+ [
+ "▁peu",
+ -10.266267776489258
+ ],
+ [
+ "▁mich",
+ -10.26630687713623
+ ],
+ [
+ "▁reports",
+ -10.267001152038574
+ ],
+ [
+ "▁km",
+ -10.267594337463379
+ ],
+ [
+ "▁print",
+ -10.2678804397583
+ ],
+ [
+ "▁Hotel",
+ -10.268101692199707
+ ],
+ [
+ "▁earlier",
+ -10.268235206604004
+ ],
+ [
+ "▁uses",
+ -10.26826286315918
+ ],
+ [
+ "▁menu",
+ -10.268416404724121
+ ],
+ [
+ "▁miles",
+ -10.26845645904541
+ ],
+ [
+ "▁classes",
+ -10.268463134765625
+ ],
+ [
+ "▁mo",
+ -10.268525123596191
+ ],
+ [
+ "▁loan",
+ -10.2691011428833
+ ],
+ [
+ "▁host",
+ -10.269192695617676
+ ],
+ [
+ "▁author",
+ -10.269274711608887
+ ],
+ [
+ "-1",
+ -10.269434928894043
+ ],
+ [
+ "▁bun",
+ -10.269940376281738
+ ],
+ [
+ "19",
+ -10.270011901855469
+ ],
+ [
+ "uch",
+ -10.270670890808105
+ ],
+ [
+ "ble",
+ -10.270813941955566
+ ],
+ [
+ "▁holiday",
+ -10.270859718322754
+ ],
+ [
+ "los",
+ -10.271894454956055
+ ],
+ [
+ "▁looked",
+ -10.272663116455078
+ ],
+ [
+ "▁Test",
+ -10.272759437561035
+ ],
+ [
+ "▁moved",
+ -10.273000717163086
+ ],
+ [
+ "▁numbers",
+ -10.273306846618652
+ ],
+ [
+ "▁covered",
+ -10.273405075073242
+ ],
+ [
+ "ker",
+ -10.273696899414062
+ ],
+ [
+ "TM",
+ -10.273768424987793
+ ],
+ [
+ "▁album",
+ -10.274727821350098
+ ],
+ [
+ "▁27",
+ -10.27476692199707
+ ],
+ [
+ "▁când",
+ -10.27523422241211
+ ],
+ [
+ "▁shopping",
+ -10.275248527526855
+ ],
+ [
+ "▁Ihr",
+ -10.27531623840332
+ ],
+ [
+ "▁requires",
+ -10.275786399841309
+ ],
+ [
+ "▁USA",
+ -10.275909423828125
+ ],
+ [
+ "000",
+ -10.275951385498047
+ ],
+ [
+ "▁official",
+ -10.276010513305664
+ ],
+ [
+ "▁states",
+ -10.276346206665039
+ ],
+ [
+ "▁tips",
+ -10.276570320129395
+ ],
+ [
+ "ible",
+ -10.277321815490723
+ ],
+ [
+ "▁Lu",
+ -10.27756404876709
+ ],
+ [
+ "ces",
+ -10.278343200683594
+ ],
+ [
+ "▁figure",
+ -10.27839469909668
+ ],
+ [
+ "▁Take",
+ -10.278576850891113
+ ],
+ [
+ "▁după",
+ -10.278687477111816
+ ],
+ [
+ "▁teams",
+ -10.278980255126953
+ ],
+ [
+ "▁song",
+ -10.279138565063477
+ ],
+ [
+ "▁master",
+ -10.279386520385742
+ ],
+ [
+ "ED",
+ -10.279841423034668
+ ],
+ [
+ "▁cleaning",
+ -10.280523300170898
+ ],
+ [
+ "▁drop",
+ -10.280651092529297
+ ],
+ [
+ "▁primary",
+ -10.2808837890625
+ ],
+ [
+ "▁Life",
+ -10.28108024597168
+ ],
+ [
+ "▁carry",
+ -10.281129837036133
+ ],
+ [
+ "▁initial",
+ -10.281270980834961
+ ],
+ [
+ "▁encore",
+ -10.281617164611816
+ ],
+ [
+ "▁Add",
+ -10.281670570373535
+ ],
+ [
+ "▁woman",
+ -10.282076835632324
+ ],
+ [
+ "▁Water",
+ -10.282219886779785
+ ],
+ [
+ "▁advantage",
+ -10.28277587890625
+ ],
+ [
+ "see",
+ -10.283234596252441
+ ],
+ [
+ "ré",
+ -10.283341407775879
+ ],
+ [
+ "▁motor",
+ -10.283479690551758
+ ],
+ [
+ "mel",
+ -10.2838716506958
+ ],
+ [
+ "▁finding",
+ -10.284419059753418
+ ],
+ [
+ "▁plastic",
+ -10.286365509033203
+ ],
+ [
+ "▁IT",
+ -10.286602973937988
+ ],
+ [
+ "▁Church",
+ -10.286916732788086
+ ],
+ [
+ "▁shape",
+ -10.287345886230469
+ ],
+ [
+ "▁gets",
+ -10.287763595581055
+ ],
+ [
+ "▁followed",
+ -10.288186073303223
+ ],
+ [
+ "▁100%",
+ -10.288315773010254
+ ],
+ [
+ "▁Program",
+ -10.28912353515625
+ ],
+ [
+ "▁Another",
+ -10.28934383392334
+ ],
+ [
+ "▁zwei",
+ -10.289522171020508
+ ],
+ [
+ "▁father",
+ -10.289839744567871
+ ],
+ [
+ "▁rich",
+ -10.290282249450684
+ ],
+ [
+ "où",
+ -10.290810585021973
+ ],
+ [
+ "▁lines",
+ -10.290934562683105
+ ],
+ [
+ "▁distance",
+ -10.291757583618164
+ ],
+ [
+ "▁cell",
+ -10.291876792907715
+ ],
+ [
+ "▁parte",
+ -10.292072296142578
+ ],
+ [
+ "bit",
+ -10.292445182800293
+ ],
+ [
+ "▁perhaps",
+ -10.292749404907227
+ ],
+ [
+ "rii",
+ -10.293590545654297
+ ],
+ [
+ "▁session",
+ -10.294137954711914
+ ],
+ [
+ "▁Pentru",
+ -10.294528007507324
+ ],
+ [
+ "ING",
+ -10.295049667358398
+ ],
+ [
+ "ants",
+ -10.295478820800781
+ ],
+ [
+ "▁remain",
+ -10.295543670654297
+ ],
+ [
+ "13",
+ -10.295588493347168
+ ],
+ [
+ "▁finished",
+ -10.295763969421387
+ ],
+ [
+ "bel",
+ -10.298725128173828
+ ],
+ [
+ "▁organizations",
+ -10.299455642700195
+ ],
+ [
+ "▁Any",
+ -10.299896240234375
+ ],
+ [
+ "▁taste",
+ -10.300277709960938
+ ],
+ [
+ "Whether",
+ -10.300600051879883
+ ],
+ [
+ "ram",
+ -10.300874710083008
+ ],
+ [
+ "like",
+ -10.301307678222656
+ ],
+ [
+ "▁artist",
+ -10.301319122314453
+ ],
+ [
+ "aire",
+ -10.303369522094727
+ ],
+ [
+ "▁French",
+ -10.303386688232422
+ ],
+ [
+ "▁donc",
+ -10.303634643554688
+ ],
+ [
+ "ow",
+ -10.30386734008789
+ ],
+ [
+ "▁200",
+ -10.303993225097656
+ ],
+ [
+ "▁paint",
+ -10.304465293884277
+ ],
+ [
+ "▁Open",
+ -10.304535865783691
+ ],
+ [
+ "▁appear",
+ -10.304722785949707
+ ],
+ [
+ "▁Washington",
+ -10.304765701293945
+ ],
+ [
+ "▁target",
+ -10.30491828918457
+ ],
+ [
+ "pir",
+ -10.305578231811523
+ ],
+ [
+ "▁generally",
+ -10.305987358093262
+ ],
+ [
+ "▁British",
+ -10.306790351867676
+ ],
+ [
+ "▁seven",
+ -10.306937217712402
+ ],
+ [
+ "▁bio",
+ -10.307162284851074
+ ],
+ [
+ "▁sector",
+ -10.307358741760254
+ ],
+ [
+ "90",
+ -10.30777359008789
+ ],
+ [
+ "▁fapt",
+ -10.307881355285645
+ ],
+ [
+ "▁prefer",
+ -10.308316230773926
+ ],
+ [
+ "▁partner",
+ -10.308427810668945
+ ],
+ [
+ "ăm",
+ -10.308547973632812
+ ],
+ [
+ "▁diverse",
+ -10.308610916137695
+ ],
+ [
+ "▁onto",
+ -10.309283256530762
+ ],
+ [
+ "▁refer",
+ -10.309828758239746
+ ],
+ [
+ "▁Law",
+ -10.310302734375
+ ],
+ [
+ "▁Ri",
+ -10.310596466064453
+ ],
+ [
+ "▁critical",
+ -10.310735702514648
+ ],
+ [
+ "▁copy",
+ -10.310897827148438
+ ],
+ [
+ "ck",
+ -10.311517715454102
+ ],
+ [
+ "ix",
+ -10.311732292175293
+ ],
+ [
+ "tag",
+ -10.311793327331543
+ ],
+ [
+ "▁Road",
+ -10.311936378479004
+ ],
+ [
+ "▁concern",
+ -10.312053680419922
+ ],
+ [
+ "▁maximum",
+ -10.312095642089844
+ ],
+ [
+ "▁train",
+ -10.312148094177246
+ ],
+ [
+ "▁într",
+ -10.312189102172852
+ ],
+ [
+ "ura",
+ -10.313023567199707
+ ],
+ [
+ "▁Qu",
+ -10.313481330871582
+ ],
+ [
+ "▁links",
+ -10.313538551330566
+ ],
+ [
+ "▁audience",
+ -10.313969612121582
+ ],
+ [
+ "▁foot",
+ -10.314554214477539
+ ],
+ [
+ "▁Blue",
+ -10.314605712890625
+ ],
+ [
+ "ification",
+ -10.315386772155762
+ ],
+ [
+ "▁developing",
+ -10.315847396850586
+ ],
+ [
+ "▁interior",
+ -10.315876007080078
+ ],
+ [
+ "=",
+ -10.316556930541992
+ ],
+ [
+ "▁aceasta",
+ -10.31698989868164
+ ],
+ [
+ "▁dedicated",
+ -10.317373275756836
+ ],
+ [
+ "▁movement",
+ -10.317383766174316
+ ],
+ [
+ "sta",
+ -10.318868637084961
+ ],
+ [
+ "▁challenges",
+ -10.319018363952637
+ ],
+ [
+ "inte",
+ -10.319074630737305
+ ],
+ [
+ "▁Euro",
+ -10.319075584411621
+ ],
+ [
+ "▁classic",
+ -10.320341110229492
+ ],
+ [
+ "▁Um",
+ -10.320767402648926
+ ],
+ [
+ "▁alternative",
+ -10.321407318115234
+ ],
+ [
+ "mann",
+ -10.321614265441895
+ ],
+ [
+ "▁Une",
+ -10.322278022766113
+ ],
+ [
+ "qu",
+ -10.322415351867676
+ ],
+ [
+ "▁heavy",
+ -10.322434425354004
+ ],
+ [
+ "▁install",
+ -10.322484970092773
+ ],
+ [
+ "▁fiind",
+ -10.322504043579102
+ ],
+ [
+ "▁leaders",
+ -10.323003768920898
+ ],
+ [
+ "▁views",
+ -10.323019981384277
+ ],
+ [
+ "▁www",
+ -10.323084831237793
+ ],
+ [
+ "▁standards",
+ -10.323270797729492
+ ],
+ [
+ "ong",
+ -10.323580741882324
+ ],
+ [
+ "40",
+ -10.323833465576172
+ ],
+ [
+ "▁cm",
+ -10.323848724365234
+ ],
+ [
+ "▁park",
+ -10.324324607849121
+ ],
+ [
+ "▁himself",
+ -10.324419021606445
+ ],
+ [
+ "▁People",
+ -10.324649810791016
+ ],
+ [
+ "▁separate",
+ -10.324843406677246
+ ],
+ [
+ "▁secure",
+ -10.325018882751465
+ ],
+ [
+ "sie",
+ -10.325084686279297
+ ],
+ [
+ "▁maintenance",
+ -10.325199127197266
+ ],
+ [
+ "▁encourage",
+ -10.32766056060791
+ ],
+ [
+ "ein",
+ -10.328139305114746
+ ],
+ [
+ "▁reviews",
+ -10.328202247619629
+ ],
+ [
+ "▁Michael",
+ -10.328210830688477
+ ],
+ [
+ "▁background",
+ -10.328283309936523
+ ],
+ [
+ "▁therefore",
+ -10.328433990478516
+ ],
+ [
+ "▁server",
+ -10.328487396240234
+ ],
+ [
+ "▁dream",
+ -10.328742027282715
+ ],
+ [
+ "ping",
+ -10.329025268554688
+ ],
+ [
+ "▁block",
+ -10.329855918884277
+ ],
+ [
+ "▁2009",
+ -10.330734252929688
+ ],
+ [
+ "▁facilities",
+ -10.330931663513184
+ ],
+ [
+ "▁II",
+ -10.331367492675781
+ ],
+ [
+ "▁attend",
+ -10.33156967163086
+ ],
+ [
+ "▁cap",
+ -10.33224105834961
+ ],
+ [
+ "35",
+ -10.332416534423828
+ ],
+ [
+ "▁steel",
+ -10.332796096801758
+ ],
+ [
+ "▁shared",
+ -10.333391189575195
+ ],
+ [
+ "▁doctor",
+ -10.333939552307129
+ ],
+ [
+ "▁River",
+ -10.33411693572998
+ ],
+ [
+ "▁Bay",
+ -10.334456443786621
+ ],
+ [
+ "▁length",
+ -10.335005760192871
+ ],
+ [
+ "▁jobs",
+ -10.335466384887695
+ ],
+ [
+ "▁Plus",
+ -10.335992813110352
+ ],
+ [
+ "▁station",
+ -10.336140632629395
+ ],
+ [
+ "▁elements",
+ -10.336268424987793
+ ],
+ [
+ "▁rock",
+ -10.336668014526367
+ ],
+ [
+ "▁professionals",
+ -10.336670875549316
+ ],
+ [
+ "cle",
+ -10.336777687072754
+ ],
+ [
+ "▁dont",
+ -10.336873054504395
+ ],
+ [
+ "urilor",
+ -10.337142944335938
+ ],
+ [
+ "▁gain",
+ -10.337271690368652
+ ],
+ [
+ "▁programme",
+ -10.337540626525879
+ ],
+ [
+ "▁Cor",
+ -10.338377952575684
+ ],
+ [
+ "▁leader",
+ -10.338542938232422
+ ],
+ [
+ "ării",
+ -10.33876895904541
+ ],
+ [
+ "▁>",
+ -10.339137077331543
+ ],
+ [
+ "▁task",
+ -10.339471817016602
+ ],
+ [
+ "▁seeing",
+ -10.339943885803223
+ ],
+ [
+ "▁statement",
+ -10.34045696258545
+ ],
+ [
+ "vin",
+ -10.341094017028809
+ ],
+ [
+ "▁fish",
+ -10.341700553894043
+ ],
+ [
+ "▁advanced",
+ -10.342403411865234
+ ],
+ [
+ "▁discuss",
+ -10.342494010925293
+ ],
+ [
+ "die",
+ -10.342904090881348
+ ],
+ [
+ "isch",
+ -10.342944145202637
+ ],
+ [
+ "▁plenty",
+ -10.342947959899902
+ ],
+ [
+ "▁Hall",
+ -10.343120574951172
+ ],
+ [
+ "▁Other",
+ -10.343339920043945
+ ],
+ [
+ "▁homes",
+ -10.344944953918457
+ ],
+ [
+ "▁Ni",
+ -10.345016479492188
+ ],
+ [
+ "▁testing",
+ -10.345102310180664
+ ],
+ [
+ "▁Last",
+ -10.345392227172852
+ ],
+ [
+ "▁Note",
+ -10.345595359802246
+ ],
+ [
+ "▁talking",
+ -10.345934867858887
+ ],
+ [
+ "▁exchange",
+ -10.347042083740234
+ ],
+ [
+ "▁exercise",
+ -10.347189903259277
+ ],
+ [
+ "▁cea",
+ -10.347546577453613
+ ],
+ [
+ "▁wife",
+ -10.34820556640625
+ ],
+ [
+ "▁Für",
+ -10.348480224609375
+ ],
+ [
+ "▁Texas",
+ -10.34981918334961
+ ],
+ [
+ "▁fr",
+ -10.35065746307373
+ ],
+ [
+ "▁speak",
+ -10.350894927978516
+ ],
+ [
+ "17",
+ -10.351007461547852
+ ],
+ [
+ "70",
+ -10.351462364196777
+ ],
+ [
+ "▁promote",
+ -10.351851463317871
+ ],
+ [
+ "tul",
+ -10.351990699768066
+ ],
+ [
+ "apos",
+ -10.35208511352539
+ ],
+ [
+ "▁Jahr",
+ -10.35214900970459
+ ],
+ [
+ "▁Trump",
+ -10.352204322814941
+ ],
+ [
+ "▁ohne",
+ -10.352357864379883
+ ],
+ [
+ "▁learned",
+ -10.353700637817383
+ ],
+ [
+ "▁Sp",
+ -10.353803634643555
+ ],
+ [
+ "▁owner",
+ -10.354275703430176
+ ],
+ [
+ "mor",
+ -10.354422569274902
+ ],
+ [
+ "▁fois",
+ -10.354452133178711
+ ],
+ [
+ "▁meaning",
+ -10.35518741607666
+ ],
+ [
+ "▁dacă",
+ -10.355249404907227
+ ],
+ [
+ "nic",
+ -10.355484008789062
+ ],
+ [
+ "а",
+ -10.355525970458984
+ ],
+ [
+ "14",
+ -10.355767250061035
+ ],
+ [
+ "▁driver",
+ -10.356258392333984
+ ],
+ [
+ "▁Amazon",
+ -10.3567533493042
+ ],
+ [
+ "▁flow",
+ -10.358469009399414
+ ],
+ [
+ "▁shot",
+ -10.358726501464844
+ ],
+ [
+ "▁sous",
+ -10.35914421081543
+ ],
+ [
+ "▁Gold",
+ -10.359339714050293
+ ],
+ [
+ "▁straight",
+ -10.359562873840332
+ ],
+ [
+ "▁conference",
+ -10.359610557556152
+ ],
+ [
+ "▁peste",
+ -10.359662055969238
+ ],
+ [
+ "whose",
+ -10.36030101776123
+ ],
+ [
+ "▁installation",
+ -10.36050796508789
+ ],
+ [
+ "▁produced",
+ -10.360607147216797
+ ],
+ [
+ "▁independent",
+ -10.36192512512207
+ ],
+ [
+ "▁Institute",
+ -10.362021446228027
+ ],
+ [
+ "▁James",
+ -10.362373352050781
+ ],
+ [
+ "▁mental",
+ -10.362601280212402
+ ],
+ [
+ "ara",
+ -10.362798690795898
+ ],
+ [
+ "ium",
+ -10.363021850585938
+ ],
+ [
+ "▁husband",
+ -10.36306095123291
+ ],
+ [
+ "▁guests",
+ -10.363907814025879
+ ],
+ [
+ "27",
+ -10.364319801330566
+ ],
+ [
+ "▁Che",
+ -10.364651679992676
+ ],
+ [
+ "▁Indian",
+ -10.364694595336914
+ ],
+ [
+ "zer",
+ -10.36478042602539
+ ],
+ [
+ "▁minimum",
+ -10.364962577819824
+ ],
+ [
+ "500",
+ -10.365096092224121
+ ],
+ [
+ "▁sit",
+ -10.36561393737793
+ ],
+ [
+ "put",
+ -10.36656379699707
+ ],
+ [
+ "▁avea",
+ -10.36665153503418
+ ],
+ [
+ "▁ride",
+ -10.367088317871094
+ ],
+ [
+ "gan",
+ -10.367152214050293
+ ],
+ [
+ "▁Ke",
+ -10.36747932434082
+ ],
+ [
+ "book",
+ -10.367515563964844
+ ],
+ [
+ "ages",
+ -10.368019104003906
+ ],
+ [
+ "▁presented",
+ -10.368157386779785
+ ],
+ [
+ "▁Com",
+ -10.368927955627441
+ ],
+ [
+ "▁Call",
+ -10.369053840637207
+ ],
+ [
+ "▁fee",
+ -10.369847297668457
+ ],
+ [
+ "ări",
+ -10.369905471801758
+ ],
+ [
+ "▁putea",
+ -10.37072467803955
+ ],
+ [
+ "▁Public",
+ -10.371030807495117
+ ],
+ [
+ "▁pa",
+ -10.371152877807617
+ ],
+ [
+ "28",
+ -10.371233940124512
+ ],
+ [
+ "▁Director",
+ -10.37126350402832
+ ],
+ [
+ "▁contains",
+ -10.3717622756958
+ ],
+ [
+ "▁factors",
+ -10.372554779052734
+ ],
+ [
+ "▁famous",
+ -10.372614860534668
+ ],
+ [
+ "▁bathroom",
+ -10.373040199279785
+ ],
+ [
+ "▁core",
+ -10.37353229522705
+ ],
+ [
+ "▁viele",
+ -10.373610496520996
+ ],
+ [
+ "▁acum",
+ -10.374361991882324
+ ],
+ [
+ "▁animal",
+ -10.374407768249512
+ ],
+ [
+ "▁Ihnen",
+ -10.374425888061523
+ ],
+ [
+ "▁Find",
+ -10.374545097351074
+ ],
+ [
+ "▁Fall",
+ -10.374861717224121
+ ],
+ [
+ "ford",
+ -10.376051902770996
+ ],
+ [
+ "▁coverage",
+ -10.3765287399292
+ ],
+ [
+ "▁smart",
+ -10.376830101013184
+ ],
+ [
+ "ries",
+ -10.376893997192383
+ ],
+ [
+ "▁memory",
+ -10.3772554397583
+ ],
+ [
+ "▁dance",
+ -10.377443313598633
+ ],
+ [
+ "11",
+ -10.37746810913086
+ ],
+ [
+ "▁communities",
+ -10.377655982971191
+ ],
+ [
+ "eurs",
+ -10.378050804138184
+ ],
+ [
+ "▁Florida",
+ -10.378463745117188
+ ],
+ [
+ "▁sport",
+ -10.379366874694824
+ ],
+ [
+ "▁bus",
+ -10.37992000579834
+ ],
+ [
+ "▁colors",
+ -10.379969596862793
+ ],
+ [
+ "▁affect",
+ -10.380044937133789
+ ],
+ [
+ "▁score",
+ -10.380183219909668
+ ],
+ [
+ "▁properties",
+ -10.38050365447998
+ ],
+ [
+ "18",
+ -10.380593299865723
+ ],
+ [
+ "▁astfel",
+ -10.381312370300293
+ ],
+ [
+ "▁beach",
+ -10.382407188415527
+ ],
+ [
+ "▁friendly",
+ -10.382795333862305
+ ],
+ [
+ "izing",
+ -10.38288688659668
+ ],
+ [
+ "▁buying",
+ -10.383146286010742
+ ],
+ [
+ "▁forget",
+ -10.383195877075195
+ ],
+ [
+ "este",
+ -10.383198738098145
+ ],
+ [
+ "▁capacity",
+ -10.38360595703125
+ ],
+ [
+ "▁lose",
+ -10.383692741394043
+ ],
+ [
+ "▁listed",
+ -10.38407039642334
+ ],
+ [
+ "ica",
+ -10.384084701538086
+ ],
+ [
+ "han",
+ -10.384085655212402
+ ],
+ [
+ "▁selbst",
+ -10.384390830993652
+ ],
+ [
+ "▁values",
+ -10.384391784667969
+ ],
+ [
+ "▁Power",
+ -10.384559631347656
+ ],
+ [
+ "▁comments",
+ -10.384831428527832
+ ],
+ [
+ "eux",
+ -10.385346412658691
+ ],
+ [
+ "ați",
+ -10.385419845581055
+ ],
+ [
+ "▁context",
+ -10.385710716247559
+ ],
+ [
+ "liche",
+ -10.385944366455078
+ ],
+ [
+ "▁keeping",
+ -10.38620662689209
+ ],
+ [
+ "▁2008",
+ -10.38647174835205
+ ],
+ [
+ "▁su",
+ -10.386670112609863
+ ],
+ [
+ "▁biggest",
+ -10.386838912963867
+ ],
+ [
+ "▁fiecare",
+ -10.387356758117676
+ ],
+ [
+ "ight",
+ -10.38845157623291
+ ],
+ [
+ "▁toute",
+ -10.389808654785156
+ ],
+ [
+ "▁dinner",
+ -10.389827728271484
+ ],
+ [
+ "bau",
+ -10.390706062316895
+ ],
+ [
+ "▁Mai",
+ -10.390762329101562
+ ],
+ [
+ "▁status",
+ -10.390776634216309
+ ],
+ [
+ "rez",
+ -10.391340255737305
+ ],
+ [
+ "▁selected",
+ -10.391549110412598
+ ],
+ [
+ "▁cells",
+ -10.392601013183594
+ ],
+ [
+ "▁eight",
+ -10.393319129943848
+ ],
+ [
+ "▁package",
+ -10.393320083618164
+ ],
+ [
+ "▁scale",
+ -10.39333724975586
+ ],
+ [
+ "din",
+ -10.39336109161377
+ ],
+ [
+ "▁Who",
+ -10.393381118774414
+ ],
+ [
+ "▁century",
+ -10.393399238586426
+ ],
+ [
+ "▁bi",
+ -10.393516540527344
+ ],
+ [
+ "▁Africa",
+ -10.39384937286377
+ ],
+ [
+ "▁http",
+ -10.394133567810059
+ ],
+ [
+ "▁named",
+ -10.394230842590332
+ ],
+ [
+ "▁adding",
+ -10.394901275634766
+ ],
+ [
+ "▁mention",
+ -10.395039558410645
+ ],
+ [
+ "▁casino",
+ -10.395421981811523
+ ],
+ [
+ "▁couldn",
+ -10.395624160766602
+ ],
+ [
+ "▁outdoor",
+ -10.395912170410156
+ ],
+ [
+ "▁sugar",
+ -10.3960542678833
+ ],
+ [
+ "▁prepared",
+ -10.396124839782715
+ ],
+ [
+ "21",
+ -10.396528244018555
+ ],
+ [
+ "▁Ba",
+ -10.396632194519043
+ ],
+ [
+ "vers",
+ -10.396697998046875
+ ],
+ [
+ "ration",
+ -10.396773338317871
+ ],
+ [
+ "▁ja",
+ -10.397035598754883
+ ],
+ [
+ "▁aspect",
+ -10.397224426269531
+ ],
+ [
+ "▁31",
+ -10.397462844848633
+ ],
+ [
+ "▁treat",
+ -10.397475242614746
+ ],
+ [
+ "tru",
+ -10.397841453552246
+ ],
+ [
+ "▁flat",
+ -10.397890090942383
+ ],
+ [
+ "32",
+ -10.397989273071289
+ ],
+ [
+ "▁reality",
+ -10.398238182067871
+ ],
+ [
+ "▁waste",
+ -10.39876937866211
+ ],
+ [
+ "▁King",
+ -10.399649620056152
+ ],
+ [
+ "▁drug",
+ -10.399870872497559
+ ],
+ [
+ "▁operations",
+ -10.400120735168457
+ ],
+ [
+ "▁aim",
+ -10.40042495727539
+ ],
+ [
+ "▁fans",
+ -10.400444984436035
+ ],
+ [
+ "▁vers",
+ -10.400891304016113
+ ],
+ [
+ "▁plants",
+ -10.400971412658691
+ ],
+ [
+ "▁Dis",
+ -10.401477813720703
+ ],
+ [
+ "▁Daten",
+ -10.401510238647461
+ ],
+ [
+ "être",
+ -10.40267276763916
+ ],
+ [
+ "▁placed",
+ -10.40326976776123
+ ],
+ [
+ "▁bon",
+ -10.403977394104004
+ ],
+ [
+ "beim",
+ -10.4041109085083
+ ],
+ [
+ "▁slow",
+ -10.40501880645752
+ ],
+ [
+ "cri",
+ -10.405512809753418
+ ],
+ [
+ "▁Care",
+ -10.405691146850586
+ ],
+ [
+ "mes",
+ -10.406211853027344
+ ],
+ [
+ "26",
+ -10.406257629394531
+ ],
+ [
+ "box",
+ -10.406330108642578
+ ],
+ [
+ "▁helpful",
+ -10.406362533569336
+ ],
+ [
+ "▁documents",
+ -10.406543731689453
+ ],
+ [
+ "▁visitors",
+ -10.406773567199707
+ ],
+ [
+ "ture",
+ -10.406862258911133
+ ],
+ [
+ "▁Menschen",
+ -10.406891822814941
+ ],
+ [
+ "▁Chi",
+ -10.406975746154785
+ ],
+ [
+ "▁recipe",
+ -10.40764045715332
+ ],
+ [
+ "▁kept",
+ -10.407693862915039
+ ],
+ [
+ "▁Grand",
+ -10.407915115356445
+ ],
+ [
+ "▁operating",
+ -10.408178329467773
+ ],
+ [
+ "point",
+ -10.408329010009766
+ ],
+ [
+ "▁bin",
+ -10.40837287902832
+ ],
+ [
+ "▁Tri",
+ -10.40845775604248
+ ],
+ [
+ "Be",
+ -10.408512115478516
+ ],
+ [
+ "▁experiences",
+ -10.40856647491455
+ ],
+ [
+ "▁academic",
+ -10.408608436584473
+ ],
+ [
+ "▁finden",
+ -10.40870475769043
+ ],
+ [
+ "▁sera",
+ -10.409092903137207
+ ],
+ [
+ "act",
+ -10.410541534423828
+ ],
+ [
+ "▁Pa",
+ -10.410907745361328
+ ],
+ [
+ "▁society",
+ -10.411056518554688
+ ],
+ [
+ "▁combination",
+ -10.411237716674805
+ ],
+ [
+ "5%",
+ -10.41182804107666
+ ],
+ [
+ "▁owners",
+ -10.41188907623291
+ ],
+ [
+ "▁poor",
+ -10.412039756774902
+ ],
+ [
+ "▁Robert",
+ -10.412378311157227
+ ],
+ [
+ "▁military",
+ -10.412964820861816
+ ],
+ [
+ "▁economy",
+ -10.413033485412598
+ ],
+ [
+ "▁aware",
+ -10.413055419921875
+ ],
+ [
+ "rot",
+ -10.413443565368652
+ ],
+ [
+ "mie",
+ -10.413544654846191
+ ],
+ [
+ "▁Thursday",
+ -10.414399147033691
+ ],
+ [
+ "▁2011",
+ -10.41490650177002
+ ],
+ [
+ "▁fantastic",
+ -10.41554069519043
+ ],
+ [
+ "▁numerous",
+ -10.415921211242676
+ ],
+ [
+ "▁fair",
+ -10.4165620803833
+ ],
+ [
+ "med",
+ -10.416753768920898
+ ],
+ [
+ "▁welche",
+ -10.416893005371094
+ ],
+ [
+ "▁fruit",
+ -10.41712760925293
+ ],
+ [
+ "ku",
+ -10.417325019836426
+ ],
+ [
+ "▁Social",
+ -10.417583465576172
+ ],
+ [
+ "▁funds",
+ -10.418157577514648
+ ],
+ [
+ "▁atunci",
+ -10.418214797973633
+ ],
+ [
+ "▁Part",
+ -10.418238639831543
+ ],
+ [
+ "▁Big",
+ -10.418301582336426
+ ],
+ [
+ "▁2010",
+ -10.419414520263672
+ ],
+ [
+ "▁detail",
+ -10.419889450073242
+ ],
+ [
+ "▁Peter",
+ -10.419942855834961
+ ],
+ [
+ "ani",
+ -10.420196533203125
+ ],
+ [
+ "▁Wie",
+ -10.420795440673828
+ ],
+ [
+ "▁Tu",
+ -10.421649932861328
+ ],
+ [
+ "ear",
+ -10.421706199645996
+ ],
+ [
+ "▁Wenn",
+ -10.421941757202148
+ ],
+ [
+ "▁manager",
+ -10.42199993133545
+ ],
+ [
+ "▁Dan",
+ -10.422409057617188
+ ],
+ [
+ "▁Pi",
+ -10.42257308959961
+ ],
+ [
+ "▁wants",
+ -10.422652244567871
+ ],
+ [
+ "▁Data",
+ -10.42322826385498
+ ],
+ [
+ "pos",
+ -10.42387580871582
+ ],
+ [
+ "▁older",
+ -10.423946380615234
+ ],
+ [
+ "▁Download",
+ -10.424071311950684
+ ],
+ [
+ "▁Was",
+ -10.424107551574707
+ ],
+ [
+ "▁corner",
+ -10.424195289611816
+ ],
+ [
+ "▁president",
+ -10.424199104309082
+ ],
+ [
+ "mas",
+ -10.424248695373535
+ ],
+ [
+ "▁smaller",
+ -10.424361228942871
+ ],
+ [
+ "▁bright",
+ -10.424459457397461
+ ],
+ [
+ "▁proper",
+ -10.424582481384277
+ ],
+ [
+ "▁Kinder",
+ -10.424637794494629
+ ],
+ [
+ "▁Two",
+ -10.424668312072754
+ ],
+ [
+ "▁award",
+ -10.42471694946289
+ ],
+ [
+ "▁premier",
+ -10.425211906433105
+ ],
+ [
+ "▁seek",
+ -10.425646781921387
+ ],
+ [
+ "▁thank",
+ -10.425662994384766
+ ],
+ [
+ "▁proud",
+ -10.426509857177734
+ ],
+ [
+ "▁workers",
+ -10.426774024963379
+ ],
+ [
+ "▁2000",
+ -10.426970481872559
+ ],
+ [
+ "▁gone",
+ -10.427482604980469
+ ],
+ [
+ "▁medium",
+ -10.427693367004395
+ ],
+ [
+ "▁grade",
+ -10.42777156829834
+ ],
+ [
+ "▁Ru",
+ -10.427800178527832
+ ],
+ [
+ "cro",
+ -10.427851676940918
+ ],
+ [
+ "▁interview",
+ -10.428311347961426
+ ],
+ [
+ "23",
+ -10.428787231445312
+ ],
+ [
+ "▁mari",
+ -10.429442405700684
+ ],
+ [
+ "▁80",
+ -10.429756164550781
+ ],
+ [
+ "▁Ga",
+ -10.430047035217285
+ ],
+ [
+ "▁90",
+ -10.431839942932129
+ ],
+ [
+ "▁anderen",
+ -10.432605743408203
+ ],
+ [
+ "▁cultural",
+ -10.433018684387207
+ ],
+ [
+ "but",
+ -10.433144569396973
+ ],
+ [
+ "rum",
+ -10.433300018310547
+ ],
+ [
+ "get",
+ -10.43338680267334
+ ],
+ [
+ "▁pop",
+ -10.433582305908203
+ ],
+ [
+ "▁Information",
+ -10.433594703674316
+ ],
+ [
+ "▁press",
+ -10.434972763061523
+ ],
+ [
+ "▁Project",
+ -10.435359001159668
+ ],
+ [
+ "▁excited",
+ -10.435755729675293
+ ],
+ [
+ "▁Saint",
+ -10.436088562011719
+ ],
+ [
+ "▁England",
+ -10.436192512512207
+ ],
+ [
+ "▁beauty",
+ -10.43643856048584
+ ],
+ [
+ "▁agreement",
+ -10.436464309692383
+ ],
+ [
+ "▁Like",
+ -10.437565803527832
+ ],
+ [
+ "▁strength",
+ -10.437664985656738
+ ],
+ [
+ "▁waiting",
+ -10.438165664672852
+ ],
+ [
+ "и",
+ -10.438270568847656
+ ],
+ [
+ "Le",
+ -10.438329696655273
+ ],
+ [
+ "▁residents",
+ -10.43835735321045
+ ],
+ [
+ "▁Ben",
+ -10.438603401184082
+ ],
+ [
+ "▁mentioned",
+ -10.439260482788086
+ ],
+ [
+ "▁etwas",
+ -10.43930721282959
+ ],
+ [
+ "▁rooms",
+ -10.439347267150879
+ ],
+ [
+ "▁neue",
+ -10.439501762390137
+ ],
+ [
+ "▁Microsoft",
+ -10.439726829528809
+ ],
+ [
+ "▁passed",
+ -10.440205574035645
+ ],
+ [
+ "▁sea",
+ -10.440893173217773
+ ],
+ [
+ "▁electric",
+ -10.441244125366211
+ ],
+ [
+ "▁forms",
+ -10.441384315490723
+ ],
+ [
+ "▁Central",
+ -10.441597938537598
+ ],
+ [
+ "▁Lord",
+ -10.442625999450684
+ ],
+ [
+ "ute",
+ -10.442763328552246
+ ],
+ [
+ "▁pré",
+ -10.442790031433105
+ ],
+ [
+ "▁square",
+ -10.44308090209961
+ ],
+ [
+ "itatea",
+ -10.443451881408691
+ ],
+ [
+ "▁debt",
+ -10.443757057189941
+ ],
+ [
+ "▁street",
+ -10.443975448608398
+ ],
+ [
+ "▁pi",
+ -10.444917678833008
+ ],
+ [
+ "▁happened",
+ -10.445326805114746
+ ],
+ [
+ "▁Tuesday",
+ -10.445592880249023
+ ],
+ [
+ "recht",
+ -10.446094512939453
+ ],
+ [
+ "▁Eine",
+ -10.44627857208252
+ ],
+ [
+ "▁Set",
+ -10.446768760681152
+ ],
+ [
+ "▁federal",
+ -10.4468412399292
+ ],
+ [
+ "CC",
+ -10.446905136108398
+ ],
+ [
+ "....",
+ -10.446938514709473
+ ],
+ [
+ "lig",
+ -10.447463035583496
+ ],
+ [
+ "▁Christian",
+ -10.44870662689209
+ ],
+ [
+ "▁truth",
+ -10.449213981628418
+ ],
+ [
+ "▁map",
+ -10.449728012084961
+ ],
+ [
+ "▁secret",
+ -10.449979782104492
+ ],
+ [
+ "▁Chinese",
+ -10.450844764709473
+ ],
+ [
+ "hol",
+ -10.450895309448242
+ ],
+ [
+ "▁wrote",
+ -10.451505661010742
+ ],
+ [
+ "▁hospital",
+ -10.451783180236816
+ ],
+ [
+ "▁Island",
+ -10.451870918273926
+ ],
+ [
+ "▁frame",
+ -10.451946258544922
+ ],
+ [
+ "▁sources",
+ -10.452117919921875
+ ],
+ [
+ "pan",
+ -10.453242301940918
+ ],
+ [
+ "▁29",
+ -10.453530311584473
+ ],
+ [
+ "▁changing",
+ -10.454547882080078
+ ],
+ [
+ "▁Where",
+ -10.454627990722656
+ ],
+ [
+ "▁negative",
+ -10.45471477508545
+ ],
+ [
+ "▁processes",
+ -10.45491886138916
+ ],
+ [
+ "▁leadership",
+ -10.455029487609863
+ ],
+ [
+ "▁nos",
+ -10.455195426940918
+ ],
+ [
+ "▁info",
+ -10.455780029296875
+ ],
+ [
+ "▁Gu",
+ -10.45595645904541
+ ],
+ [
+ "▁CO",
+ -10.45605182647705
+ ],
+ [
+ "▁reference",
+ -10.456884384155273
+ ],
+ [
+ "▁corporate",
+ -10.457097053527832
+ ],
+ [
+ "▁characters",
+ -10.457563400268555
+ ],
+ [
+ "▁dining",
+ -10.4577054977417
+ ],
+ [
+ "▁becoming",
+ -10.459708213806152
+ ],
+ [
+ "▁4.",
+ -10.460311889648438
+ ],
+ [
+ "▁Science",
+ -10.460626602172852
+ ],
+ [
+ "▁Education",
+ -10.461943626403809
+ ],
+ [
+ "▁camp",
+ -10.46207046508789
+ ],
+ [
+ "fall",
+ -10.462146759033203
+ ],
+ [
+ "▁Auch",
+ -10.462471961975098
+ ],
+ [
+ "▁topic",
+ -10.462519645690918
+ ],
+ [
+ "▁influence",
+ -10.463460922241211
+ ],
+ [
+ "▁70",
+ -10.463892936706543
+ ],
+ [
+ "▁identify",
+ -10.464459419250488
+ ],
+ [
+ "▁(19",
+ -10.464646339416504
+ ],
+ [
+ "care",
+ -10.465216636657715
+ ],
+ [
+ "ions",
+ -10.466215133666992
+ ],
+ [
+ "ray",
+ -10.4663724899292
+ ],
+ [
+ "▁Both",
+ -10.466577529907227
+ ],
+ [
+ "▁collect",
+ -10.466997146606445
+ ],
+ [
+ "▁practices",
+ -10.467667579650879
+ ],
+ [
+ "▁fight",
+ -10.468058586120605
+ ],
+ [
+ "▁injury",
+ -10.46873664855957
+ ],
+ [
+ "▁nici",
+ -10.46905517578125
+ ],
+ [
+ "▁depuis",
+ -10.469563484191895
+ ],
+ [
+ "▁actions",
+ -10.469609260559082
+ ],
+ [
+ "▁Wednesday",
+ -10.47089958190918
+ ],
+ [
+ "▁bill",
+ -10.471086502075195
+ ],
+ [
+ "▁cheap",
+ -10.471318244934082
+ ],
+ [
+ "lui",
+ -10.471719741821289
+ ],
+ [
+ "▁awesome",
+ -10.471731185913086
+ ],
+ [
+ "tig",
+ -10.472554206848145
+ ],
+ [
+ "▁expensive",
+ -10.472636222839355
+ ],
+ [
+ "ceea",
+ -10.472834587097168
+ ],
+ [
+ "▁exact",
+ -10.472907066345215
+ ],
+ [
+ "22",
+ -10.473462104797363
+ ],
+ [
+ "▁avant",
+ -10.47352123260498
+ ],
+ [
+ "▁fat",
+ -10.47353744506836
+ ],
+ [
+ "▁spending",
+ -10.474353790283203
+ ],
+ [
+ "▁designs",
+ -10.47608470916748
+ ],
+ [
+ "▁damit",
+ -10.4761323928833
+ ],
+ [
+ "▁comp",
+ -10.47619342803955
+ ],
+ [
+ "▁whatever",
+ -10.476434707641602
+ ],
+ [
+ "▁Light",
+ -10.476442337036133
+ ],
+ [
+ "▁quarter",
+ -10.47680377960205
+ ],
+ [
+ "hand",
+ -10.477301597595215
+ ],
+ [
+ "▁connected",
+ -10.477584838867188
+ ],
+ [
+ "▁technologies",
+ -10.47772216796875
+ ],
+ [
+ "ges",
+ -10.477808952331543
+ ],
+ [
+ "▁shower",
+ -10.478998184204102
+ ],
+ [
+ "▁500",
+ -10.47923469543457
+ ],
+ [
+ "▁Time",
+ -10.479436874389648
+ ],
+ [
+ "▁zone",
+ -10.480525970458984
+ ],
+ [
+ "▁vote",
+ -10.480624198913574
+ ],
+ [
+ "▁andere",
+ -10.480871200561523
+ ],
+ [
+ "▁otherwise",
+ -10.480988502502441
+ ],
+ [
+ "tur",
+ -10.481294631958008
+ ],
+ [
+ "▁happens",
+ -10.481504440307617
+ ],
+ [
+ "hin",
+ -10.481597900390625
+ ],
+ [
+ "▁volume",
+ -10.482161521911621
+ ],
+ [
+ "▁thousands",
+ -10.482391357421875
+ ],
+ [
+ "war",
+ -10.482551574707031
+ ],
+ [
+ "▁Play",
+ -10.482900619506836
+ ],
+ [
+ "▁temperature",
+ -10.48371410369873
+ ],
+ [
+ "▁industrial",
+ -10.483830451965332
+ ],
+ [
+ "▁fuel",
+ -10.483915328979492
+ ],
+ [
+ "100",
+ -10.48409366607666
+ ],
+ [
+ "top",
+ -10.484210014343262
+ ],
+ [
+ "kin",
+ -10.484312057495117
+ ],
+ [
+ "▁efficient",
+ -10.484414100646973
+ ],
+ [
+ "teil",
+ -10.484525680541992
+ ],
+ [
+ "alt",
+ -10.484578132629395
+ ],
+ [
+ "▁monde",
+ -10.48483657836914
+ ],
+ [
+ "▁Ra",
+ -10.484899520874023
+ ],
+ [
+ "▁bedroom",
+ -10.485103607177734
+ ],
+ [
+ "▁showing",
+ -10.485316276550293
+ ],
+ [
+ "▁continued",
+ -10.485490798950195
+ ],
+ [
+ "▁Plan",
+ -10.48552131652832
+ ],
+ [
+ "▁assistance",
+ -10.486014366149902
+ ],
+ [
+ "▁discover",
+ -10.48622989654541
+ ],
+ [
+ "▁Year",
+ -10.486238479614258
+ ],
+ [
+ "▁applied",
+ -10.486433029174805
+ ],
+ [
+ "▁audio",
+ -10.48755931854248
+ ],
+ [
+ "▁thus",
+ -10.487645149230957
+ ],
+ [
+ "▁permet",
+ -10.48806095123291
+ ],
+ [
+ "▁fashion",
+ -10.488532066345215
+ ],
+ [
+ "cra",
+ -10.488645553588867
+ ],
+ [
+ "ious",
+ -10.488700866699219
+ ],
+ [
+ "▁focused",
+ -10.489258766174316
+ ],
+ [
+ "16",
+ -10.48930549621582
+ ],
+ [
+ "▁arm",
+ -10.489364624023438
+ ],
+ [
+ "▁Their",
+ -10.489789962768555
+ ],
+ [
+ "▁Foundation",
+ -10.49022388458252
+ ],
+ [
+ "▁majority",
+ -10.49022388458252
+ ],
+ [
+ "▁wind",
+ -10.490785598754883
+ ],
+ [
+ "▁bought",
+ -10.491056442260742
+ ],
+ [
+ "▁factor",
+ -10.491918563842773
+ ],
+ [
+ "▁opened",
+ -10.49213695526123
+ ],
+ [
+ "tern",
+ -10.492374420166016
+ ],
+ [
+ "▁cars",
+ -10.492597579956055
+ ],
+ [
+ "▁exciting",
+ -10.492691040039062
+ ],
+ [
+ "▁affordable",
+ -10.493510246276855
+ ],
+ [
+ "ches",
+ -10.493563652038574
+ ],
+ [
+ "▁panel",
+ -10.493720054626465
+ ],
+ [
+ "▁caused",
+ -10.493793487548828
+ ],
+ [
+ "▁travail",
+ -10.493998527526855
+ ],
+ [
+ "▁roof",
+ -10.494073867797852
+ ],
+ [
+ "▁enable",
+ -10.494202613830566
+ ],
+ [
+ "▁toward",
+ -10.494491577148438
+ ],
+ [
+ "▁Development",
+ -10.494688987731934
+ ],
+ [
+ "▁foreign",
+ -10.495308876037598
+ ],
+ [
+ "avi",
+ -10.495320320129395
+ ],
+ [
+ "long",
+ -10.495328903198242
+ ],
+ [
+ "De",
+ -10.49578857421875
+ ],
+ [
+ "▁Mon",
+ -10.49588394165039
+ ],
+ [
+ "▁Va",
+ -10.495942115783691
+ ],
+ [
+ "AP",
+ -10.496097564697266
+ ],
+ [
+ "▁asta",
+ -10.49720573425293
+ ],
+ [
+ "▁prepare",
+ -10.497220993041992
+ ],
+ [
+ "▁German",
+ -10.497261047363281
+ ],
+ [
+ "▁Centre",
+ -10.497325897216797
+ ],
+ [
+ "ère",
+ -10.497367858886719
+ ],
+ [
+ "▁fear",
+ -10.497537612915039
+ ],
+ [
+ "▁Este",
+ -10.497878074645996
+ ],
+ [
+ "▁Des",
+ -10.49793529510498
+ ],
+ [
+ "▁Kon",
+ -10.499308586120605
+ ],
+ [
+ "á",
+ -10.499866485595703
+ ],
+ [
+ "stand",
+ -10.500805854797363
+ ],
+ [
+ "▁Real",
+ -10.500842094421387
+ ],
+ [
+ "lichen",
+ -10.50098705291748
+ ],
+ [
+ "▁Beach",
+ -10.501455307006836
+ ],
+ [
+ "▁expertise",
+ -10.50185775756836
+ ],
+ [
+ "▁route",
+ -10.502445220947266
+ ],
+ [
+ "▁nation",
+ -10.502551078796387
+ ],
+ [
+ "▁snow",
+ -10.503022193908691
+ ],
+ [
+ "▁articles",
+ -10.503127098083496
+ ],
+ [
+ "▁Wood",
+ -10.504426956176758
+ ],
+ [
+ "▁operation",
+ -10.50494384765625
+ ],
+ [
+ "▁passion",
+ -10.505215644836426
+ ],
+ [
+ "▁cand",
+ -10.505690574645996
+ ],
+ [
+ "haus",
+ -10.505701065063477
+ ],
+ [
+ "OR",
+ -10.505711555480957
+ ],
+ [
+ "▁senior",
+ -10.506511688232422
+ ],
+ [
+ "▁becomes",
+ -10.506546020507812
+ ],
+ [
+ "▁sounds",
+ -10.506878852844238
+ ],
+ [
+ "▁enjoyed",
+ -10.50704574584961
+ ],
+ [
+ "▁gegen",
+ -10.507533073425293
+ ],
+ [
+ "▁courses",
+ -10.507919311523438
+ ],
+ [
+ "▁absolutely",
+ -10.508257865905762
+ ],
+ [
+ "tim",
+ -10.508264541625977
+ ],
+ [
+ "uff",
+ -10.508516311645508
+ ],
+ [
+ "▁moins",
+ -10.50860595703125
+ ],
+ [
+ "▁TO",
+ -10.509060859680176
+ ],
+ [
+ "▁fabric",
+ -10.509267807006836
+ ],
+ [
+ "poli",
+ -10.509326934814453
+ ],
+ [
+ "▁Bre",
+ -10.509761810302734
+ ],
+ [
+ "▁bo",
+ -10.509916305541992
+ ],
+ [
+ "▁Elle",
+ -10.510469436645508
+ ],
+ [
+ "bu",
+ -10.512336730957031
+ ],
+ [
+ "▁participants",
+ -10.512401580810547
+ ],
+ [
+ "stone",
+ -10.512794494628906
+ ],
+ [
+ "ties",
+ -10.51366138458252
+ ],
+ [
+ "▁listen",
+ -10.513700485229492
+ ],
+ [
+ "▁Spiel",
+ -10.513752937316895
+ ],
+ [
+ "pot",
+ -10.513872146606445
+ ],
+ [
+ "▁selling",
+ -10.514358520507812
+ ],
+ [
+ "▁geht",
+ -10.514680862426758
+ ],
+ [
+ "▁mini",
+ -10.515146255493164
+ ],
+ [
+ "▁trans",
+ -10.515408515930176
+ ],
+ [
+ "▁ingredients",
+ -10.515642166137695
+ ],
+ [
+ "auf",
+ -10.515671730041504
+ ],
+ [
+ "▁orice",
+ -10.51595401763916
+ ],
+ [
+ "▁Next",
+ -10.516300201416016
+ ],
+ [
+ "▁cream",
+ -10.516756057739258
+ ],
+ [
+ "▁edge",
+ -10.516973495483398
+ ],
+ [
+ "▁recommended",
+ -10.517022132873535
+ ],
+ [
+ "▁Form",
+ -10.517277717590332
+ ],
+ [
+ "▁processing",
+ -10.51746940612793
+ ],
+ [
+ "vert",
+ -10.517709732055664
+ ],
+ [
+ "▁described",
+ -10.518362998962402
+ ],
+ [
+ "▁installed",
+ -10.51884937286377
+ ],
+ [
+ "▁managed",
+ -10.518952369689941
+ ],
+ [
+ "▁electronic",
+ -10.518966674804688
+ ],
+ [
+ "▁performed",
+ -10.519064903259277
+ ],
+ [
+ "▁raise",
+ -10.519098281860352
+ ],
+ [
+ "▁imagine",
+ -10.519281387329102
+ ],
+ [
+ "down",
+ -10.51952838897705
+ ],
+ [
+ "▁fond",
+ -10.519978523254395
+ ],
+ [
+ "▁Inter",
+ -10.520434379577637
+ ],
+ [
+ "▁Mc",
+ -10.520550727844238
+ ],
+ [
+ "▁Dans",
+ -10.520679473876953
+ ],
+ [
+ "istic",
+ -10.520966529846191
+ ],
+ [
+ "▁miss",
+ -10.521052360534668
+ ],
+ [
+ "sur",
+ -10.521062850952148
+ ],
+ [
+ "▁Col",
+ -10.521879196166992
+ ],
+ [
+ "cut",
+ -10.522021293640137
+ ],
+ [
+ "▁dupa",
+ -10.522160530090332
+ ],
+ [
+ "▁Twitter",
+ -10.522604942321777
+ ],
+ [
+ "▁bowl",
+ -10.523721694946289
+ ],
+ [
+ "▁remains",
+ -10.5237455368042
+ ],
+ [
+ "▁Jan",
+ -10.524046897888184
+ ],
+ [
+ "▁smooth",
+ -10.524162292480469
+ ],
+ [
+ "▁fees",
+ -10.524415969848633
+ ],
+ [
+ "▁aid",
+ -10.524494171142578
+ ],
+ [
+ "▁presence",
+ -10.524827003479004
+ ],
+ [
+ "▁Android",
+ -10.52499771118164
+ ],
+ [
+ "▁decisions",
+ -10.52539348602295
+ ],
+ [
+ "▁names",
+ -10.5254487991333
+ ],
+ [
+ "▁Music",
+ -10.525546073913574
+ ],
+ [
+ "▁innovative",
+ -10.525578498840332
+ ],
+ [
+ "▁Tom",
+ -10.525997161865234
+ ],
+ [
+ "▁spread",
+ -10.526165962219238
+ ],
+ [
+ "▁lovely",
+ -10.526222229003906
+ ],
+ [
+ "▁daughter",
+ -10.526397705078125
+ ],
+ [
+ "US",
+ -10.527050971984863
+ ],
+ [
+ "▁facility",
+ -10.52710247039795
+ ],
+ [
+ "▁peace",
+ -10.527105331420898
+ ],
+ [
+ "▁department",
+ -10.527277946472168
+ ],
+ [
+ "▁weiter",
+ -10.527591705322266
+ ],
+ [
+ "▁Sun",
+ -10.527756690979004
+ ],
+ [
+ "▁fund",
+ -10.527772903442383
+ ],
+ [
+ "▁2018.",
+ -10.52792739868164
+ ],
+ [
+ "▁discussion",
+ -10.528186798095703
+ ],
+ [
+ "75",
+ -10.528799057006836
+ ],
+ [
+ "EC",
+ -10.529126167297363
+ ],
+ [
+ "▁lunch",
+ -10.529144287109375
+ ],
+ [
+ "▁videos",
+ -10.52927017211914
+ ],
+ [
+ "05",
+ -10.531253814697266
+ ],
+ [
+ "ige",
+ -10.531266212463379
+ ],
+ [
+ "▁parking",
+ -10.531564712524414
+ ],
+ [
+ "▁relationships",
+ -10.531732559204102
+ ],
+ [
+ "▁George",
+ -10.532986640930176
+ ],
+ [
+ "▁teachers",
+ -10.53299617767334
+ ],
+ [
+ "room",
+ -10.533458709716797
+ ],
+ [
+ "▁Tra",
+ -10.533605575561523
+ ],
+ [
+ "▁Sam",
+ -10.533651351928711
+ ],
+ [
+ "▁properly",
+ -10.535590171813965
+ ],
+ [
+ "▁Book",
+ -10.535629272460938
+ ],
+ [
+ "▁CA",
+ -10.536957740783691
+ ],
+ [
+ "▁calls",
+ -10.53756046295166
+ ],
+ [
+ "▁stat",
+ -10.538175582885742
+ ],
+ [
+ "ux",
+ -10.538220405578613
+ ],
+ [
+ "▁soit",
+ -10.538439750671387
+ ],
+ [
+ "▁Community",
+ -10.538684844970703
+ ],
+ [
+ "▁Jahren",
+ -10.538714408874512
+ ],
+ [
+ "▁increasing",
+ -10.539575576782227
+ ],
+ [
+ "▁civil",
+ -10.540184020996094
+ ],
+ [
+ "app",
+ -10.540573120117188
+ ],
+ [
+ "▁35",
+ -10.540589332580566
+ ],
+ [
+ "▁rise",
+ -10.540600776672363
+ ],
+ [
+ "▁dabei",
+ -10.540989875793457
+ ],
+ [
+ "▁studio",
+ -10.541803359985352
+ ],
+ [
+ "▁policies",
+ -10.542054176330566
+ ],
+ [
+ "▁agent",
+ -10.542055130004883
+ ],
+ [
+ "▁Before",
+ -10.542601585388184
+ ],
+ [
+ "▁Cal",
+ -10.543017387390137
+ ],
+ [
+ "▁2005",
+ -10.543404579162598
+ ],
+ [
+ "▁sample",
+ -10.543777465820312
+ ],
+ [
+ "▁manner",
+ -10.545186996459961
+ ],
+ [
+ "wing",
+ -10.54521369934082
+ ],
+ [
+ "stra",
+ -10.545552253723145
+ ],
+ [
+ "▁fel",
+ -10.545793533325195
+ ],
+ [
+ "▁Show",
+ -10.545952796936035
+ ],
+ [
+ "▁scene",
+ -10.54656982421875
+ ],
+ [
+ "mic",
+ -10.546764373779297
+ ],
+ [
+ "nom",
+ -10.546995162963867
+ ],
+ [
+ "▁typically",
+ -10.547088623046875
+ ],
+ [
+ "▁pair",
+ -10.547104835510254
+ ],
+ [
+ "▁detailed",
+ -10.547394752502441
+ ],
+ [
+ "▁Work",
+ -10.547422409057617
+ ],
+ [
+ "▁cities",
+ -10.547451972961426
+ ],
+ [
+ "▁Rock",
+ -10.54749584197998
+ ],
+ [
+ "▁Gar",
+ -10.547906875610352
+ ],
+ [
+ "▁serving",
+ -10.548352241516113
+ ],
+ [
+ "▁machen",
+ -10.548521995544434
+ ],
+ [
+ "▁trees",
+ -10.54888916015625
+ ],
+ [
+ "▁accident",
+ -10.549199104309082
+ ],
+ [
+ "▁cloud",
+ -10.54920482635498
+ ],
+ [
+ "▁animals",
+ -10.549297332763672
+ ],
+ [
+ "▁Den",
+ -10.549897193908691
+ ],
+ [
+ "▁Wa",
+ -10.54990291595459
+ ],
+ [
+ "▁suggest",
+ -10.550220489501953
+ ],
+ [
+ "putting",
+ -10.550407409667969
+ ],
+ [
+ "▁suite",
+ -10.550434112548828
+ ],
+ [
+ "▁clearly",
+ -10.550849914550781
+ ],
+ [
+ "▁net",
+ -10.551287651062012
+ ],
+ [
+ "▁funding",
+ -10.551506996154785
+ ],
+ [
+ "▁salt",
+ -10.551935195922852
+ ],
+ [
+ "▁Men",
+ -10.552119255065918
+ ],
+ [
+ "ped",
+ -10.552419662475586
+ ],
+ [
+ "▁Food",
+ -10.553142547607422
+ ],
+ [
+ "▁leaving",
+ -10.553544998168945
+ ],
+ [
+ "▁Government",
+ -10.554243087768555
+ ],
+ [
+ "ick",
+ -10.554381370544434
+ ],
+ [
+ "▁seat",
+ -10.555121421813965
+ ],
+ [
+ "▁Los",
+ -10.555183410644531
+ ],
+ [
+ "▁teacher",
+ -10.555587768554688
+ ],
+ [
+ "▁iPhone",
+ -10.555693626403809
+ ],
+ [
+ "▁300",
+ -10.556120872497559
+ ],
+ [
+ "▁commitment",
+ -10.556180000305176
+ ],
+ [
+ "▁aspects",
+ -10.556498527526855
+ ],
+ [
+ "▁previously",
+ -10.55711555480957
+ ],
+ [
+ "▁cent",
+ -10.5572509765625
+ ],
+ [
+ "▁Vo",
+ -10.557341575622559
+ ],
+ [
+ "▁artists",
+ -10.557963371276855
+ ],
+ [
+ "▁runs",
+ -10.558130264282227
+ ],
+ [
+ ">",
+ -10.558155059814453
+ ],
+ [
+ "▁Gi",
+ -10.558273315429688
+ ],
+ [
+ "▁mar",
+ -10.5585355758667
+ ],
+ [
+ "!!!",
+ -10.558544158935547
+ ],
+ [
+ "▁Media",
+ -10.558943748474121
+ ],
+ [
+ "▁feedback",
+ -10.559109687805176
+ ],
+ [
+ "▁resolution",
+ -10.559117317199707
+ ],
+ [
+ "IN",
+ -10.55915641784668
+ ],
+ [
+ "▁wurden",
+ -10.55952262878418
+ ],
+ [
+ "▁busy",
+ -10.559832572937012
+ ],
+ [
+ "▁adult",
+ -10.5600004196167
+ ],
+ [
+ "29",
+ -10.560487747192383
+ ],
+ [
+ "elles",
+ -10.561375617980957
+ ],
+ [
+ "▁closed",
+ -10.561762809753418
+ ],
+ [
+ "▁trouble",
+ -10.561767578125
+ ],
+ [
+ "▁rent",
+ -10.561984062194824
+ ],
+ [
+ "lot",
+ -10.56224536895752
+ ],
+ [
+ "▁importance",
+ -10.562314987182617
+ ],
+ [
+ "▁units",
+ -10.56257438659668
+ ],
+ [
+ "Pro",
+ -10.562713623046875
+ ],
+ [
+ "▁provider",
+ -10.563005447387695
+ ],
+ [
+ "▁visual",
+ -10.563288688659668
+ ],
+ [
+ "IT",
+ -10.563385009765625
+ ],
+ [
+ "▁diet",
+ -10.563733100891113
+ ],
+ [
+ "▁appearance",
+ -10.563932418823242
+ ],
+ [
+ "pin",
+ -10.564576148986816
+ ],
+ [
+ "▁Din",
+ -10.564760208129883
+ ],
+ [
+ "▁eating",
+ -10.565516471862793
+ ],
+ [
+ "Fi",
+ -10.565762519836426
+ ],
+ [
+ "ball",
+ -10.565765380859375
+ ],
+ [
+ "är",
+ -10.565861701965332
+ ],
+ [
+ "ney",
+ -10.565878868103027
+ ],
+ [
+ "▁records",
+ -10.566070556640625
+ ],
+ [
+ "▁Fi",
+ -10.566180229187012
+ ],
+ [
+ "▁faut",
+ -10.566329002380371
+ ],
+ [
+ "▁CD",
+ -10.566803932189941
+ ],
+ [
+ "ign",
+ -10.566930770874023
+ ],
+ [
+ "▁vă",
+ -10.566996574401855
+ ],
+ [
+ "▁agency",
+ -10.567153930664062
+ ],
+ [
+ "ierung",
+ -10.567323684692383
+ ],
+ [
+ "▁Back",
+ -10.567361831665039
+ ],
+ [
+ "▁windows",
+ -10.567545890808105
+ ],
+ [
+ "▁pull",
+ -10.567888259887695
+ ],
+ [
+ "ash",
+ -10.567959785461426
+ ],
+ [
+ "▁profit",
+ -10.568593978881836
+ ],
+ [
+ "▁brings",
+ -10.568605422973633
+ ],
+ [
+ "▁Committee",
+ -10.569122314453125
+ ],
+ [
+ "▁girl",
+ -10.569174766540527
+ ],
+ [
+ "▁vehicles",
+ -10.569372177124023
+ ],
+ [
+ "▁Hier",
+ -10.569567680358887
+ ],
+ [
+ "ES",
+ -10.569639205932617
+ ],
+ [
+ "până",
+ -10.569880485534668
+ ],
+ [
+ "▁Kunden",
+ -10.570380210876465
+ ],
+ [
+ "pen",
+ -10.570462226867676
+ ],
+ [
+ "▁explain",
+ -10.570505142211914
+ ],
+ [
+ "▁cadru",
+ -10.570760726928711
+ ],
+ [
+ "▁attack",
+ -10.571100234985352
+ ],
+ [
+ "▁markets",
+ -10.571115493774414
+ ],
+ [
+ "▁claims",
+ -10.571340560913086
+ ],
+ [
+ "▁walking",
+ -10.571385383605957
+ ],
+ [
+ "▁pouv",
+ -10.571528434753418
+ ],
+ [
+ "low",
+ -10.571642875671387
+ ],
+ [
+ "▁showed",
+ -10.572114944458008
+ ],
+ [
+ "▁principal",
+ -10.57211971282959
+ ],
+ [
+ "▁lucru",
+ -10.572144508361816
+ ],
+ [
+ "▁precum",
+ -10.572712898254395
+ ],
+ [
+ "TA",
+ -10.573094367980957
+ ],
+ [
+ "▁partners",
+ -10.573104858398438
+ ],
+ [
+ "▁exist",
+ -10.573136329650879
+ ],
+ [
+ "▁internal",
+ -10.57334041595459
+ ],
+ [
+ "hen",
+ -10.573945045471191
+ ],
+ [
+ "▁Master",
+ -10.573966979980469
+ ],
+ [
+ "unless",
+ -10.574013710021973
+ ],
+ [
+ "▁doubt",
+ -10.574721336364746
+ ],
+ [
+ "$",
+ -10.574785232543945
+ ],
+ [
+ "▁Long",
+ -10.574888229370117
+ ],
+ [
+ "▁leaves",
+ -10.574907302856445
+ ],
+ [
+ "allowing",
+ -10.575063705444336
+ ],
+ [
+ "pol",
+ -10.575272560119629
+ ],
+ [
+ "▁Up",
+ -10.575491905212402
+ ],
+ [
+ "▁Contact",
+ -10.576093673706055
+ ],
+ [
+ "▁practical",
+ -10.57708740234375
+ ],
+ [
+ "▁suit",
+ -10.57758903503418
+ ],
+ [
+ "▁Site",
+ -10.577656745910645
+ ],
+ [
+ "▁formation",
+ -10.57768726348877
+ ],
+ [
+ "▁signal",
+ -10.578215599060059
+ ],
+ [
+ "▁approximately",
+ -10.578414916992188
+ ],
+ [
+ "▁ourselves",
+ -10.578497886657715
+ ],
+ [
+ "▁colour",
+ -10.578519821166992
+ ],
+ [
+ "▁species",
+ -10.578530311584473
+ ],
+ [
+ "▁advance",
+ -10.578753471374512
+ ],
+ [
+ "▁PM",
+ -10.57891845703125
+ ],
+ [
+ "ans",
+ -10.579121589660645
+ ],
+ [
+ "▁locations",
+ -10.579397201538086
+ ],
+ [
+ "vous",
+ -10.579601287841797
+ ],
+ [
+ "▁updated",
+ -10.579636573791504
+ ],
+ [
+ "▁faith",
+ -10.579673767089844
+ ],
+ [
+ "mus",
+ -10.579740524291992
+ ],
+ [
+ "▁stores",
+ -10.579863548278809
+ ],
+ [
+ "heim",
+ -10.580127716064453
+ ],
+ [
+ "▁suitable",
+ -10.580558776855469
+ ],
+ [
+ "▁continues",
+ -10.580703735351562
+ ],
+ [
+ "▁fac",
+ -10.581133842468262
+ ],
+ [
+ "ever",
+ -10.581156730651855
+ ],
+ [
+ "▁Bill",
+ -10.581195831298828
+ ],
+ [
+ "▁chose",
+ -10.58121109008789
+ ],
+ [
+ "▁inform",
+ -10.581228256225586
+ ],
+ [
+ "▁environmental",
+ -10.581427574157715
+ ],
+ [
+ "▁responsibility",
+ -10.58188533782959
+ ],
+ [
+ "99",
+ -10.582542419433594
+ ],
+ [
+ "▁competitive",
+ -10.583723068237305
+ ],
+ [
+ "▁strategies",
+ -10.583903312683105
+ ],
+ [
+ "▁toujours",
+ -10.584270477294922
+ ],
+ [
+ "tive",
+ -10.58430290222168
+ ],
+ [
+ "▁automatically",
+ -10.585600852966309
+ ],
+ [
+ "▁dress",
+ -10.585609436035156
+ ],
+ [
+ "▁Minister",
+ -10.585624694824219
+ ],
+ [
+ "har",
+ -10.586076736450195
+ ],
+ [
+ "▁Start",
+ -10.586249351501465
+ ],
+ [
+ "▁=",
+ -10.586563110351562
+ ],
+ [
+ "▁pattern",
+ -10.58659553527832
+ ],
+ [
+ "tier",
+ -10.58676528930664
+ ],
+ [
+ "▁pays",
+ -10.587034225463867
+ ],
+ [
+ "▁profile",
+ -10.58725357055664
+ ],
+ [
+ "▁raised",
+ -10.587263107299805
+ ],
+ [
+ "ange",
+ -10.587288856506348
+ ],
+ [
+ "▁drink",
+ -10.587762832641602
+ ],
+ [
+ "▁element",
+ -10.588042259216309
+ ],
+ [
+ "▁landscape",
+ -10.58875560760498
+ ],
+ [
+ "▁Tag",
+ -10.589073181152344
+ ],
+ [
+ "▁cheese",
+ -10.589590072631836
+ ],
+ [
+ "ific",
+ -10.590009689331055
+ ],
+ [
+ "▁Stadt",
+ -10.590181350708008
+ ],
+ [
+ "39",
+ -10.591398239135742
+ ],
+ [
+ "▁launch",
+ -10.592113494873047
+ ],
+ [
+ "▁wouldn",
+ -10.592150688171387
+ ],
+ [
+ "AS",
+ -10.592202186584473
+ ],
+ [
+ "▁push",
+ -10.593059539794922
+ ],
+ [
+ "▁mill",
+ -10.593452453613281
+ ],
+ [
+ "▁mass",
+ -10.593647003173828
+ ],
+ [
+ "▁category",
+ -10.593790054321289
+ ],
+ [
+ "sondern",
+ -10.594050407409668
+ ],
+ [
+ "col",
+ -10.594111442565918
+ ],
+ [
+ "▁climate",
+ -10.594313621520996
+ ],
+ [
+ "lier",
+ -10.594437599182129
+ ],
+ [
+ "▁slightly",
+ -10.595514297485352
+ ],
+ [
+ "95",
+ -10.596519470214844
+ ],
+ [
+ "ace",
+ -10.596612930297852
+ ],
+ [
+ "▁domain",
+ -10.597633361816406
+ ],
+ [
+ "kan",
+ -10.598306655883789
+ ],
+ [
+ "▁feed",
+ -10.598485946655273
+ ],
+ [
+ "▁Live",
+ -10.598837852478027
+ ],
+ [
+ "▁Mais",
+ -10.599113464355469
+ ],
+ [
+ "▁après",
+ -10.599365234375
+ ],
+ [
+ "▁village",
+ -10.59941577911377
+ ],
+ [
+ "▁hatte",
+ -10.59968090057373
+ ],
+ [
+ "▁joined",
+ -10.599881172180176
+ ],
+ [
+ "▁Museum",
+ -10.600311279296875
+ ],
+ [
+ "head",
+ -10.600855827331543
+ ],
+ [
+ "▁draw",
+ -10.6009521484375
+ ],
+ [
+ "▁concerns",
+ -10.600966453552246
+ ],
+ [
+ "ER",
+ -10.601505279541016
+ ],
+ [
+ "▁technique",
+ -10.601648330688477
+ ],
+ [
+ "▁Bio",
+ -10.601861000061035
+ ],
+ [
+ "▁Sea",
+ -10.601881980895996
+ ],
+ [
+ "▁@",
+ -10.601927757263184
+ ],
+ [
+ "wer",
+ -10.6021146774292
+ ],
+ [
+ "▁battery",
+ -10.602462768554688
+ ],
+ [
+ "▁mostly",
+ -10.60267448425293
+ ],
+ [
+ "▁familiar",
+ -10.602680206298828
+ ],
+ [
+ "▁Sub",
+ -10.602689743041992
+ ],
+ [
+ "▁delicious",
+ -10.603222846984863
+ ],
+ [
+ "doch",
+ -10.60326099395752
+ ],
+ [
+ "60",
+ -10.603395462036133
+ ],
+ [
+ "▁carte",
+ -10.603611946105957
+ ],
+ [
+ "▁avut",
+ -10.604146957397461
+ ],
+ [
+ "▁premium",
+ -10.60460376739502
+ ],
+ [
+ "▁attempt",
+ -10.604704856872559
+ ],
+ [
+ "▁Über",
+ -10.60473346710205
+ ],
+ [
+ "▁combined",
+ -10.604935646057129
+ ],
+ [
+ "lement",
+ -10.604947090148926
+ ],
+ [
+ "▁voi",
+ -10.605031967163086
+ ],
+ [
+ "▁wonder",
+ -10.605376243591309
+ ],
+ [
+ "▁failure",
+ -10.606106758117676
+ ],
+ [
+ "which",
+ -10.606147766113281
+ ],
+ [
+ "esti",
+ -10.606316566467285
+ ],
+ [
+ "31",
+ -10.606547355651855
+ ],
+ [
+ "▁sta",
+ -10.606734275817871
+ ],
+ [
+ "▁transform",
+ -10.60673999786377
+ ],
+ [
+ "▁license",
+ -10.606743812561035
+ ],
+ [
+ "▁depending",
+ -10.606758117675781
+ ],
+ [
+ "▁specifically",
+ -10.606782913208008
+ ],
+ [
+ "▁OF",
+ -10.60693645477295
+ ],
+ [
+ "band",
+ -10.606959342956543
+ ],
+ [
+ "▁Sport",
+ -10.60731315612793
+ ],
+ [
+ "list",
+ -10.607434272766113
+ ],
+ [
+ "▁Tour",
+ -10.60753059387207
+ ],
+ [
+ "▁Israel",
+ -10.607564926147461
+ ],
+ [
+ "▁filled",
+ -10.607722282409668
+ ],
+ [
+ "▁manual",
+ -10.60776138305664
+ ],
+ [
+ "▁watching",
+ -10.608621597290039
+ ],
+ [
+ "▁rule",
+ -10.608877182006836
+ ],
+ [
+ "mat",
+ -10.60901927947998
+ ],
+ [
+ "▁notes",
+ -10.609585762023926
+ ],
+ [
+ "▁Oh",
+ -10.60960578918457
+ ],
+ [
+ "▁bereits",
+ -10.609634399414062
+ ],
+ [
+ "▁foundation",
+ -10.609916687011719
+ ],
+ [
+ "▁vital",
+ -10.610146522521973
+ ],
+ [
+ "▁lassen",
+ -10.610747337341309
+ ],
+ [
+ "▁cât",
+ -10.611162185668945
+ ],
+ [
+ "▁shipping",
+ -10.611433029174805
+ ],
+ [
+ "▁registered",
+ -10.611513137817383
+ ],
+ [
+ "▁jour",
+ -10.612669944763184
+ ],
+ [
+ "▁island",
+ -10.61276626586914
+ ],
+ [
+ "▁sets",
+ -10.613068580627441
+ ],
+ [
+ "▁football",
+ -10.613683700561523
+ ],
+ [
+ "▁EU",
+ -10.613860130310059
+ ],
+ [
+ "▁stone",
+ -10.614019393920898
+ ],
+ [
+ "▁Press",
+ -10.614699363708496
+ ],
+ [
+ "▁adapt",
+ -10.615066528320312
+ ],
+ [
+ "ised",
+ -10.615425109863281
+ ],
+ [
+ "▁thoughts",
+ -10.615434646606445
+ ],
+ [
+ "▁doors",
+ -10.615851402282715
+ ],
+ [
+ "€",
+ -10.615954399108887
+ ],
+ [
+ "▁components",
+ -10.616040229797363
+ ],
+ [
+ "rig",
+ -10.616332054138184
+ ],
+ [
+ "▁generation",
+ -10.616585731506348
+ ],
+ [
+ "▁guess",
+ -10.616700172424316
+ ],
+ [
+ "cker",
+ -10.61694049835205
+ ],
+ [
+ "▁realize",
+ -10.617207527160645
+ ],
+ [
+ "▁Roman",
+ -10.617310523986816
+ ],
+ [
+ "▁contre",
+ -10.617693901062012
+ ],
+ [
+ "▁Out",
+ -10.617938995361328
+ ],
+ [
+ "▁IN",
+ -10.619051933288574
+ ],
+ [
+ "cip",
+ -10.619085311889648
+ ],
+ [
+ "59",
+ -10.619330406188965
+ ],
+ [
+ "▁enhance",
+ -10.619768142700195
+ ],
+ [
+ "▁battle",
+ -10.61982250213623
+ ],
+ [
+ "▁monitor",
+ -10.619863510131836
+ ],
+ [
+ "▁Martin",
+ -10.62045955657959
+ ],
+ [
+ "▁websites",
+ -10.620461463928223
+ ],
+ [
+ "▁DE",
+ -10.620599746704102
+ ],
+ [
+ "▁Festival",
+ -10.620951652526855
+ ],
+ [
+ "ân",
+ -10.62131118774414
+ ],
+ [
+ "▁Place",
+ -10.621419906616211
+ ],
+ [
+ "▁rare",
+ -10.621554374694824
+ ],
+ [
+ "această",
+ -10.621726989746094
+ ],
+ [
+ "▁sollte",
+ -10.621731758117676
+ ],
+ [
+ "▁Read",
+ -10.621816635131836
+ ],
+ [
+ "ware",
+ -10.622169494628906
+ ],
+ [
+ "Those",
+ -10.622671127319336
+ ],
+ [
+ "ende",
+ -10.623543739318848
+ ],
+ [
+ "▁prix",
+ -10.623835563659668
+ ],
+ [
+ "▁roman",
+ -10.624101638793945
+ ],
+ [
+ "▁creation",
+ -10.624224662780762
+ ],
+ [
+ "▁confidence",
+ -10.624552726745605
+ ],
+ [
+ "▁Japan",
+ -10.624638557434082
+ ],
+ [
+ "▁rain",
+ -10.624942779541016
+ ],
+ [
+ "▁guys",
+ -10.62518310546875
+ ],
+ [
+ "▁south",
+ -10.625236511230469
+ ],
+ [
+ "▁trading",
+ -10.625646591186523
+ ],
+ [
+ "▁€",
+ -10.626100540161133
+ ],
+ [
+ "▁Film",
+ -10.626341819763184
+ ],
+ [
+ "▁pana",
+ -10.627065658569336
+ ],
+ [
+ "▁asemenea",
+ -10.627066612243652
+ ],
+ [
+ "36",
+ -10.627190589904785
+ ],
+ [
+ "▁instance",
+ -10.627884864807129
+ ],
+ [
+ "cou",
+ -10.629385948181152
+ ],
+ [
+ "▁nun",
+ -10.630074501037598
+ ],
+ [
+ "▁Pass",
+ -10.630390167236328
+ ],
+ [
+ "Cette",
+ -10.630579948425293
+ ],
+ [
+ "▁Network",
+ -10.630876541137695
+ ],
+ [
+ "▁prime",
+ -10.631010055541992
+ ],
+ [
+ "▁spiritual",
+ -10.632098197937012
+ ],
+ [
+ "▁tough",
+ -10.633030891418457
+ ],
+ [
+ "▁AND",
+ -10.633086204528809
+ ],
+ [
+ "▁Cat",
+ -10.633601188659668
+ ],
+ [
+ "▁boat",
+ -10.633611679077148
+ ],
+ [
+ "▁leads",
+ -10.634864807128906
+ ],
+ [
+ "▁Germany",
+ -10.63509750366211
+ ],
+ [
+ "▁valuable",
+ -10.635635375976562
+ ],
+ [
+ "57",
+ -10.635892868041992
+ ],
+ [
+ "lect",
+ -10.636148452758789
+ ],
+ [
+ "▁distribution",
+ -10.636445045471191
+ ],
+ [
+ "dar",
+ -10.636518478393555
+ ],
+ [
+ "▁Manager",
+ -10.637701988220215
+ ],
+ [
+ "cha",
+ -10.637725830078125
+ ],
+ [
+ "▁obtain",
+ -10.637741088867188
+ ],
+ [
+ "GB",
+ -10.637908935546875
+ ],
+ [
+ "▁unor",
+ -10.638079643249512
+ ],
+ [
+ "schaft",
+ -10.638603210449219
+ ],
+ [
+ "▁zwischen",
+ -10.638723373413086
+ ],
+ [
+ "▁winning",
+ -10.639172554016113
+ ],
+ [
+ "▁suis",
+ -10.639811515808105
+ ],
+ [
+ "58",
+ -10.640130996704102
+ ],
+ [
+ "▁Party",
+ -10.640372276306152
+ ],
+ [
+ "▁ceva",
+ -10.640416145324707
+ ],
+ [
+ "▁comprehensive",
+ -10.640684127807617
+ ],
+ [
+ "▁aceste",
+ -10.640726089477539
+ ],
+ [
+ "▁committed",
+ -10.640726089477539
+ ],
+ [
+ "▁Hu",
+ -10.641382217407227
+ ],
+ [
+ "ţ",
+ -10.64149284362793
+ ],
+ [
+ "▁north",
+ -10.642021179199219
+ ],
+ [
+ "werk",
+ -10.642542839050293
+ ],
+ [
+ "▁interface",
+ -10.642794609069824
+ ],
+ [
+ "▁Valley",
+ -10.64281177520752
+ ],
+ [
+ "▁anywhere",
+ -10.64281177520752
+ ],
+ [
+ "▁Only",
+ -10.642851829528809
+ ],
+ [
+ "TE",
+ -10.643295288085938
+ ],
+ [
+ "hui",
+ -10.6436767578125
+ ],
+ [
+ "bus",
+ -10.643951416015625
+ ],
+ [
+ "vis",
+ -10.6439790725708
+ ],
+ [
+ "▁Society",
+ -10.645116806030273
+ ],
+ [
+ "▁reliable",
+ -10.64556884765625
+ ],
+ [
+ "▁quelques",
+ -10.64563274383545
+ ],
+ [
+ "tech",
+ -10.646187782287598
+ ],
+ [
+ "ual",
+ -10.646377563476562
+ ],
+ [
+ "▁educational",
+ -10.646418571472168
+ ],
+ [
+ "serv",
+ -10.646490097045898
+ ],
+ [
+ "▁opinion",
+ -10.646628379821777
+ ],
+ [
+ "▁appears",
+ -10.646702766418457
+ ],
+ [
+ "▁count",
+ -10.646795272827148
+ ],
+ [
+ "irea",
+ -10.646981239318848
+ ],
+ [
+ "ban",
+ -10.647504806518555
+ ],
+ [
+ "▁45",
+ -10.647530555725098
+ ],
+ [
+ "▁contain",
+ -10.647661209106445
+ ],
+ [
+ "ost",
+ -10.647663116455078
+ ],
+ [
+ "▁anul",
+ -10.647706031799316
+ ],
+ [
+ "rien",
+ -10.648159980773926
+ ],
+ [
+ "gra",
+ -10.648360252380371
+ ],
+ [
+ "▁counter",
+ -10.648946762084961
+ ],
+ [
+ "-3",
+ -10.650411605834961
+ ],
+ [
+ "▁resource",
+ -10.650463104248047
+ ],
+ [
+ "▁Wo",
+ -10.6505126953125
+ ],
+ [
+ "▁posts",
+ -10.650618553161621
+ ],
+ [
+ "▁employee",
+ -10.651320457458496
+ ],
+ [
+ "rol",
+ -10.651863098144531
+ ],
+ [
+ "▁ended",
+ -10.651969909667969
+ ],
+ [
+ "met",
+ -10.653080940246582
+ ],
+ [
+ "▁meine",
+ -10.653165817260742
+ ],
+ [
+ "▁reached",
+ -10.653368949890137
+ ],
+ [
+ "gri",
+ -10.653716087341309
+ ],
+ [
+ "▁Bra",
+ -10.65374755859375
+ ],
+ [
+ "▁conduct",
+ -10.654294967651367
+ ],
+ [
+ "▁housing",
+ -10.654422760009766
+ ],
+ [
+ "▁tickets",
+ -10.654792785644531
+ ],
+ [
+ "▁database",
+ -10.655674934387207
+ ],
+ [
+ "IL",
+ -10.656150817871094
+ ],
+ [
+ "▁perspective",
+ -10.656359672546387
+ ],
+ [
+ "▁Har",
+ -10.656404495239258
+ ],
+ [
+ "▁error",
+ -10.656549453735352
+ ],
+ [
+ "▁meal",
+ -10.656569480895996
+ ],
+ [
+ "▁hearing",
+ -10.657238006591797
+ ],
+ [
+ "▁transition",
+ -10.657302856445312
+ ],
+ [
+ "▁browser",
+ -10.657609939575195
+ ],
+ [
+ "▁supported",
+ -10.657609939575195
+ ],
+ [
+ "▁starts",
+ -10.658814430236816
+ ],
+ [
+ "țe",
+ -10.658902168273926
+ ],
+ [
+ "▁adults",
+ -10.658905029296875
+ ],
+ [
+ "▁România",
+ -10.65917682647705
+ ],
+ [
+ "dra",
+ -10.659884452819824
+ ],
+ [
+ "▁worry",
+ -10.660222053527832
+ ],
+ [
+ "▁avoir",
+ -10.660497665405273
+ ],
+ [
+ "▁regional",
+ -10.660507202148438
+ ],
+ [
+ "▁min",
+ -10.660722732543945
+ ],
+ [
+ "▁Does",
+ -10.660806655883789
+ ],
+ [
+ "▁Keep",
+ -10.661200523376465
+ ],
+ [
+ "rom",
+ -10.661237716674805
+ ],
+ [
+ "sco",
+ -10.661320686340332
+ ],
+ [
+ "tem",
+ -10.661898612976074
+ ],
+ [
+ "▁Old",
+ -10.661954879760742
+ ],
+ [
+ "▁Under",
+ -10.662552833557129
+ ],
+ [
+ "▁Commission",
+ -10.662557601928711
+ ],
+ [
+ "▁Bau",
+ -10.6632661819458
+ ],
+ [
+ "▁News",
+ -10.663358688354492
+ ],
+ [
+ "▁mois",
+ -10.663444519042969
+ ],
+ [
+ "▁respond",
+ -10.66356372833252
+ ],
+ [
+ "▁alles",
+ -10.663878440856934
+ ],
+ [
+ "▁chair",
+ -10.664475440979004
+ ],
+ [
+ "▁ho",
+ -10.664854049682617
+ ],
+ [
+ "right",
+ -10.664908409118652
+ ],
+ [
+ "▁totally",
+ -10.665532112121582
+ ],
+ [
+ "gle",
+ -10.665534973144531
+ ],
+ [
+ "▁32",
+ -10.665604591369629
+ ],
+ [
+ "66",
+ -10.665664672851562
+ ],
+ [
+ "town",
+ -10.665902137756348
+ ],
+ [
+ "Ch",
+ -10.666261672973633
+ ],
+ [
+ "▁gr",
+ -10.66629695892334
+ ],
+ [
+ "▁garage",
+ -10.666328430175781
+ ],
+ [
+ "ții",
+ -10.666495323181152
+ ],
+ [
+ "▁Union",
+ -10.667136192321777
+ ],
+ [
+ "ică",
+ -10.667343139648438
+ ],
+ [
+ "▁2,",
+ -10.668437004089355
+ ],
+ [
+ "▁reflect",
+ -10.669163703918457
+ ],
+ [
+ "▁retail",
+ -10.669388771057129
+ ],
+ [
+ "▁unde",
+ -10.669605255126953
+ ],
+ [
+ "▁accessible",
+ -10.670262336730957
+ ],
+ [
+ "water",
+ -10.67059326171875
+ ],
+ [
+ "▁regard",
+ -10.670710563659668
+ ],
+ [
+ "▁logo",
+ -10.671489715576172
+ ],
+ [
+ "▁inspired",
+ -10.671518325805664
+ ],
+ [
+ "▁Wall",
+ -10.671859741210938
+ ],
+ [
+ "▁Ste",
+ -10.672093391418457
+ ],
+ [
+ "▁asking",
+ -10.672179222106934
+ ],
+ [
+ "▁Journal",
+ -10.673028945922852
+ ],
+ [
+ "▁Teil",
+ -10.674042701721191
+ ],
+ [
+ "▁collaboration",
+ -10.674185752868652
+ ],
+ [
+ "▁acid",
+ -10.674266815185547
+ ],
+ [
+ "▁Fund",
+ -10.674382209777832
+ ],
+ [
+ "▁spirit",
+ -10.6744384765625
+ ],
+ [
+ "despite",
+ -10.674457550048828
+ ],
+ [
+ "▁delivered",
+ -10.674821853637695
+ ],
+ [
+ "▁girls",
+ -10.675374984741211
+ ],
+ [
+ "▁Look",
+ -10.675896644592285
+ ],
+ [
+ "rant",
+ -10.675949096679688
+ ],
+ [
+ "▁District",
+ -10.676460266113281
+ ],
+ [
+ "▁rental",
+ -10.676709175109863
+ ],
+ [
+ "▁spune",
+ -10.676733016967773
+ ],
+ [
+ "els",
+ -10.677544593811035
+ ],
+ [
+ "▁permanent",
+ -10.677659034729004
+ ],
+ [
+ "▁iron",
+ -10.677709579467773
+ ],
+ [
+ "▁Thomas",
+ -10.677745819091797
+ ],
+ [
+ "EL",
+ -10.678071022033691
+ ],
+ [
+ "▁except",
+ -10.678074836730957
+ ],
+ [
+ "▁catch",
+ -10.678366661071777
+ ],
+ [
+ "▁providers",
+ -10.678375244140625
+ ],
+ [
+ "▁2006",
+ -10.678435325622559
+ ],
+ [
+ "▁chat",
+ -10.679931640625
+ ],
+ [
+ "▁emergency",
+ -10.680281639099121
+ ],
+ [
+ "gre",
+ -10.68030834197998
+ ],
+ [
+ "site",
+ -10.680888175964355
+ ],
+ [
+ "▁missing",
+ -10.68089485168457
+ ],
+ [
+ "abil",
+ -10.680914878845215
+ ],
+ [
+ "▁Hill",
+ -10.68099594116211
+ ],
+ [
+ "urs",
+ -10.681312561035156
+ ],
+ [
+ "▁plusieurs",
+ -10.681716918945312
+ ],
+ [
+ "▁birthday",
+ -10.681726455688477
+ ],
+ [
+ "DS",
+ -10.682019233703613
+ ],
+ [
+ "ersten",
+ -10.682381629943848
+ ],
+ [
+ "▁5.",
+ -10.68252944946289
+ ],
+ [
+ "▁library",
+ -10.68333911895752
+ ],
+ [
+ "▁earth",
+ -10.683515548706055
+ ],
+ [
+ "CI",
+ -10.683645248413086
+ ],
+ [
+ "▁lighting",
+ -10.684442520141602
+ ],
+ [
+ "▁fixed",
+ -10.684879302978516
+ ],
+ [
+ "tori",
+ -10.684891700744629
+ ],
+ [
+ "▁replace",
+ -10.684995651245117
+ ],
+ [
+ "▁administration",
+ -10.685074806213379
+ ],
+ [
+ "leurs",
+ -10.685229301452637
+ ],
+ [
+ "▁meat",
+ -10.686142921447754
+ ],
+ [
+ "▁songs",
+ -10.686662673950195
+ ],
+ [
+ "▁confirm",
+ -10.686866760253906
+ ],
+ [
+ "▁rapid",
+ -10.68698787689209
+ ],
+ [
+ "▁Special",
+ -10.686995506286621
+ ],
+ [
+ "▁holding",
+ -10.687115669250488
+ ],
+ [
+ "▁honor",
+ -10.687271118164062
+ ],
+ [
+ "▁Market",
+ -10.687409400939941
+ ],
+ [
+ "La",
+ -10.687535285949707
+ ],
+ [
+ "▁measure",
+ -10.687760353088379
+ ],
+ [
+ "▁guarantee",
+ -10.68785572052002
+ ],
+ [
+ "▁switch",
+ -10.68813419342041
+ ],
+ [
+ "▁extensive",
+ -10.688294410705566
+ ],
+ [
+ "▁Neu",
+ -10.688674926757812
+ ],
+ [
+ "avez",
+ -10.688901901245117
+ ],
+ [
+ "▁protein",
+ -10.688984870910645
+ ],
+ [
+ "▁infrastructure",
+ -10.689454078674316
+ ],
+ [
+ "▁functions",
+ -10.689494132995605
+ ],
+ [
+ "▁cont",
+ -10.689496040344238
+ ],
+ [
+ "row",
+ -10.689760208129883
+ ],
+ [
+ "star",
+ -10.689773559570312
+ ],
+ [
+ "▁Port",
+ -10.690192222595215
+ ],
+ [
+ "Using",
+ -10.690336227416992
+ ],
+ [
+ "▁faster",
+ -10.690557479858398
+ ],
+ [
+ "44",
+ -10.691168785095215
+ ],
+ [
+ "▁measures",
+ -10.691615104675293
+ ],
+ [
+ "▁celor",
+ -10.69186019897461
+ ],
+ [
+ "▁exam",
+ -10.69189739227295
+ ],
+ [
+ "200",
+ -10.69202995300293
+ ],
+ [
+ "î",
+ -10.692545890808105
+ ],
+ [
+ "▁conversation",
+ -10.692832946777344
+ ],
+ [
+ "▁brands",
+ -10.692959785461426
+ ],
+ [
+ "▁Code",
+ -10.69359016418457
+ ],
+ [
+ "▁Website",
+ -10.693748474121094
+ ],
+ [
+ "OS",
+ -10.693782806396484
+ ],
+ [
+ "▁alors",
+ -10.693822860717773
+ ],
+ [
+ "▁organ",
+ -10.694032669067383
+ ],
+ [
+ "▁removed",
+ -10.694823265075684
+ ],
+ [
+ "▁Head",
+ -10.694905281066895
+ ],
+ [
+ "▁Cha",
+ -10.694908142089844
+ ],
+ [
+ "▁visiting",
+ -10.694928169250488
+ ],
+ [
+ "▁wild",
+ -10.694928169250488
+ ],
+ [
+ "▁seit",
+ -10.694962501525879
+ ],
+ [
+ "49",
+ -10.695109367370605
+ ],
+ [
+ "▁organic",
+ -10.69539737701416
+ ],
+ [
+ "aţi",
+ -10.695775032043457
+ ],
+ [
+ "▁kit",
+ -10.695947647094727
+ ],
+ [
+ "68",
+ -10.695959091186523
+ ],
+ [
+ "▁flowers",
+ -10.696124076843262
+ ],
+ [
+ "▁appreciate",
+ -10.697006225585938
+ ],
+ [
+ "▁dead",
+ -10.697439193725586
+ ],
+ [
+ "▁Fire",
+ -10.697539329528809
+ ],
+ [
+ "▁cela",
+ -10.697591781616211
+ ],
+ [
+ "▁Ph",
+ -10.697633743286133
+ ],
+ [
+ "▁arrive",
+ -10.697921752929688
+ ],
+ [
+ "▁purposes",
+ -10.698213577270508
+ ],
+ [
+ "▁qualité",
+ -10.698226928710938
+ ],
+ [
+ "▁restaurants",
+ -10.698478698730469
+ ],
+ [
+ "▁advertising",
+ -10.698541641235352
+ ],
+ [
+ "cur",
+ -10.69855785369873
+ ],
+ [
+ "▁ça",
+ -10.698973655700684
+ ],
+ [
+ "▁introduced",
+ -10.699088096618652
+ ],
+ [
+ "▁returned",
+ -10.699111938476562
+ ],
+ [
+ "▁desire",
+ -10.699511528015137
+ ],
+ [
+ "▁soul",
+ -10.699983596801758
+ ],
+ [
+ "▁Technology",
+ -10.699994087219238
+ ],
+ [
+ ");",
+ -10.700163841247559
+ ],
+ [
+ "▁Royal",
+ -10.700282096862793
+ ],
+ [
+ "tant",
+ -10.70068645477295
+ ],
+ [
+ "▁possibly",
+ -10.700702667236328
+ ],
+ [
+ "▁consumers",
+ -10.700812339782715
+ ],
+ [
+ "▁doua",
+ -10.70097541809082
+ ],
+ [
+ "ified",
+ -10.70097827911377
+ ],
+ [
+ "▁Award",
+ -10.70114803314209
+ ],
+ [
+ "toutes",
+ -10.70130443572998
+ ],
+ [
+ "▁meant",
+ -10.701325416564941
+ ],
+ [
+ "ezi",
+ -10.701616287231445
+ ],
+ [
+ "▁plu",
+ -10.701766014099121
+ ],
+ [
+ "ţii",
+ -10.7021484375
+ ],
+ [
+ "▁talent",
+ -10.702789306640625
+ ],
+ [
+ "▁Security",
+ -10.703309059143066
+ ],
+ [
+ "arii",
+ -10.703352928161621
+ ],
+ [
+ "▁zi",
+ -10.703455924987793
+ ],
+ [
+ "▁Shop",
+ -10.703667640686035
+ ],
+ [
+ "▁breakfast",
+ -10.704107284545898
+ ],
+ [
+ "▁trial",
+ -10.704485893249512
+ ],
+ [
+ "ami",
+ -10.704936981201172
+ ],
+ [
+ "▁register",
+ -10.705301284790039
+ ],
+ [
+ "unserer",
+ -10.705646514892578
+ ],
+ [
+ "▁solar",
+ -10.705697059631348
+ ],
+ [
+ "▁deals",
+ -10.70591926574707
+ ],
+ [
+ "▁Ku",
+ -10.7059326171875
+ ],
+ [
+ "To",
+ -10.706186294555664
+ ],
+ [
+ "bat",
+ -10.70680046081543
+ ],
+ [
+ "MC",
+ -10.707010269165039
+ ],
+ [
+ "▁Global",
+ -10.707018852233887
+ ],
+ [
+ "у",
+ -10.707405090332031
+ ],
+ [
+ "▁nor",
+ -10.707818984985352
+ ],
+ [
+ "▁milk",
+ -10.707868576049805
+ ],
+ [
+ "▁choices",
+ -10.708206176757812
+ ],
+ [
+ "»",
+ -10.7086763381958
+ ],
+ [
+ "▁Sur",
+ -10.708695411682129
+ ],
+ [
+ "more",
+ -10.708739280700684
+ ],
+ [
+ "48",
+ -10.709024429321289
+ ],
+ [
+ "67",
+ -10.709375381469727
+ ],
+ [
+ "▁replacement",
+ -10.709942817687988
+ ],
+ [
+ "34",
+ -10.710440635681152
+ ],
+ [
+ "▁chocolate",
+ -10.710485458374023
+ ],
+ [
+ "▁Family",
+ -10.71059513092041
+ ],
+ [
+ "This",
+ -10.71122932434082
+ ],
+ [
+ "▁novel",
+ -10.711435317993164
+ ],
+ [
+ "▁Chicago",
+ -10.711563110351562
+ ],
+ [
+ "▁participate",
+ -10.71166706085205
+ ],
+ [
+ "▁trei",
+ -10.712727546691895
+ ],
+ [
+ "▁monthly",
+ -10.713729858398438
+ ],
+ [
+ "▁survey",
+ -10.713977813720703
+ ],
+ [
+ "▁End",
+ -10.714285850524902
+ ],
+ [
+ "▁Medical",
+ -10.71442699432373
+ ],
+ [
+ "autres",
+ -10.714678764343262
+ ],
+ [
+ "rich",
+ -10.714698791503906
+ ],
+ [
+ "▁bike",
+ -10.714703559875488
+ ],
+ [
+ "▁eventually",
+ -10.714717864990234
+ ],
+ [
+ "▁HD",
+ -10.714722633361816
+ ],
+ [
+ "bil",
+ -10.714744567871094
+ ],
+ [
+ "cent",
+ -10.714902877807617
+ ],
+ [
+ "▁afin",
+ -10.715676307678223
+ ],
+ [
+ "▁surgery",
+ -10.716160774230957
+ ],
+ [
+ "▁sin",
+ -10.716455459594727
+ ],
+ [
+ "▁manufacturing",
+ -10.716955184936523
+ ],
+ [
+ "▁consumer",
+ -10.717245101928711
+ ],
+ [
+ "system",
+ -10.717306137084961
+ ],
+ [
+ "▁object",
+ -10.717400550842285
+ ],
+ [
+ "▁Ju",
+ -10.717422485351562
+ ],
+ [
+ "ered",
+ -10.7178373336792
+ ],
+ [
+ "rac",
+ -10.718070030212402
+ ],
+ [
+ "▁clinical",
+ -10.718664169311523
+ ],
+ [
+ "▁dollars",
+ -10.719761848449707
+ ],
+ [
+ "▁chain",
+ -10.71994686126709
+ ],
+ [
+ "▁afternoon",
+ -10.720196723937988
+ ],
+ [
+ "▁ligne",
+ -10.720422744750977
+ ],
+ [
+ "▁accounts",
+ -10.721806526184082
+ ],
+ [
+ "ving",
+ -10.722037315368652
+ ],
+ [
+ "▁Australian",
+ -10.72240924835205
+ ],
+ [
+ "38",
+ -10.722542762756348
+ ],
+ [
+ "▁persoane",
+ -10.72258472442627
+ ],
+ [
+ "▁grande",
+ -10.722668647766113
+ ],
+ [
+ "▁Report",
+ -10.723472595214844
+ ],
+ [
+ "▁revenue",
+ -10.723649024963379
+ ],
+ [
+ "▁spre",
+ -10.723760604858398
+ ],
+ [
+ "▁cutting",
+ -10.7239990234375
+ ],
+ [
+ "▁approved",
+ -10.724133491516113
+ ],
+ [
+ "▁glad",
+ -10.724188804626465
+ ],
+ [
+ "chaque",
+ -10.724395751953125
+ ],
+ [
+ "win",
+ -10.724435806274414
+ ],
+ [
+ "▁waren",
+ -10.724733352661133
+ ],
+ [
+ "▁launched",
+ -10.725071907043457
+ ],
+ [
+ "▁layer",
+ -10.725645065307617
+ ],
+ [
+ "▁airport",
+ -10.725716590881348
+ ],
+ [
+ "▁effectively",
+ -10.72572135925293
+ ],
+ [
+ "▁coach",
+ -10.725946426391602
+ ],
+ [
+ "dé",
+ -10.726130485534668
+ ],
+ [
+ "LE",
+ -10.72627067565918
+ ],
+ [
+ "▁müssen",
+ -10.726386070251465
+ ],
+ [
+ "plan",
+ -10.726641654968262
+ ],
+ [
+ "dan",
+ -10.726705551147461
+ ],
+ [
+ "55",
+ -10.726786613464355
+ ],
+ [
+ "bringing",
+ -10.726895332336426
+ ],
+ [
+ "▁$2",
+ -10.726995468139648
+ ],
+ [
+ "nce",
+ -10.727181434631348
+ ],
+ [
+ "▁inspiration",
+ -10.728177070617676
+ ],
+ [
+ "You",
+ -10.728657722473145
+ ],
+ [
+ "▁soll",
+ -10.729095458984375
+ ],
+ [
+ "▁seemed",
+ -10.729595184326172
+ ],
+ [
+ "▁flight",
+ -10.729687690734863
+ ],
+ [
+ "▁prima",
+ -10.729883193969727
+ ],
+ [
+ "▁Welt",
+ -10.730123519897461
+ ],
+ [
+ "▁jetzt",
+ -10.730315208435059
+ ],
+ [
+ "ky",
+ -10.730428695678711
+ ],
+ [
+ "▁Western",
+ -10.73054027557373
+ ],
+ [
+ "▁label",
+ -10.730600357055664
+ ],
+ [
+ "▁möglich",
+ -10.73081111907959
+ ],
+ [
+ "▁input",
+ -10.730862617492676
+ ],
+ [
+ "▁laws",
+ -10.730995178222656
+ ],
+ [
+ "▁personnes",
+ -10.731708526611328
+ ],
+ [
+ "▁paying",
+ -10.731731414794922
+ ],
+ [
+ "▁Uhr",
+ -10.73173713684082
+ ],
+ [
+ "▁Mary",
+ -10.731745719909668
+ ],
+ [
+ "pur",
+ -10.73190689086914
+ ],
+ [
+ "▁covers",
+ -10.732133865356445
+ ],
+ [
+ "▁throw",
+ -10.732522964477539
+ ],
+ [
+ "▁Tor",
+ -10.733281135559082
+ ],
+ [
+ "▁bat",
+ -10.73355484008789
+ ],
+ [
+ "▁Gr",
+ -10.73373031616211
+ ],
+ [
+ "▁farm",
+ -10.73376178741455
+ ],
+ [
+ "▁improved",
+ -10.733843803405762
+ ],
+ [
+ "▁fără",
+ -10.734286308288574
+ ],
+ [
+ "▁theme",
+ -10.73437213897705
+ ],
+ [
+ "pens",
+ -10.734865188598633
+ ],
+ [
+ "▁Cup",
+ -10.734975814819336
+ ],
+ [
+ "▁settings",
+ -10.735114097595215
+ ],
+ [
+ "▁hire",
+ -10.735234260559082
+ ],
+ [
+ "▁massive",
+ -10.735248565673828
+ ],
+ [
+ "▁generate",
+ -10.735405921936035
+ ],
+ [
+ "▁earn",
+ -10.735837936401367
+ ],
+ [
+ "▁tab",
+ -10.736431121826172
+ ],
+ [
+ "For",
+ -10.736616134643555
+ ],
+ [
+ "gang",
+ -10.736891746520996
+ ],
+ [
+ "▁hin",
+ -10.73709487915039
+ ],
+ [
+ "▁roll",
+ -10.737113952636719
+ ],
+ [
+ "▁engagement",
+ -10.737157821655273
+ ],
+ [
+ "▁signed",
+ -10.737177848815918
+ ],
+ [
+ "▁League",
+ -10.737323760986328
+ ],
+ [
+ "▁registration",
+ -10.737931251525879
+ ],
+ [
+ "▁première",
+ -10.738763809204102
+ ],
+ [
+ "isse",
+ -10.73896598815918
+ ],
+ [
+ "▁university",
+ -10.739027976989746
+ ],
+ [
+ "ell",
+ -10.739157676696777
+ ],
+ [
+ "▁nou",
+ -10.739169120788574
+ ],
+ [
+ "rog",
+ -10.739191055297852
+ ],
+ [
+ "▁sitting",
+ -10.739206314086914
+ ],
+ [
+ "▁cazul",
+ -10.739571571350098
+ ],
+ [
+ "▁surrounding",
+ -10.73983383178711
+ ],
+ [
+ "▁Asia",
+ -10.740357398986816
+ ],
+ [
+ "▁bath",
+ -10.740825653076172
+ ],
+ [
+ "hal",
+ -10.740923881530762
+ ],
+ [
+ "▁plate",
+ -10.741026878356934
+ ],
+ [
+ "▁tests",
+ -10.741151809692383
+ ],
+ [
+ "▁presentation",
+ -10.741156578063965
+ ],
+ [
+ "▁chicken",
+ -10.741501808166504
+ ],
+ [
+ "▁Val",
+ -10.741586685180664
+ ],
+ [
+ "ably",
+ -10.74166488647461
+ ],
+ [
+ "▁magazine",
+ -10.741697311401367
+ ],
+ [
+ "▁Maybe",
+ -10.74187183380127
+ ],
+ [
+ "▁sauce",
+ -10.742673873901367
+ ],
+ [
+ "TC",
+ -10.742887496948242
+ ],
+ [
+ "▁exclusive",
+ -10.74296760559082
+ ],
+ [
+ "86",
+ -10.74306869506836
+ ],
+ [
+ "▁teeth",
+ -10.743474960327148
+ ],
+ [
+ "▁regularly",
+ -10.743524551391602
+ ],
+ [
+ "sed",
+ -10.743824005126953
+ ],
+ [
+ "gro",
+ -10.744174003601074
+ ],
+ [
+ "He",
+ -10.744211196899414
+ ],
+ [
+ "▁2017.",
+ -10.744302749633789
+ ],
+ [
+ "▁template",
+ -10.74489688873291
+ ],
+ [
+ "▁gleich",
+ -10.744938850402832
+ ],
+ [
+ "bal",
+ -10.745061874389648
+ ],
+ [
+ "▁African",
+ -10.74511432647705
+ ],
+ [
+ "în",
+ -10.745231628417969
+ ],
+ [
+ "▁rep",
+ -10.74543571472168
+ ],
+ [
+ "▁beat",
+ -10.74588394165039
+ ],
+ [
+ "▁deck",
+ -10.746064186096191
+ ],
+ [
+ "▁intended",
+ -10.746221542358398
+ ],
+ [
+ "▁para",
+ -10.746513366699219
+ ],
+ [
+ "▁IP",
+ -10.746712684631348
+ ],
+ [
+ "▁bra",
+ -10.746881484985352
+ ],
+ [
+ "▁forces",
+ -10.746966361999512
+ ],
+ [
+ "▁routine",
+ -10.747184753417969
+ ],
+ [
+ "▁Jahre",
+ -10.747758865356445
+ ],
+ [
+ "▁Bad",
+ -10.74797534942627
+ ],
+ [
+ "▁drivers",
+ -10.748074531555176
+ ],
+ [
+ "▁updates",
+ -10.748095512390137
+ ],
+ [
+ "▁elegant",
+ -10.748279571533203
+ ],
+ [
+ "▁external",
+ -10.748444557189941
+ ],
+ [
+ "▁engineering",
+ -10.748819351196289
+ ],
+ [
+ "ender",
+ -10.749544143676758
+ ],
+ [
+ "table",
+ -10.749755859375
+ ],
+ [
+ "inter",
+ -10.749878883361816
+ ],
+ [
+ "▁Romania",
+ -10.749948501586914
+ ],
+ [
+ "▁zile",
+ -10.750468254089355
+ ],
+ [
+ "▁luxury",
+ -10.750570297241211
+ ],
+ [
+ "▁calling",
+ -10.750750541687012
+ ],
+ [
+ "▁cooking",
+ -10.75101375579834
+ ],
+ [
+ "▁component",
+ -10.75114631652832
+ ],
+ [
+ "wan",
+ -10.75121021270752
+ ],
+ [
+ "schen",
+ -10.751212120056152
+ ],
+ [
+ "▁birth",
+ -10.751242637634277
+ ],
+ [
+ "asupra",
+ -10.751349449157715
+ ],
+ [
+ "Co",
+ -10.751471519470215
+ ],
+ [
+ "▁opt",
+ -10.75153923034668
+ ],
+ [
+ "▁discovered",
+ -10.751860618591309
+ ],
+ [
+ "▁teach",
+ -10.752084732055664
+ ],
+ [
+ "▁Son",
+ -10.75234317779541
+ ],
+ [
+ "▁guest",
+ -10.752384185791016
+ ],
+ [
+ "▁dogs",
+ -10.752695083618164
+ ],
+ [
+ "▁2003",
+ -10.752745628356934
+ ],
+ [
+ "▁behavior",
+ -10.752750396728516
+ ],
+ [
+ "pé",
+ -10.7529935836792
+ ],
+ [
+ "63",
+ -10.75316333770752
+ ],
+ [
+ "▁Human",
+ -10.753702163696289
+ ],
+ [
+ "▁expression",
+ -10.754800796508789
+ ],
+ [
+ "▁nevoie",
+ -10.754936218261719
+ ],
+ [
+ "▁recherche",
+ -10.75528621673584
+ ],
+ [
+ "ging",
+ -10.755767822265625
+ ],
+ [
+ "related",
+ -10.755948066711426
+ ],
+ [
+ "▁discount",
+ -10.756040573120117
+ ],
+ [
+ "▁Brown",
+ -10.756054878234863
+ ],
+ [
+ "▁Such",
+ -10.756107330322266
+ ],
+ [
+ "▁Ve",
+ -10.757149696350098
+ ],
+ [
+ "▁height",
+ -10.757265090942383
+ ],
+ [
+ "clo",
+ -10.757414817810059
+ ],
+ [
+ "▁incredible",
+ -10.757912635803223
+ ],
+ [
+ "▁bas",
+ -10.757916450500488
+ ],
+ [
+ "▁mă",
+ -10.75798225402832
+ ],
+ [
+ "▁purchased",
+ -10.758240699768066
+ ],
+ [
+ "▁compte",
+ -10.75831127166748
+ ],
+ [
+ "▁instructions",
+ -10.758537292480469
+ ],
+ [
+ "▁Instead",
+ -10.75866985321045
+ ],
+ [
+ "▁output",
+ -10.758706092834473
+ ],
+ [
+ "▁mom",
+ -10.758886337280273
+ ],
+ [
+ "DR",
+ -10.759828567504883
+ ],
+ [
+ "89",
+ -10.760168075561523
+ ],
+ [
+ "▁reduced",
+ -10.760621070861816
+ ],
+ [
+ "98",
+ -10.7606840133667
+ ],
+ [
+ "▁constant",
+ -10.760879516601562
+ ],
+ [
+ "▁therapy",
+ -10.762417793273926
+ ],
+ [
+ "▁capable",
+ -10.762757301330566
+ ],
+ [
+ "mark",
+ -10.763265609741211
+ ],
+ [
+ "▁Sometimes",
+ -10.76332950592041
+ ],
+ [
+ "▁joy",
+ -10.763419151306152
+ ],
+ [
+ "▁perfectly",
+ -10.763589859008789
+ ],
+ [
+ "▁painting",
+ -10.763704299926758
+ ],
+ [
+ "avait",
+ -10.763765335083008
+ ],
+ [
+ "▁Sha",
+ -10.764384269714355
+ ],
+ [
+ "▁dat",
+ -10.764463424682617
+ ],
+ [
+ "▁produits",
+ -10.764479637145996
+ ],
+ [
+ "tric",
+ -10.76456356048584
+ ],
+ [
+ "ierte",
+ -10.765153884887695
+ ],
+ [
+ "▁Smith",
+ -10.765836715698242
+ ],
+ [
+ "▁trebui",
+ -10.766264915466309
+ ],
+ [
+ "▁beaucoup",
+ -10.766630172729492
+ ],
+ [
+ "▁chosen",
+ -10.767189025878906
+ ],
+ [
+ "▁cre",
+ -10.76732063293457
+ ],
+ [
+ "▁complet",
+ -10.767341613769531
+ ],
+ [
+ "▁Ltd",
+ -10.767599105834961
+ ],
+ [
+ "▁recovery",
+ -10.76781940460205
+ ],
+ [
+ "▁district",
+ -10.768423080444336
+ ],
+ [
+ "78",
+ -10.768640518188477
+ ],
+ [
+ "▁Unter",
+ -10.76872730255127
+ ],
+ [
+ "▁schnell",
+ -10.768729209899902
+ ],
+ [
+ "▁apart",
+ -10.768943786621094
+ ],
+ [
+ "▁phase",
+ -10.76894760131836
+ ],
+ [
+ "▁seeking",
+ -10.769091606140137
+ ],
+ [
+ "▁mark",
+ -10.769148826599121
+ ],
+ [
+ "▁pet",
+ -10.769233703613281
+ ],
+ [
+ "▁PDF",
+ -10.769296646118164
+ ],
+ [
+ "▁efficiency",
+ -10.769577980041504
+ ],
+ [
+ "▁buildings",
+ -10.769611358642578
+ ],
+ [
+ "69",
+ -10.769723892211914
+ ],
+ [
+ "▁sens",
+ -10.769858360290527
+ ],
+ [
+ "▁Video",
+ -10.770115852355957
+ ],
+ [
+ "▁destination",
+ -10.770181655883789
+ ],
+ [
+ "▁female",
+ -10.770319938659668
+ ],
+ [
+ "▁supporting",
+ -10.770674705505371
+ ],
+ [
+ "▁signs",
+ -10.77077865600586
+ ],
+ [
+ "▁appeal",
+ -10.770784378051758
+ ],
+ [
+ "76",
+ -10.77110481262207
+ ],
+ [
+ "▁favourite",
+ -10.771612167358398
+ ],
+ [
+ "ock",
+ -10.771702766418457
+ ],
+ [
+ "▁readers",
+ -10.771757125854492
+ ],
+ [
+ "▁Did",
+ -10.771868705749512
+ ],
+ [
+ "rou",
+ -10.772045135498047
+ ],
+ [
+ "PA",
+ -10.77222728729248
+ ],
+ [
+ "▁Jean",
+ -10.772480964660645
+ ],
+ [
+ "▁Em",
+ -10.772586822509766
+ ],
+ [
+ "pass",
+ -10.77280330657959
+ ],
+ [
+ "▁Zi",
+ -10.773090362548828
+ ],
+ [
+ "▁între",
+ -10.773261070251465
+ ],
+ [
+ "▁fly",
+ -10.773427963256836
+ ],
+ [
+ "mos",
+ -10.773666381835938
+ ],
+ [
+ "▁emotional",
+ -10.773860931396484
+ ],
+ [
+ "asse",
+ -10.774768829345703
+ ],
+ [
+ "▁sessions",
+ -10.775086402893066
+ ],
+ [
+ "▁symptoms",
+ -10.77564811706543
+ ],
+ [
+ "▁died",
+ -10.776217460632324
+ ],
+ [
+ "▁seconds",
+ -10.776628494262695
+ ],
+ [
+ "▁procedure",
+ -10.777206420898438
+ ],
+ [
+ "▁express",
+ -10.777420997619629
+ ],
+ [
+ "▁două",
+ -10.777885437011719
+ ],
+ [
+ "▁valid",
+ -10.778393745422363
+ ],
+ [
+ "▁euro",
+ -10.7788667678833
+ ],
+ [
+ "▁interests",
+ -10.779032707214355
+ ],
+ [
+ "Having",
+ -10.779237747192383
+ ],
+ [
+ "▁hundreds",
+ -10.779669761657715
+ ],
+ [
+ "grad",
+ -10.780023574829102
+ ],
+ [
+ "▁neuen",
+ -10.780084609985352
+ ],
+ [
+ "▁cook",
+ -10.780552864074707
+ ],
+ [
+ "▁pur",
+ -10.780834197998047
+ ],
+ [
+ "▁charges",
+ -10.781024932861328
+ ],
+ [
+ "sche",
+ -10.78118896484375
+ ],
+ [
+ "▁smile",
+ -10.781468391418457
+ ],
+ [
+ "▁festival",
+ -10.781611442565918
+ ],
+ [
+ "cho",
+ -10.781672477722168
+ ],
+ [
+ "▁£",
+ -10.781937599182129
+ ],
+ [
+ "cht",
+ -10.78201675415039
+ ],
+ [
+ "▁macht",
+ -10.782021522521973
+ ],
+ [
+ "▁Wasser",
+ -10.782028198242188
+ ],
+ [
+ "▁Cap",
+ -10.78226375579834
+ ],
+ [
+ "▁Learn",
+ -10.78274154663086
+ ],
+ [
+ "▁load",
+ -10.783162117004395
+ ],
+ [
+ "▁aici",
+ -10.783225059509277
+ ],
+ [
+ "▁Ch",
+ -10.784143447875977
+ ],
+ [
+ "▁cycle",
+ -10.784223556518555
+ ],
+ [
+ "▁carried",
+ -10.784337997436523
+ ],
+ [
+ "▁jusqu",
+ -10.784517288208008
+ ],
+ [
+ "stein",
+ -10.78505802154541
+ ],
+ [
+ "ski",
+ -10.78513240814209
+ ],
+ [
+ "cap",
+ -10.78579330444336
+ ],
+ [
+ "▁Bal",
+ -10.785852432250977
+ ],
+ [
+ "▁minor",
+ -10.786053657531738
+ ],
+ [
+ "77",
+ -10.786175727844238
+ ],
+ [
+ "▁considering",
+ -10.78632640838623
+ ],
+ [
+ "innen",
+ -10.78644847869873
+ ],
+ [
+ "▁greatest",
+ -10.787055015563965
+ ],
+ [
+ "▁Training",
+ -10.787137031555176
+ ],
+ [
+ "08",
+ -10.787307739257812
+ ],
+ [
+ "▁significantly",
+ -10.787607192993164
+ ],
+ [
+ "gé",
+ -10.787728309631348
+ ],
+ [
+ "▁dumpster",
+ -10.788351058959961
+ ],
+ [
+ "▁allem",
+ -10.788930892944336
+ ],
+ [
+ "▁bonus",
+ -10.7889404296875
+ ],
+ [
+ "▁guy",
+ -10.789036750793457
+ ],
+ [
+ "fel",
+ -10.78904914855957
+ ],
+ [
+ "▁lifestyle",
+ -10.789241790771484
+ ],
+ [
+ "▁Bro",
+ -10.78961181640625
+ ],
+ [
+ "▁implement",
+ -10.789687156677246
+ ],
+ [
+ "lock",
+ -10.790046691894531
+ ],
+ [
+ "▁Earth",
+ -10.790142059326172
+ ],
+ [
+ "kar",
+ -10.790733337402344
+ ],
+ [
+ "▁invest",
+ -10.790833473205566
+ ],
+ [
+ "▁river",
+ -10.790933609008789
+ ],
+ [
+ "▁accurate",
+ -10.791494369506836
+ ],
+ [
+ "▁mu",
+ -10.791579246520996
+ ],
+ [
+ "▁celebrate",
+ -10.792119979858398
+ ],
+ [
+ "▁ran",
+ -10.79256820678711
+ ],
+ [
+ "▁bigger",
+ -10.792988777160645
+ ],
+ [
+ "▁Mer",
+ -10.793476104736328
+ ],
+ [
+ "▁millions",
+ -10.793486595153809
+ ],
+ [
+ "▁partie",
+ -10.793563842773438
+ ],
+ [
+ "▁dazu",
+ -10.793951988220215
+ ],
+ [
+ "▁Full",
+ -10.794130325317383
+ ],
+ [
+ "gie",
+ -10.794207572937012
+ ],
+ [
+ "bot",
+ -10.794373512268066
+ ],
+ [
+ "roll",
+ -10.79472827911377
+ ],
+ [
+ "▁Women",
+ -10.795303344726562
+ ],
+ [
+ "▁compare",
+ -10.796135902404785
+ ],
+ [
+ "▁van",
+ -10.796503067016602
+ ],
+ [
+ "▁apps",
+ -10.796521186828613
+ ],
+ [
+ "PC",
+ -10.797050476074219
+ ],
+ [
+ "▁drei",
+ -10.79736042022705
+ ],
+ [
+ "▁maison",
+ -10.797588348388672
+ ],
+ [
+ "▁knows",
+ -10.797712326049805
+ ],
+ [
+ "rid",
+ -10.797972679138184
+ ],
+ [
+ "62",
+ -10.798396110534668
+ ],
+ [
+ "class",
+ -10.798508644104004
+ ],
+ [
+ "▁chez",
+ -10.798669815063477
+ ],
+ [
+ "char",
+ -10.798828125
+ ],
+ [
+ "88",
+ -10.798989295959473
+ ],
+ [
+ "▁cast",
+ -10.79948902130127
+ ],
+ [
+ "▁examples",
+ -10.79973030090332
+ ],
+ [
+ "▁Therefore",
+ -10.799823760986328
+ ],
+ [
+ "▁topics",
+ -10.799941062927246
+ ],
+ [
+ "with",
+ -10.80013656616211
+ ],
+ [
+ "▁Anti",
+ -10.800555229187012
+ ],
+ [
+ "how",
+ -10.800620079040527
+ ],
+ [
+ "▁whom",
+ -10.80094051361084
+ ],
+ [
+ "▁Deutschland",
+ -10.801124572753906
+ ],
+ [
+ "tine",
+ -10.80113697052002
+ ],
+ [
+ "▁CEO",
+ -10.801224708557129
+ ],
+ [
+ "▁truck",
+ -10.801350593566895
+ ],
+ [
+ "▁Which",
+ -10.8015718460083
+ ],
+ [
+ "erie",
+ -10.802017211914062
+ ],
+ [
+ "fect",
+ -10.802069664001465
+ ],
+ [
+ "bou",
+ -10.8026762008667
+ ],
+ [
+ "▁(1",
+ -10.802818298339844
+ ],
+ [
+ "sum",
+ -10.802980422973633
+ ],
+ [
+ "▁bonne",
+ -10.803068161010742
+ ],
+ [
+ "▁remaining",
+ -10.80321216583252
+ ],
+ [
+ "▁equal",
+ -10.803543090820312
+ ],
+ [
+ "▁engage",
+ -10.803561210632324
+ ],
+ [
+ "▁RE",
+ -10.803849220275879
+ ],
+ [
+ "style",
+ -10.804182052612305
+ ],
+ [
+ "▁urma",
+ -10.804337501525879
+ ],
+ [
+ "▁Grund",
+ -10.80496883392334
+ ],
+ [
+ "ür",
+ -10.8051176071167
+ ],
+ [
+ "▁font",
+ -10.805353164672852
+ ],
+ [
+ "▁assets",
+ -10.805916786193848
+ ],
+ [
+ "AL",
+ -10.806102752685547
+ ],
+ [
+ "▁rear",
+ -10.80635929107666
+ ],
+ [
+ "▁contemporary",
+ -10.80646800994873
+ ],
+ [
+ "▁occur",
+ -10.8067045211792
+ ],
+ [
+ "rated",
+ -10.806941986083984
+ ],
+ [
+ "▁tight",
+ -10.807088851928711
+ ],
+ [
+ "▁machines",
+ -10.807921409606934
+ ],
+ [
+ "▁0.",
+ -10.808456420898438
+ ],
+ [
+ "▁Aber",
+ -10.808470726013184
+ ],
+ [
+ "sol",
+ -10.808517456054688
+ ],
+ [
+ "rü",
+ -10.80858039855957
+ ],
+ [
+ "▁2007",
+ -10.809479713439941
+ ],
+ [
+ "gg",
+ -10.809488296508789
+ ],
+ [
+ "▁unul",
+ -10.809691429138184
+ ],
+ [
+ "▁était",
+ -10.809908866882324
+ ],
+ [
+ "▁capture",
+ -10.809980392456055
+ ],
+ [
+ "▁command",
+ -10.810037612915039
+ ],
+ [
+ "▁wire",
+ -10.810425758361816
+ ],
+ [
+ "▁shift",
+ -10.810762405395508
+ ],
+ [
+ "▁bread",
+ -10.81084156036377
+ ],
+ [
+ "▁causes",
+ -10.810937881469727
+ ],
+ [
+ "PI",
+ -10.810938835144043
+ ],
+ [
+ "SC",
+ -10.811086654663086
+ ],
+ [
+ "▁lights",
+ -10.811190605163574
+ ],
+ [
+ "▁lived",
+ -10.811293601989746
+ ],
+ [
+ "mul",
+ -10.811446189880371
+ ],
+ [
+ "▁Cur",
+ -10.811917304992676
+ ],
+ [
+ "▁Richard",
+ -10.811973571777344
+ ],
+ [
+ "37",
+ -10.812638282775879
+ ],
+ [
+ "▁cup",
+ -10.812737464904785
+ ],
+ [
+ "▁fields",
+ -10.812983512878418
+ ],
+ [
+ "▁crusher",
+ -10.813389778137207
+ ],
+ [
+ "65",
+ -10.813774108886719
+ ],
+ [
+ "avons",
+ -10.813822746276855
+ ],
+ [
+ "▁gear",
+ -10.813835144042969
+ ],
+ [
+ "▁standing",
+ -10.813844680786133
+ ],
+ [
+ "▁thick",
+ -10.81445026397705
+ ],
+ [
+ "aff",
+ -10.815132141113281
+ ],
+ [
+ "ments",
+ -10.815434455871582
+ ],
+ [
+ "▁conflict",
+ -10.815728187561035
+ ],
+ [
+ "ität",
+ -10.815825462341309
+ ],
+ [
+ "▁worse",
+ -10.816295623779297
+ ],
+ [
+ "SE",
+ -10.816332817077637
+ ],
+ [
+ "imi",
+ -10.816459655761719
+ ],
+ [
+ "▁dating",
+ -10.817033767700195
+ ],
+ [
+ "Do",
+ -10.817073822021484
+ ],
+ [
+ "▁flexible",
+ -10.817093849182129
+ ],
+ [
+ "ologie",
+ -10.817131996154785
+ ],
+ [
+ "SU",
+ -10.817200660705566
+ ],
+ [
+ "▁contribute",
+ -10.817306518554688
+ ],
+ [
+ "▁denn",
+ -10.817428588867188
+ ],
+ [
+ "▁appointment",
+ -10.81746768951416
+ ],
+ [
+ "▁ticket",
+ -10.817523002624512
+ ],
+ [
+ "bed",
+ -10.817892074584961
+ ],
+ [
+ "▁2019.",
+ -10.817936897277832
+ ],
+ [
+ "▁tasks",
+ -10.81871223449707
+ ],
+ [
+ "▁carbon",
+ -10.818734169006348
+ ],
+ [
+ "▁situations",
+ -10.819400787353516
+ ],
+ [
+ "MA",
+ -10.819402694702148
+ ],
+ [
+ "▁portion",
+ -10.819498062133789
+ ],
+ [
+ "▁urban",
+ -10.819585800170898
+ ],
+ [
+ "▁Canadian",
+ -10.819805145263672
+ ],
+ [
+ "▁Bur",
+ -10.819937705993652
+ ],
+ [
+ "▁pack",
+ -10.81995964050293
+ ],
+ [
+ "▁effet",
+ -10.819992065429688
+ ],
+ [
+ "▁Ball",
+ -10.82008171081543
+ ],
+ [
+ "▁timpul",
+ -10.82014274597168
+ ],
+ [
+ "▁owned",
+ -10.820211410522461
+ ],
+ [
+ "▁surprise",
+ -10.820413589477539
+ ],
+ [
+ "▁Mu",
+ -10.820582389831543
+ ],
+ [
+ "▁decades",
+ -10.821001052856445
+ ],
+ [
+ "▁affected",
+ -10.821728706359863
+ ],
+ [
+ "▁proven",
+ -10.821732521057129
+ ],
+ [
+ "▁Fe",
+ -10.821990966796875
+ ],
+ [
+ "zy",
+ -10.822042465209961
+ ],
+ [
+ "42",
+ -10.822175979614258
+ ],
+ [
+ "▁trend",
+ -10.8223876953125
+ ],
+ [
+ "▁autres",
+ -10.82262897491455
+ ],
+ [
+ "No",
+ -10.823028564453125
+ ],
+ [
+ "▁nine",
+ -10.823565483093262
+ ],
+ [
+ "ON",
+ -10.82376480102539
+ ],
+ [
+ "NE",
+ -10.823953628540039
+ ],
+ [
+ "oli",
+ -10.824359893798828
+ ],
+ [
+ "▁Daniel",
+ -10.824434280395508
+ ],
+ [
+ "▁spa",
+ -10.824939727783203
+ ],
+ [
+ "▁messages",
+ -10.825084686279297
+ ],
+ [
+ "PS",
+ -10.825183868408203
+ ],
+ [
+ "47",
+ -10.825703620910645
+ ],
+ [
+ "▁doch",
+ -10.826032638549805
+ ],
+ [
+ "▁improvement",
+ -10.826187133789062
+ ],
+ [
+ "▁mountain",
+ -10.826350212097168
+ ],
+ [
+ "▁Room",
+ -10.826451301574707
+ ],
+ [
+ "▁edition",
+ -10.826546669006348
+ ],
+ [
+ "▁musical",
+ -10.826712608337402
+ ],
+ [
+ "CP",
+ -10.827024459838867
+ ],
+ [
+ "▁Mill",
+ -10.827027320861816
+ ],
+ [
+ "▁steht",
+ -10.827740669250488
+ ],
+ [
+ "▁determined",
+ -10.828083038330078
+ ],
+ [
+ "you",
+ -10.828392028808594
+ ],
+ [
+ "weg",
+ -10.828554153442383
+ ],
+ [
+ "▁Digital",
+ -10.828624725341797
+ ],
+ [
+ "▁filter",
+ -10.828903198242188
+ ],
+ [
+ "▁youth",
+ -10.829047203063965
+ ],
+ [
+ "▁assessment",
+ -10.829301834106445
+ ],
+ [
+ "▁butter",
+ -10.829370498657227
+ ],
+ [
+ "▁Watch",
+ -10.829427719116211
+ ],
+ [
+ "▁zusammen",
+ -10.829471588134766
+ ],
+ [
+ "▁View",
+ -10.829606056213379
+ ],
+ [
+ "09",
+ -10.829649925231934
+ ],
+ [
+ "▁sole",
+ -10.829816818237305
+ ],
+ [
+ ".00",
+ -10.830018997192383
+ ],
+ [
+ "33",
+ -10.83015251159668
+ ],
+ [
+ "▁export",
+ -10.830229759216309
+ ],
+ [
+ "ery",
+ -10.830373764038086
+ ],
+ [
+ "▁zurück",
+ -10.830426216125488
+ ],
+ [
+ "▁walls",
+ -10.83048152923584
+ ],
+ [
+ "▁recognize",
+ -10.8306884765625
+ ],
+ [
+ "law",
+ -10.830801963806152
+ ],
+ [
+ "▁parent",
+ -10.830863952636719
+ ],
+ [
+ "ST",
+ -10.831357955932617
+ ],
+ [
+ "▁description",
+ -10.831669807434082
+ ],
+ [
+ "MS",
+ -10.831887245178223
+ ],
+ [
+ "SM",
+ -10.83189582824707
+ ],
+ [
+ "▁Finally",
+ -10.831940650939941
+ ],
+ [
+ "▁hardware",
+ -10.831965446472168
+ ],
+ [
+ "ident",
+ -10.832464218139648
+ ],
+ [
+ "▁brown",
+ -10.832566261291504
+ ],
+ [
+ "▁kinds",
+ -10.832950592041016
+ ],
+ [
+ "▁Arts",
+ -10.83297061920166
+ ],
+ [
+ "▁concert",
+ -10.83341121673584
+ ],
+ [
+ "▁sec",
+ -10.83342456817627
+ ],
+ [
+ "▁represent",
+ -10.833512306213379
+ ],
+ [
+ "▁institutions",
+ -10.833597183227539
+ ],
+ [
+ "▁fur",
+ -10.833998680114746
+ ],
+ [
+ "▁Support",
+ -10.83403205871582
+ ],
+ [
+ "87",
+ -10.834076881408691
+ ],
+ [
+ "▁ease",
+ -10.834178924560547
+ ],
+ [
+ "▁feels",
+ -10.834218978881836
+ ],
+ [
+ "▁sheet",
+ -10.834342002868652
+ ],
+ [
+ "▁Though",
+ -10.83437442779541
+ ],
+ [
+ "▁propose",
+ -10.834381103515625
+ ],
+ [
+ "▁personnel",
+ -10.834409713745117
+ ],
+ [
+ "bie",
+ -10.834794044494629
+ ],
+ [
+ "▁contest",
+ -10.834836959838867
+ ],
+ [
+ "▁successfully",
+ -10.835152626037598
+ ],
+ [
+ "▁direkt",
+ -10.835397720336914
+ ],
+ [
+ "bietet",
+ -10.835597038269043
+ ],
+ [
+ "▁submit",
+ -10.835888862609863
+ ],
+ [
+ "▁sicher",
+ -10.835919380187988
+ ],
+ [
+ "▁Personal",
+ -10.83607006072998
+ ],
+ [
+ "94",
+ -10.836341857910156
+ ],
+ [
+ "61",
+ -10.836400985717773
+ ],
+ [
+ "▁Very",
+ -10.836540222167969
+ ],
+ [
+ "bol",
+ -10.836603164672852
+ ],
+ [
+ "▁ha",
+ -10.837089538574219
+ ],
+ [
+ "▁channel",
+ -10.8372220993042
+ ],
+ [
+ "mut",
+ -10.837289810180664
+ ],
+ [
+ "▁mouth",
+ -10.837342262268066
+ ],
+ [
+ "▁vast",
+ -10.837395668029785
+ ],
+ [
+ "▁Ob",
+ -10.837569236755371
+ ],
+ [
+ "lit",
+ -10.83763313293457
+ ],
+ [
+ "▁poly",
+ -10.837878227233887
+ ],
+ [
+ "▁trained",
+ -10.838102340698242
+ ],
+ [
+ "▁specialist",
+ -10.838122367858887
+ ],
+ [
+ "UL",
+ -10.83822250366211
+ ],
+ [
+ "▁seiner",
+ -10.838336944580078
+ ],
+ [
+ "SS",
+ -10.838627815246582
+ ],
+ [
+ "▁vacation",
+ -10.838672637939453
+ ],
+ [
+ "▁resume",
+ -10.839157104492188
+ ],
+ [
+ "▁constantly",
+ -10.839717864990234
+ ],
+ [
+ "▁treated",
+ -10.83986759185791
+ ],
+ [
+ "▁150",
+ -10.840936660766602
+ ],
+ [
+ "▁native",
+ -10.841246604919434
+ ],
+ [
+ "▁Russian",
+ -10.841329574584961
+ ],
+ [
+ "▁patterns",
+ -10.841371536254883
+ ],
+ [
+ "▁knowing",
+ -10.841670989990234
+ ],
+ [
+ "▁Pan",
+ -10.841682434082031
+ ],
+ [
+ "peri",
+ -10.841848373413086
+ ],
+ [
+ "aci",
+ -10.841864585876465
+ ],
+ [
+ "▁answers",
+ -10.842114448547363
+ ],
+ [
+ "▁heute",
+ -10.842985153198242
+ ],
+ [
+ "93",
+ -10.843056678771973
+ ],
+ [
+ "▁Winter",
+ -10.844083786010742
+ ],
+ [
+ "▁yes",
+ -10.844173431396484
+ ],
+ [
+ "SP",
+ -10.844185829162598
+ ],
+ [
+ "].",
+ -10.844388008117676
+ ],
+ [
+ "▁kein",
+ -10.844862937927246
+ ],
+ [
+ "▁introduce",
+ -10.8450927734375
+ ],
+ [
+ "-4",
+ -10.84555435180664
+ ],
+ [
+ "▁shoot",
+ -10.845762252807617
+ ],
+ [
+ "AR",
+ -10.84576416015625
+ ],
+ [
+ "▁receiving",
+ -10.845864295959473
+ ],
+ [
+ "▁intre",
+ -10.84702205657959
+ ],
+ [
+ "▁appeared",
+ -10.84708023071289
+ ],
+ [
+ "▁brother",
+ -10.847321510314941
+ ],
+ [
+ "▁extend",
+ -10.847765922546387
+ ],
+ [
+ "▁fara",
+ -10.848737716674805
+ ],
+ [
+ "▁kommt",
+ -10.848876953125
+ ],
+ [
+ "ali",
+ -10.848913192749023
+ ],
+ [
+ "▁numai",
+ -10.849047660827637
+ ],
+ [
+ "▁scientific",
+ -10.84913158416748
+ ],
+ [
+ "▁virtual",
+ -10.849145889282227
+ ],
+ [
+ "▁Ac",
+ -10.849513053894043
+ ],
+ [
+ "▁procedures",
+ -10.849631309509277
+ ],
+ [
+ "▁silver",
+ -10.849821090698242
+ ],
+ [
+ "▁leather",
+ -10.849979400634766
+ ],
+ [
+ "DA",
+ -10.85014820098877
+ ],
+ [
+ "▁executive",
+ -10.850263595581055
+ ],
+ [
+ "▁officials",
+ -10.850496292114258
+ ],
+ [
+ "▁agencies",
+ -10.850503921508789
+ ],
+ [
+ "▁Software",
+ -10.850540161132812
+ ],
+ [
+ "▁cor",
+ -10.850690841674805
+ ],
+ [
+ "Con",
+ -10.850741386413574
+ ],
+ [
+ "▁log",
+ -10.851066589355469
+ ],
+ [
+ "ț",
+ -10.851147651672363
+ ],
+ [
+ "02",
+ -10.851195335388184
+ ],
+ [
+ "▁7.",
+ -10.85245132446289
+ ],
+ [
+ "▁accepted",
+ -10.852483749389648
+ ],
+ [
+ "▁Berlin",
+ -10.852538108825684
+ ],
+ [
+ "ID",
+ -10.852582931518555
+ ],
+ [
+ "cot",
+ -10.852788925170898
+ ],
+ [
+ "▁employment",
+ -10.852799415588379
+ ],
+ [
+ "run",
+ -10.853020668029785
+ ],
+ [
+ "▁identified",
+ -10.853178977966309
+ ],
+ [
+ "96",
+ -10.853887557983398
+ ],
+ [
+ "▁déjà",
+ -10.853944778442383
+ ],
+ [
+ "▁cuisine",
+ -10.853952407836914
+ ],
+ [
+ "turi",
+ -10.854070663452148
+ ],
+ [
+ "▁Japanese",
+ -10.854316711425781
+ ],
+ [
+ "▁golf",
+ -10.854514122009277
+ ],
+ [
+ "▁Ki",
+ -10.854787826538086
+ ],
+ [
+ "▁carefully",
+ -10.854863166809082
+ ],
+ [
+ "▁remote",
+ -10.854973793029785
+ ],
+ [
+ "▁2018,",
+ -10.855148315429688
+ ],
+ [
+ "▁sus",
+ -10.855154991149902
+ ],
+ [
+ "tique",
+ -10.855293273925781
+ ],
+ [
+ "▁residential",
+ -10.855695724487305
+ ],
+ [
+ "97",
+ -10.855809211730957
+ ],
+ [
+ "▁Spring",
+ -10.855908393859863
+ ],
+ [
+ "▁Marketing",
+ -10.856186866760254
+ ],
+ [
+ "▁Control",
+ -10.85630989074707
+ ],
+ [
+ "var",
+ -10.856344223022461
+ ],
+ [
+ "▁historical",
+ -10.8563814163208
+ ],
+ [
+ "▁freedom",
+ -10.856423377990723
+ ],
+ [
+ "sure",
+ -10.856426239013672
+ ],
+ [
+ "▁broken",
+ -10.856796264648438
+ ],
+ [
+ "▁criminal",
+ -10.856949806213379
+ ],
+ [
+ "▁innovation",
+ -10.857075691223145
+ ],
+ [
+ "▁Italian",
+ -10.857192039489746
+ ],
+ [
+ "sper",
+ -10.857282638549805
+ ],
+ [
+ "▁cake",
+ -10.857653617858887
+ ],
+ [
+ "▁candidates",
+ -10.857894897460938
+ ],
+ [
+ "▁sizes",
+ -10.858267784118652
+ ],
+ [
+ "pel",
+ -10.858366966247559
+ ],
+ [
+ "▁frequently",
+ -10.85889720916748
+ ],
+ [
+ "▁planet",
+ -10.859138488769531
+ ],
+ [
+ "▁writer",
+ -10.859519958496094
+ ],
+ [
+ "1,",
+ -10.859569549560547
+ ],
+ [
+ "uvent",
+ -10.85959529876709
+ ],
+ [
+ "▁awareness",
+ -10.859807968139648
+ ],
+ [
+ "name",
+ -10.859954833984375
+ ],
+ [
+ "▁Children",
+ -10.859980583190918
+ ],
+ [
+ "▁relatively",
+ -10.860311508178711
+ ],
+ [
+ "▁pu",
+ -10.860321998596191
+ ],
+ [
+ "▁quiet",
+ -10.86038875579834
+ ],
+ [
+ "▁planned",
+ -10.860716819763184
+ ],
+ [
+ "▁election",
+ -10.861419677734375
+ ],
+ [
+ "▁6.",
+ -10.861761093139648
+ ],
+ [
+ "▁broad",
+ -10.861772537231445
+ ],
+ [
+ "▁skill",
+ -10.861835479736328
+ ],
+ [
+ "▁reasonable",
+ -10.862037658691406
+ ],
+ [
+ "▁Fort",
+ -10.862283706665039
+ ],
+ [
+ "▁aceea",
+ -10.862407684326172
+ ],
+ [
+ "▁arrived",
+ -10.86263370513916
+ ],
+ [
+ "▁payments",
+ -10.862680435180664
+ ],
+ [
+ "ack",
+ -10.862700462341309
+ ],
+ [
+ "▁Ort",
+ -10.863354682922363
+ ],
+ [
+ "▁investors",
+ -10.863364219665527
+ ],
+ [
+ "▁operate",
+ -10.86351203918457
+ ],
+ [
+ "ME",
+ -10.863556861877441
+ ],
+ [
+ "dic",
+ -10.863683700561523
+ ],
+ [
+ "▁foods",
+ -10.863731384277344
+ ],
+ [
+ "▁stick",
+ -10.863831520080566
+ ],
+ [
+ "▁agents",
+ -10.86412525177002
+ ],
+ [
+ "▁crowd",
+ -10.864175796508789
+ ],
+ [
+ "▁Students",
+ -10.864480972290039
+ ],
+ [
+ "▁concerned",
+ -10.864609718322754
+ ],
+ [
+ "test",
+ -10.864740371704102
+ ],
+ [
+ "▁designer",
+ -10.865334510803223
+ ],
+ [
+ "▁Conference",
+ -10.865593910217285
+ ],
+ [
+ "▁saving",
+ -10.866105079650879
+ ],
+ [
+ "▁recorded",
+ -10.866422653198242
+ ],
+ [
+ "▁proposed",
+ -10.866564750671387
+ ],
+ [
+ "▁ship",
+ -10.86657428741455
+ ],
+ [
+ "▁cred",
+ -10.867274284362793
+ ],
+ [
+ "▁Ci",
+ -10.867440223693848
+ ],
+ [
+ "RE",
+ -10.867619514465332
+ ],
+ [
+ "▁tradition",
+ -10.867753982543945
+ ],
+ [
+ "▁worldwide",
+ -10.867779731750488
+ ],
+ [
+ "64",
+ -10.867944717407227
+ ],
+ [
+ "▁television",
+ -10.867989540100098
+ ],
+ [
+ "▁projet",
+ -10.868102073669434
+ ],
+ [
+ "ency",
+ -10.868487358093262
+ ],
+ [
+ "▁struggle",
+ -10.868514060974121
+ ],
+ [
+ "▁twice",
+ -10.868955612182617
+ ],
+ [
+ "▁Off",
+ -10.869234085083008
+ ],
+ [
+ "▁begins",
+ -10.869577407836914
+ ],
+ [
+ "key",
+ -10.869794845581055
+ ],
+ [
+ "▁Table",
+ -10.869963645935059
+ ],
+ [
+ "▁demande",
+ -10.870177268981934
+ ],
+ [
+ "▁liquid",
+ -10.870441436767578
+ ],
+ [
+ "meter",
+ -10.870684623718262
+ ],
+ [
+ "▁2001",
+ -10.871190071105957
+ ],
+ [
+ "▁willing",
+ -10.871660232543945
+ ],
+ [
+ "▁medicine",
+ -10.871707916259766
+ ],
+ [
+ "▁expand",
+ -10.871747970581055
+ ],
+ [
+ "▁2004",
+ -10.871804237365723
+ ],
+ [
+ "▁2002",
+ -10.872016906738281
+ ],
+ [
+ "▁accord",
+ -10.872292518615723
+ ],
+ [
+ "▁Chris",
+ -10.872446060180664
+ ],
+ [
+ "▁prove",
+ -10.872543334960938
+ ],
+ [
+ "ston",
+ -10.872740745544434
+ ],
+ [
+ "mettre",
+ -10.872800827026367
+ ],
+ [
+ "▁moments",
+ -10.873537063598633
+ ],
+ [
+ "tik",
+ -10.87368392944336
+ ],
+ [
+ "such",
+ -10.874055862426758
+ ],
+ [
+ "2.",
+ -10.874431610107422
+ ],
+ [
+ "▁UN",
+ -10.874561309814453
+ ],
+ [
+ "▁jump",
+ -10.874737739562988
+ ],
+ [
+ "▁dish",
+ -10.87539291381836
+ ],
+ [
+ "▁Key",
+ -10.875663757324219
+ ],
+ [
+ "▁challenging",
+ -10.875975608825684
+ ],
+ [
+ "▁domestic",
+ -10.876410484313965
+ ],
+ [
+ "▁impressive",
+ -10.876752853393555
+ ],
+ [
+ "iger",
+ -10.877022743225098
+ ],
+ [
+ "▁Ram",
+ -10.877157211303711
+ ],
+ [
+ "▁doit",
+ -10.877263069152832
+ ],
+ [
+ "▁concrete",
+ -10.87734317779541
+ ],
+ [
+ "▁Unternehmen",
+ -10.877397537231445
+ ],
+ [
+ "▁LED",
+ -10.877429008483887
+ ],
+ [
+ "▁trouver",
+ -10.877533912658691
+ ],
+ [
+ "▁fundamental",
+ -10.877875328063965
+ ],
+ [
+ "▁implementation",
+ -10.878121376037598
+ ],
+ [
+ "85",
+ -10.878247261047363
+ ],
+ [
+ "▁hosting",
+ -10.87856388092041
+ ],
+ [
+ "▁Game",
+ -10.878691673278809
+ ],
+ [
+ "▁taught",
+ -10.878981590270996
+ ],
+ [
+ "tung",
+ -10.879016876220703
+ ],
+ [
+ "ront",
+ -10.87940502166748
+ ],
+ [
+ "▁shoes",
+ -10.879639625549316
+ ],
+ [
+ "79",
+ -10.8797607421875
+ ],
+ [
+ "▁stunning",
+ -10.879778861999512
+ ],
+ [
+ "▁Congress",
+ -10.880142211914062
+ ],
+ [
+ "▁Ent",
+ -10.880278587341309
+ ],
+ [
+ "▁Wer",
+ -10.880607604980469
+ ],
+ [
+ "▁alt",
+ -10.880608558654785
+ ],
+ [
+ "ör",
+ -10.880699157714844
+ ],
+ [
+ "▁calm",
+ -10.8808012008667
+ ],
+ [
+ "46",
+ -10.881132125854492
+ ],
+ [
+ "▁Daca",
+ -10.881404876708984
+ ],
+ [
+ "71",
+ -10.881938934326172
+ ],
+ [
+ "▁Dec",
+ -10.882392883300781
+ ],
+ [
+ "▁Fo",
+ -10.882437705993652
+ ],
+ [
+ "▁defense",
+ -10.88313102722168
+ ],
+ [
+ "▁expectations",
+ -10.883166313171387
+ ],
+ [
+ "▁Alle",
+ -10.88318920135498
+ ],
+ [
+ "▁brief",
+ -10.883691787719727
+ ],
+ [
+ "▁Hospital",
+ -10.883975982666016
+ ],
+ [
+ "▁sides",
+ -10.884121894836426
+ ],
+ [
+ "▁yellow",
+ -10.884140014648438
+ ],
+ [
+ "lei",
+ -10.88451862335205
+ ],
+ [
+ "▁speaking",
+ -10.884589195251465
+ ],
+ [
+ "▁crucial",
+ -10.885198593139648
+ ],
+ [
+ "▁Town",
+ -10.8854341506958
+ ],
+ [
+ "▁married",
+ -10.885574340820312
+ ],
+ [
+ "▁acesta",
+ -10.885583877563477
+ ],
+ [
+ "▁noted",
+ -10.885611534118652
+ ],
+ [
+ "▁Word",
+ -10.885659217834473
+ ],
+ [
+ "▁conducted",
+ -10.885963439941406
+ ],
+ [
+ "▁decor",
+ -10.886249542236328
+ ],
+ [
+ "kon",
+ -10.886565208435059
+ ],
+ [
+ "▁supplies",
+ -10.8866605758667
+ ],
+ [
+ "▁adventure",
+ -10.886691093444824
+ ],
+ [
+ "▁exhibition",
+ -10.887163162231445
+ ],
+ [
+ "heit",
+ -10.887300491333008
+ ],
+ [
+ "▁36",
+ -10.88744831085205
+ ],
+ [
+ "eria",
+ -10.887505531311035
+ ],
+ [
+ "ines",
+ -10.887551307678223
+ ],
+ [
+ "ological",
+ -10.887582778930664
+ ],
+ [
+ "quel",
+ -10.88806438446045
+ ],
+ [
+ "▁Van",
+ -10.88825511932373
+ ],
+ [
+ "-19",
+ -10.88853645324707
+ ],
+ [
+ "2,",
+ -10.888566970825195
+ ],
+ [
+ "▁Band",
+ -10.888989448547363
+ ],
+ [
+ "▁soil",
+ -10.889184951782227
+ ],
+ [
+ "▁Tim",
+ -10.889599800109863
+ ],
+ [
+ "▁NOT",
+ -10.88968563079834
+ ],
+ [
+ "▁pilot",
+ -10.889753341674805
+ ],
+ [
+ "▁Sh",
+ -10.889774322509766
+ ],
+ [
+ "Ho",
+ -10.890361785888672
+ ],
+ [
+ "CA",
+ -10.890509605407715
+ ],
+ [
+ "▁Eu",
+ -10.890745162963867
+ ],
+ [
+ "▁committee",
+ -10.890829086303711
+ ],
+ [
+ "▁Store",
+ -10.891075134277344
+ ],
+ [
+ "▁joint",
+ -10.89111614227295
+ ],
+ [
+ "▁Op",
+ -10.891315460205078
+ ],
+ [
+ "▁Jack",
+ -10.891985893249512
+ ],
+ [
+ "quality",
+ -10.89216423034668
+ ],
+ [
+ "▁Has",
+ -10.892489433288574
+ ],
+ [
+ "▁wenig",
+ -10.892507553100586
+ ],
+ [
+ "hood",
+ -10.892545700073242
+ ],
+ [
+ "▁Class",
+ -10.892582893371582
+ ],
+ [
+ "rus",
+ -10.892773628234863
+ ],
+ [
+ "▁grown",
+ -10.89294719696045
+ ],
+ [
+ "▁About",
+ -10.893518447875977
+ ],
+ [
+ "▁sum",
+ -10.893942832946777
+ ],
+ [
+ "▁Fair",
+ -10.893946647644043
+ ],
+ [
+ "SA",
+ -10.894149780273438
+ ],
+ [
+ "92",
+ -10.894185066223145
+ ],
+ [
+ "▁fourth",
+ -10.894354820251465
+ ],
+ [
+ "▁featured",
+ -10.894384384155273
+ ],
+ [
+ "▁Pen",
+ -10.89444637298584
+ ],
+ [
+ "▁natürlich",
+ -10.894885063171387
+ ],
+ [
+ "ched",
+ -10.894901275634766
+ ],
+ [
+ "▁ban",
+ -10.895112991333008
+ ],
+ [
+ "anne",
+ -10.89522647857666
+ ],
+ [
+ "▁theory",
+ -10.895413398742676
+ ],
+ [
+ "bin",
+ -10.895438194274902
+ ],
+ [
+ "iers",
+ -10.895819664001465
+ ],
+ [
+ "▁strategic",
+ -10.895903587341309
+ ],
+ [
+ "▁jours",
+ -10.895956039428711
+ ],
+ [
+ "▁communicate",
+ -10.896124839782715
+ ],
+ [
+ "▁pin",
+ -10.896320343017578
+ ],
+ [
+ "▁Bon",
+ -10.89721393585205
+ ],
+ [
+ "kom",
+ -10.897290229797363
+ ],
+ [
+ "-5",
+ -10.898177146911621
+ ],
+ [
+ "▁degrees",
+ -10.898643493652344
+ ],
+ [
+ "▁entertainment",
+ -10.899014472961426
+ ],
+ [
+ "ară",
+ -10.899248123168945
+ ],
+ [
+ "ales",
+ -10.899425506591797
+ ],
+ [
+ "▁pendant",
+ -10.89954662322998
+ ],
+ [
+ "▁Series",
+ -10.899575233459473
+ ],
+ [
+ "▁holds",
+ -10.899592399597168
+ ],
+ [
+ "▁Mini",
+ -10.899828910827637
+ ],
+ [
+ "▁Obama",
+ -10.899898529052734
+ ],
+ [
+ "▁conform",
+ -10.900163650512695
+ ],
+ [
+ "-10",
+ -10.900216102600098
+ ],
+ [
+ "▁preparation",
+ -10.9009370803833
+ ],
+ [
+ "▁autre",
+ -10.90105152130127
+ ],
+ [
+ "▁mortgage",
+ -10.901155471801758
+ ],
+ [
+ "▁Kan",
+ -10.901508331298828
+ ],
+ [
+ "▁typical",
+ -10.901538848876953
+ ],
+ [
+ "01",
+ -10.901711463928223
+ ],
+ [
+ "▁Review",
+ -10.901862144470215
+ ],
+ [
+ "▁laptop",
+ -10.902127265930176
+ ],
+ [
+ "CR",
+ -10.902610778808594
+ ],
+ [
+ "▁thread",
+ -10.90265941619873
+ ],
+ [
+ "BS",
+ -10.902661323547363
+ ],
+ [
+ "▁upper",
+ -10.902700424194336
+ ],
+ [
+ "▁searching",
+ -10.902932167053223
+ ],
+ [
+ "▁pen",
+ -10.903214454650879
+ ],
+ [
+ "▁Middle",
+ -10.90333080291748
+ ],
+ [
+ "73",
+ -10.903359413146973
+ ],
+ [
+ "▁leg",
+ -10.903650283813477
+ ],
+ [
+ "onic",
+ -10.904272079467773
+ ],
+ [
+ "IS",
+ -10.904356956481934
+ ],
+ [
+ "▁Kar",
+ -10.904623985290527
+ ],
+ [
+ "anz",
+ -10.9046630859375
+ ],
+ [
+ "▁circuit",
+ -10.904901504516602
+ ],
+ [
+ "▁Casino",
+ -10.905384063720703
+ ],
+ [
+ "07",
+ -10.90584659576416
+ ],
+ [
+ "▁petit",
+ -10.905906677246094
+ ],
+ [
+ "TV",
+ -10.905978202819824
+ ],
+ [
+ "level",
+ -10.906311988830566
+ ],
+ [
+ "▁Point",
+ -10.906312942504883
+ ],
+ [
+ "rau",
+ -10.906474113464355
+ ],
+ [
+ "▁cabinet",
+ -10.906991958618164
+ ],
+ [
+ "▁failed",
+ -10.907042503356934
+ ],
+ [
+ "▁stated",
+ -10.907126426696777
+ ],
+ [
+ "LA",
+ -10.907461166381836
+ ],
+ [
+ "▁privacy",
+ -10.907596588134766
+ ],
+ [
+ "vol",
+ -10.907901763916016
+ ],
+ [
+ "ativ",
+ -10.908151626586914
+ ],
+ [
+ "▁matters",
+ -10.908210754394531
+ ],
+ [
+ "▁Mor",
+ -10.908555030822754
+ ],
+ [
+ "▁Ur",
+ -10.90860652923584
+ ],
+ [
+ "view",
+ -10.908968925476074
+ ],
+ [
+ "▁consultation",
+ -10.90921688079834
+ ],
+ [
+ "TS",
+ -10.909296989440918
+ ],
+ [
+ "▁apartment",
+ -10.909412384033203
+ ],
+ [
+ "▁integrated",
+ -10.909425735473633
+ ],
+ [
+ "74",
+ -10.909669876098633
+ ],
+ [
+ "▁Through",
+ -10.909710884094238
+ ],
+ [
+ "▁kick",
+ -10.909798622131348
+ ],
+ [
+ "▁perioada",
+ -10.90993881225586
+ ],
+ [
+ "▁entirely",
+ -10.909953117370605
+ ],
+ [
+ "▁impossible",
+ -10.91015911102295
+ ],
+ [
+ "▁consideration",
+ -10.910268783569336
+ ],
+ [
+ "▁Alt",
+ -10.91054916381836
+ ],
+ [
+ "▁Come",
+ -10.911089897155762
+ ],
+ [
+ "▁outstanding",
+ -10.911276817321777
+ ],
+ [
+ "83",
+ -10.911727905273438
+ ],
+ [
+ "▁prezent",
+ -10.911859512329102
+ ],
+ [
+ "▁Local",
+ -10.911993980407715
+ ],
+ [
+ "▁Camp",
+ -10.912056922912598
+ ],
+ [
+ "▁bear",
+ -10.912067413330078
+ ],
+ [
+ "enden",
+ -10.912262916564941
+ ],
+ [
+ "life",
+ -10.91236686706543
+ ],
+ [
+ "▁Haus",
+ -10.912516593933105
+ ],
+ [
+ "▁William",
+ -10.912644386291504
+ ],
+ [
+ "“,",
+ -10.912665367126465
+ ],
+ [
+ "▁Instagram",
+ -10.91285514831543
+ ],
+ [
+ "▁solve",
+ -10.913195610046387
+ ],
+ [
+ "▁Ze",
+ -10.913431167602539
+ ],
+ [
+ "▁everyday",
+ -10.91357135772705
+ ],
+ [
+ "bla",
+ -10.913615226745605
+ ],
+ [
+ "eng",
+ -10.913662910461426
+ ],
+ [
+ "ough",
+ -10.914246559143066
+ ],
+ [
+ "84",
+ -10.914483070373535
+ ],
+ [
+ "?\"",
+ -10.914599418640137
+ ],
+ [
+ "rely",
+ -10.91476821899414
+ ],
+ [
+ "TH",
+ -10.914841651916504
+ ],
+ [
+ "lang",
+ -10.91511058807373
+ ],
+ [
+ "82",
+ -10.915817260742188
+ ],
+ [
+ "▁removal",
+ -10.91589641571045
+ ],
+ [
+ "ală",
+ -10.915956497192383
+ ],
+ [
+ "▁circumstances",
+ -10.916097640991211
+ ],
+ [
+ "ente",
+ -10.91622257232666
+ ],
+ [
+ "▁lieu",
+ -10.91645336151123
+ ],
+ [
+ "▁2016.",
+ -10.91710376739502
+ ],
+ [
+ "▁ales",
+ -10.917342185974121
+ ],
+ [
+ "▁pure",
+ -10.917482376098633
+ ],
+ [
+ "▁choosing",
+ -10.917590141296387
+ ],
+ [
+ "▁Russia",
+ -10.917698860168457
+ ],
+ [
+ "amp",
+ -10.917703628540039
+ ],
+ [
+ "▁Santa",
+ -10.91788387298584
+ ],
+ [
+ "▁happening",
+ -10.918203353881836
+ ],
+ [
+ "▁crew",
+ -10.91822338104248
+ ],
+ [
+ "▁lei",
+ -10.91855239868164
+ ],
+ [
+ "IP",
+ -10.91858196258545
+ ],
+ [
+ "RO",
+ -10.919425964355469
+ ],
+ [
+ "▁resort",
+ -10.919514656066895
+ ],
+ [
+ "ened",
+ -10.919689178466797
+ ],
+ [
+ "MB",
+ -10.920031547546387
+ ],
+ [
+ "▁styles",
+ -10.920052528381348
+ ],
+ [
+ "▁dernier",
+ -10.920533180236816
+ ],
+ [
+ "uck",
+ -10.920699119567871
+ ],
+ [
+ "▁Guide",
+ -10.920710563659668
+ ],
+ [
+ "fic",
+ -10.92096996307373
+ ],
+ [
+ "▁fitness",
+ -10.921977996826172
+ ],
+ [
+ "▁healthcare",
+ -10.92223072052002
+ ],
+ [
+ "mol",
+ -10.92237663269043
+ ],
+ [
+ "▁vis",
+ -10.922721862792969
+ ],
+ [
+ "▁atmosphere",
+ -10.922972679138184
+ ],
+ [
+ "▁motion",
+ -10.922989845275879
+ ],
+ [
+ "▁closer",
+ -10.923114776611328
+ ],
+ [
+ "▁SA",
+ -10.92335319519043
+ ],
+ [
+ "▁default",
+ -10.923371315002441
+ ],
+ [
+ "▁architecture",
+ -10.923471450805664
+ ],
+ [
+ "iile",
+ -10.923528671264648
+ ],
+ [
+ "zel",
+ -10.923675537109375
+ ],
+ [
+ "cla",
+ -10.92387866973877
+ ],
+ [
+ "OP",
+ -10.924382209777832
+ ],
+ [
+ "▁west",
+ -10.924965858459473
+ ],
+ [
+ "▁Energy",
+ -10.925613403320312
+ ],
+ [
+ "▁positions",
+ -10.925777435302734
+ ],
+ [
+ "▁contrast",
+ -10.925885200500488
+ ],
+ [
+ "▁serves",
+ -10.92605972290039
+ ],
+ [
+ "cup",
+ -10.926340103149414
+ ],
+ [
+ "▁rose",
+ -10.926485061645508
+ ],
+ [
+ "pers",
+ -10.92664623260498
+ ],
+ [
+ "▁noise",
+ -10.926846504211426
+ ],
+ [
+ "mont",
+ -10.92690658569336
+ ],
+ [
+ "#",
+ -10.927061080932617
+ ],
+ [
+ "lies",
+ -10.927326202392578
+ ],
+ [
+ "pat",
+ -10.927718162536621
+ ],
+ [
+ "IC",
+ -10.927956581115723
+ ],
+ [
+ "arc",
+ -10.927989959716797
+ ],
+ [
+ "▁winner",
+ -10.928524017333984
+ ],
+ [
+ "tent",
+ -10.928732872009277
+ ],
+ [
+ "▁Preis",
+ -10.929106712341309
+ ],
+ [
+ "▁vin",
+ -10.929254531860352
+ ],
+ [
+ "blo",
+ -10.92929458618164
+ ],
+ [
+ "ție",
+ -10.929520606994629
+ ],
+ [
+ "▁OR",
+ -10.930315017700195
+ ],
+ [
+ "▁Buch",
+ -10.930798530578613
+ ],
+ [
+ "▁nearby",
+ -10.931190490722656
+ ],
+ [
+ "▁meetings",
+ -10.931290626525879
+ ],
+ [
+ "▁48",
+ -10.931465148925781
+ ],
+ [
+ "▁quand",
+ -10.93152904510498
+ ],
+ [
+ "▁usual",
+ -10.931936264038086
+ ],
+ [
+ "▁weitere",
+ -10.932539939880371
+ ],
+ [
+ "▁caught",
+ -10.932571411132812
+ ],
+ [
+ "▁issued",
+ -10.932626724243164
+ ],
+ [
+ "ști",
+ -10.932896614074707
+ ],
+ [
+ "upcoming",
+ -10.933232307434082
+ ],
+ [
+ "▁agreed",
+ -10.933233261108398
+ ],
+ [
+ "place",
+ -10.933353424072266
+ ],
+ [
+ "▁Brand",
+ -10.93344497680664
+ ],
+ [
+ "▁relation",
+ -10.933969497680664
+ ],
+ [
+ "▁atât",
+ -10.934090614318848
+ ],
+ [
+ "▁Tre",
+ -10.934176445007324
+ ],
+ [
+ "▁lors",
+ -10.934438705444336
+ ],
+ [
+ "▁adopt",
+ -10.934452056884766
+ ],
+ [
+ "▁celui",
+ -10.93458366394043
+ ],
+ [
+ "cken",
+ -10.93505859375
+ ],
+ [
+ "▁partnership",
+ -10.935284614562988
+ ],
+ [
+ "?”",
+ -10.935376167297363
+ ],
+ [
+ "▁ba",
+ -10.935746192932129
+ ],
+ [
+ "▁ID",
+ -10.935832023620605
+ ],
+ [
+ "▁consistent",
+ -10.935835838317871
+ ],
+ [
+ "▁Ya",
+ -10.935941696166992
+ ],
+ [
+ "▁Academy",
+ -10.936182022094727
+ ],
+ [
+ "cial",
+ -10.936230659484863
+ ],
+ [
+ "1%",
+ -10.936366081237793
+ ],
+ [
+ "▁mise",
+ -10.936684608459473
+ ],
+ [
+ "▁gute",
+ -10.936728477478027
+ ],
+ [
+ "gli",
+ -10.936939239501953
+ ],
+ [
+ "▁Bu",
+ -10.937679290771484
+ ],
+ [
+ "▁reduction",
+ -10.937917709350586
+ ],
+ [
+ "acy",
+ -10.938126564025879
+ ],
+ [
+ "aga",
+ -10.938161849975586
+ ],
+ [
+ "▁Sc",
+ -10.938273429870605
+ ],
+ [
+ "▁Informationen",
+ -10.938308715820312
+ ],
+ [
+ "▁kommen",
+ -10.938352584838867
+ ],
+ [
+ "press",
+ -10.93837833404541
+ ],
+ [
+ "▁bridge",
+ -10.938379287719727
+ ],
+ [
+ "▁qualified",
+ -10.938671112060547
+ ],
+ [
+ "position",
+ -10.938821792602539
+ ],
+ [
+ "▁combat",
+ -10.938933372497559
+ ],
+ [
+ "!\"",
+ -10.938993453979492
+ ],
+ [
+ "eva",
+ -10.939217567443848
+ ],
+ [
+ "oase",
+ -10.939380645751953
+ ],
+ [
+ "▁inner",
+ -10.939410209655762
+ ],
+ [
+ "▁loans",
+ -10.939720153808594
+ ],
+ [
+ "made",
+ -10.939786911010742
+ ],
+ [
+ "▁Mexico",
+ -10.93993091583252
+ ],
+ [
+ "▁formal",
+ -10.940092086791992
+ ],
+ [
+ "▁fell",
+ -10.94021987915039
+ ],
+ [
+ "91",
+ -10.940524101257324
+ ],
+ [
+ "▁campus",
+ -10.9407320022583
+ ],
+ [
+ "ienne",
+ -10.940869331359863
+ ],
+ [
+ "▁framework",
+ -10.94105339050293
+ ],
+ [
+ "ncing",
+ -10.941157341003418
+ ],
+ [
+ "▁Para",
+ -10.941222190856934
+ ],
+ [
+ "▁password",
+ -10.941298484802246
+ ],
+ [
+ "▁sei",
+ -10.941422462463379
+ ],
+ [
+ "▁Cross",
+ -10.941532135009766
+ ],
+ [
+ "▁Ten",
+ -10.941873550415039
+ ],
+ [
+ "bank",
+ -10.941887855529785
+ ],
+ [
+ "▁gun",
+ -10.942000389099121
+ ],
+ [
+ "ient",
+ -10.942021369934082
+ ],
+ [
+ "▁usage",
+ -10.942176818847656
+ ],
+ [
+ "▁(2",
+ -10.942278861999512
+ ],
+ [
+ "Gra",
+ -10.942320823669434
+ ],
+ [
+ "▁prea",
+ -10.94253158569336
+ ],
+ [
+ "▁Als",
+ -10.942619323730469
+ ],
+ [
+ "▁finance",
+ -10.942638397216797
+ ],
+ [
+ "tate",
+ -10.942665100097656
+ ],
+ [
+ "ition",
+ -10.942703247070312
+ ],
+ [
+ "▁regulations",
+ -10.942741394042969
+ ],
+ [
+ "▁Professional",
+ -10.943001747131348
+ ],
+ [
+ "▁pl",
+ -10.94336986541748
+ ],
+ [
+ "▁SEO",
+ -10.943472862243652
+ ],
+ [
+ "▁trecut",
+ -10.943487167358398
+ ],
+ [
+ "▁aller",
+ -10.943509101867676
+ ],
+ [
+ "▁violence",
+ -10.943986892700195
+ ],
+ [
+ "▁membership",
+ -10.944117546081543
+ ],
+ [
+ "▁picked",
+ -10.944162368774414
+ ],
+ [
+ "▁collected",
+ -10.9443359375
+ ],
+ [
+ "▁extended",
+ -10.944449424743652
+ ],
+ [
+ "▁religious",
+ -10.944661140441895
+ ],
+ [
+ "▁salle",
+ -10.944767951965332
+ ],
+ [
+ "RA",
+ -10.944781303405762
+ ],
+ [
+ "▁blend",
+ -10.945232391357422
+ ],
+ [
+ "▁Min",
+ -10.94532299041748
+ ],
+ [
+ "kal",
+ -10.945887565612793
+ ],
+ [
+ "▁featuring",
+ -10.945902824401855
+ ],
+ [
+ "▁researchers",
+ -10.946263313293457
+ ],
+ [
+ "▁Search",
+ -10.946558952331543
+ ],
+ [
+ "CE",
+ -10.946675300598145
+ ],
+ [
+ "▁recognized",
+ -10.94682502746582
+ ],
+ [
+ "▁semi",
+ -10.94692611694336
+ ],
+ [
+ "▁exposure",
+ -10.94718074798584
+ ],
+ [
+ "grew",
+ -10.947466850280762
+ ],
+ [
+ "▁candidate",
+ -10.948250770568848
+ ],
+ [
+ "▁shares",
+ -10.948908805847168
+ ],
+ [
+ "▁edit",
+ -10.949745178222656
+ ],
+ [
+ "CS",
+ -10.949905395507812
+ ],
+ [
+ "▁Cl",
+ -10.950240135192871
+ ],
+ [
+ "▁Enjoy",
+ -10.951438903808594
+ ],
+ [
+ "▁hurt",
+ -10.951482772827148
+ ],
+ [
+ "▁bottle",
+ -10.951593399047852
+ ],
+ [
+ "▁Buy",
+ -10.95159912109375
+ ],
+ [
+ "▁superior",
+ -10.952286720275879
+ ],
+ [
+ "▁missed",
+ -10.952424049377441
+ ],
+ [
+ "▁workshop",
+ -10.952433586120605
+ ],
+ [
+ "action",
+ -10.952437400817871
+ ],
+ [
+ "ple",
+ -10.952699661254883
+ ],
+ [
+ "▁Schul",
+ -10.952814102172852
+ ],
+ [
+ "▁houses",
+ -10.953080177307129
+ ],
+ [
+ "▁2017,",
+ -10.953569412231445
+ ],
+ [
+ "▁killed",
+ -10.953750610351562
+ ],
+ [
+ "▁calendar",
+ -10.954306602478027
+ ],
+ [
+ "▁Mike",
+ -10.954597473144531
+ ],
+ [
+ "FA",
+ -10.954627990722656
+ ],
+ [
+ "nut",
+ -10.95487117767334
+ ],
+ [
+ "▁establish",
+ -10.955140113830566
+ ],
+ [
+ "▁alcohol",
+ -10.95514965057373
+ ],
+ [
+ "▁closely",
+ -10.955170631408691
+ ],
+ [
+ "▁MA",
+ -10.955381393432617
+ ],
+ [
+ "pul",
+ -10.955389022827148
+ ],
+ [
+ "▁defined",
+ -10.955666542053223
+ ],
+ [
+ "aires",
+ -10.955692291259766
+ ],
+ [
+ "▁Shi",
+ -10.955703735351562
+ ],
+ [
+ "▁plays",
+ -10.956303596496582
+ ],
+ [
+ "▁sister",
+ -10.95690631866455
+ ],
+ [
+ "▁cable",
+ -10.957179069519043
+ ],
+ [
+ "▁desk",
+ -10.957215309143066
+ ],
+ [
+ "▁apoi",
+ -10.957738876342773
+ ],
+ [
+ "▁identity",
+ -10.95785140991211
+ ],
+ [
+ "▁stars",
+ -10.957931518554688
+ ],
+ [
+ "▁fata",
+ -10.958008766174316
+ ],
+ [
+ "▁obvious",
+ -10.958330154418945
+ ],
+ [
+ "▁dental",
+ -10.95843505859375
+ ],
+ [
+ "AM",
+ -10.958802223205566
+ ],
+ [
+ "▁sharp",
+ -10.95881175994873
+ ],
+ [
+ "duc",
+ -10.959053993225098
+ ],
+ [
+ "▁manufacturer",
+ -10.95914077758789
+ ],
+ [
+ "!)",
+ -10.959270477294922
+ ],
+ [
+ "▁objects",
+ -10.959720611572266
+ ],
+ [
+ "▁Ag",
+ -10.959989547729492
+ ],
+ [
+ "referred",
+ -10.960195541381836
+ ],
+ [
+ "▁Ak",
+ -10.960308074951172
+ ],
+ [
+ "burg",
+ -10.960360527038574
+ ],
+ [
+ "▁nouveau",
+ -10.960854530334473
+ ],
+ [
+ "▁Pal",
+ -10.960994720458984
+ ],
+ [
+ "▁Arbeits",
+ -10.961280822753906
+ ],
+ [
+ "▁personally",
+ -10.961288452148438
+ ],
+ [
+ "▁Dé",
+ -10.961292266845703
+ ],
+ [
+ "▁import",
+ -10.961688041687012
+ ],
+ [
+ "▁justice",
+ -10.961913108825684
+ ],
+ [
+ "▁photography",
+ -10.962705612182617
+ ],
+ [
+ "▁portfolio",
+ -10.962841987609863
+ ],
+ [
+ "56",
+ -10.96314525604248
+ ],
+ [
+ "▁nouvelle",
+ -10.963293075561523
+ ],
+ [
+ "▁oven",
+ -10.964197158813477
+ ],
+ [
+ "▁400",
+ -10.964272499084473
+ ],
+ [
+ "▁mixed",
+ -10.964395523071289
+ ],
+ [
+ "▁relax",
+ -10.964427947998047
+ ],
+ [
+ "▁imp",
+ -10.964703559875488
+ ],
+ [
+ "▁».",
+ -10.964734077453613
+ ],
+ [
+ "▁mail",
+ -10.964777946472168
+ ],
+ [
+ "rage",
+ -10.964861869812012
+ ],
+ [
+ "nos",
+ -10.964974403381348
+ ],
+ [
+ "▁drugs",
+ -10.965195655822754
+ ],
+ [
+ "▁jede",
+ -10.965211868286133
+ ],
+ [
+ "▁einige",
+ -10.965232849121094
+ ],
+ [
+ "▁8.",
+ -10.965325355529785
+ ],
+ [
+ "ters",
+ -10.965412139892578
+ ],
+ [
+ "▁electrical",
+ -10.965432167053223
+ ],
+ [
+ "▁puis",
+ -10.965836524963379
+ ],
+ [
+ "▁films",
+ -10.965903282165527
+ ],
+ [
+ "41",
+ -10.966036796569824
+ ],
+ [
+ "▁moral",
+ -10.966398239135742
+ ],
+ [
+ "lage",
+ -10.966402053833008
+ ],
+ [
+ "▁spaces",
+ -10.966415405273438
+ ],
+ [
+ "▁Ed",
+ -10.966462135314941
+ ],
+ [
+ "▁classroom",
+ -10.966588020324707
+ ],
+ [
+ "▁große",
+ -10.966588973999023
+ ],
+ [
+ "▁baza",
+ -10.966887474060059
+ ],
+ [
+ "face",
+ -10.967308044433594
+ ],
+ [
+ "▁informed",
+ -10.967333793640137
+ ],
+ [
+ "▁improving",
+ -10.967477798461914
+ ],
+ [
+ "▁guidance",
+ -10.967880249023438
+ ],
+ [
+ "▁gallery",
+ -10.96800708770752
+ ],
+ [
+ "cular",
+ -10.968046188354492
+ ],
+ [
+ "53",
+ -10.968094825744629
+ ],
+ [
+ "Despite",
+ -10.968238830566406
+ ],
+ [
+ "▁forme",
+ -10.968304634094238
+ ],
+ [
+ "▁système",
+ -10.968415260314941
+ ],
+ [
+ "▁Win",
+ -10.968494415283203
+ ],
+ [
+ "▁Small",
+ -10.968537330627441
+ ],
+ [
+ "▁Mobile",
+ -10.968564987182617
+ ],
+ [
+ "▁tape",
+ -10.968606948852539
+ ],
+ [
+ "▁erhalten",
+ -10.968914985656738
+ ],
+ [
+ "▁movies",
+ -10.968928337097168
+ ],
+ [
+ "▁Unfortunately",
+ -10.968963623046875
+ ],
+ [
+ "▁Looking",
+ -10.96945858001709
+ ],
+ [
+ "▁guard",
+ -10.969584465026855
+ ],
+ [
+ "▁pr",
+ -10.969820976257324
+ ],
+ [
+ "▁confident",
+ -10.96988582611084
+ ],
+ [
+ "BA",
+ -10.970229148864746
+ ],
+ [
+ "bas",
+ -10.970272064208984
+ ],
+ [
+ "hum",
+ -10.97050666809082
+ ],
+ [
+ "ular",
+ -10.9705171585083
+ ],
+ [
+ "▁Still",
+ -10.970593452453613
+ ],
+ [
+ "▁flavor",
+ -10.970656394958496
+ ],
+ [
+ "▁boost",
+ -10.970773696899414
+ ],
+ [
+ "▁division",
+ -10.970842361450195
+ ],
+ [
+ "ising",
+ -10.971006393432617
+ ],
+ [
+ "▁monitoring",
+ -10.971044540405273
+ ],
+ [
+ "▁Sen",
+ -10.97105884552002
+ ],
+ [
+ "▁https",
+ -10.971527099609375
+ ],
+ [
+ "mainly",
+ -10.971735000610352
+ ],
+ [
+ "play",
+ -10.972251892089844
+ ],
+ [
+ "▁dynamic",
+ -10.972357749938965
+ ],
+ [
+ "▁coup",
+ -10.972370147705078
+ ],
+ [
+ "▁carpet",
+ -10.972561836242676
+ ],
+ [
+ "iner",
+ -10.972846984863281
+ ],
+ [
+ "ral",
+ -10.97325611114502
+ ],
+ [
+ "iser",
+ -10.973320007324219
+ ],
+ [
+ "RC",
+ -10.9739990234375
+ ],
+ [
+ "▁definition",
+ -10.97475814819336
+ ],
+ [
+ "▁Za",
+ -10.974767684936523
+ ],
+ [
+ "friendly",
+ -10.974883079528809
+ ],
+ [
+ "43",
+ -10.975123405456543
+ ],
+ [
+ "link",
+ -10.975180625915527
+ ],
+ [
+ "▁Multi",
+ -10.97519302368164
+ ],
+ [
+ "▁einmal",
+ -10.975272178649902
+ ],
+ [
+ "▁stopped",
+ -10.975394248962402
+ ],
+ [
+ "vel",
+ -10.975456237792969
+ ],
+ [
+ "▁ongoing",
+ -10.975565910339355
+ ],
+ [
+ "▁ancient",
+ -10.976259231567383
+ ],
+ [
+ "take",
+ -10.976301193237305
+ ],
+ [
+ "cia",
+ -10.976432800292969
+ ],
+ [
+ "▁USB",
+ -10.976545333862305
+ ],
+ [
+ "▁attorney",
+ -10.976866722106934
+ ],
+ [
+ "▁slot",
+ -10.976866722106934
+ ],
+ [
+ "▁Line",
+ -10.97693157196045
+ ],
+ [
+ "rice",
+ -10.977087020874023
+ ],
+ [
+ "ify",
+ -10.977520942687988
+ ],
+ [
+ "ó",
+ -10.978260040283203
+ ],
+ [
+ "▁flash",
+ -10.978483200073242
+ ],
+ [
+ "▁extension",
+ -10.978555679321289
+ ],
+ [
+ "▁Ende",
+ -10.979022979736328
+ ],
+ [
+ "▁powder",
+ -10.979114532470703
+ ],
+ [
+ "ească",
+ -10.979143142700195
+ ],
+ [
+ "03",
+ -10.979327201843262
+ ],
+ [
+ "▁normally",
+ -10.979416847229004
+ ],
+ [
+ "▁pun",
+ -10.980108261108398
+ ],
+ [
+ "viewed",
+ -10.980138778686523
+ ],
+ [
+ "ssen",
+ -10.980896949768066
+ ],
+ [
+ "ache",
+ -10.981121063232422
+ ],
+ [
+ "ește",
+ -10.98122787475586
+ ],
+ [
+ "▁PA",
+ -10.981266021728516
+ ],
+ [
+ "FI",
+ -10.981945991516113
+ ],
+ [
+ "▁Frank",
+ -10.98198127746582
+ ],
+ [
+ "▁apa",
+ -10.98242473602295
+ ],
+ [
+ "▁coast",
+ -10.982614517211914
+ ],
+ [
+ "▁boy",
+ -10.982665061950684
+ ],
+ [
+ "lim",
+ -10.982902526855469
+ ],
+ [
+ "▁putin",
+ -10.983194351196289
+ ],
+ [
+ "▁script",
+ -10.983332633972168
+ ],
+ [
+ "▁noticed",
+ -10.9837007522583
+ ],
+ [
+ "▁dealing",
+ -10.983922004699707
+ ],
+ [
+ "▁Trans",
+ -10.984100341796875
+ ],
+ [
+ "▁border",
+ -10.984447479248047
+ ],
+ [
+ "▁reputation",
+ -10.984657287597656
+ ],
+ [
+ "-2",
+ -10.984662055969238
+ ],
+ [
+ "HS",
+ -10.984707832336426
+ ],
+ [
+ "▁supports",
+ -10.984724998474121
+ ],
+ [
+ "▁horse",
+ -10.985146522521973
+ ],
+ [
+ "nik",
+ -10.98520565032959
+ ],
+ [
+ "▁clothes",
+ -10.985234260559082
+ ],
+ [
+ "▁Card",
+ -10.985612869262695
+ ],
+ [
+ "▁relief",
+ -10.98595905303955
+ ],
+ [
+ "▁Visit",
+ -10.986259460449219
+ ],
+ [
+ "▁luni",
+ -10.986593246459961
+ ],
+ [
+ "81",
+ -10.986693382263184
+ ],
+ [
+ "qua",
+ -10.986945152282715
+ ],
+ [
+ "▁Comp",
+ -10.98697280883789
+ ],
+ [
+ "▁investigation",
+ -10.987137794494629
+ ],
+ [
+ "▁depth",
+ -10.987598419189453
+ ],
+ [
+ "▁earned",
+ -10.987709045410156
+ ],
+ [
+ "▁Ren",
+ -10.988090515136719
+ ],
+ [
+ "▁Dumnezeu",
+ -10.988107681274414
+ ],
+ [
+ "▁Joe",
+ -10.988210678100586
+ ],
+ [
+ "▁goods",
+ -10.988288879394531
+ ],
+ [
+ "▁Vol",
+ -10.988686561584473
+ ],
+ [
+ "▁certified",
+ -10.989118576049805
+ ],
+ [
+ "▁favor",
+ -10.989326477050781
+ ],
+ [
+ "▁Scott",
+ -10.989599227905273
+ ],
+ [
+ "▁protest",
+ -10.989802360534668
+ ],
+ [
+ "▁pace",
+ -10.989803314208984
+ ],
+ [
+ "▁Angeles",
+ -10.990368843078613
+ ],
+ [
+ "inch",
+ -10.99050521850586
+ ],
+ [
+ "▁charged",
+ -10.99052619934082
+ ],
+ [
+ "code",
+ -10.990968704223633
+ ],
+ [
+ "▁convenient",
+ -10.99138355255127
+ ],
+ [
+ "▁Nord",
+ -10.991556167602539
+ ],
+ [
+ "▁yesterday",
+ -10.991691589355469
+ ],
+ [
+ "Dacă",
+ -10.99169635772705
+ ],
+ [
+ "▁Travel",
+ -10.991786003112793
+ ],
+ [
+ "▁kid",
+ -10.991941452026367
+ ],
+ [
+ "ction",
+ -10.991986274719238
+ ],
+ [
+ "▁groupe",
+ -10.992770195007324
+ ],
+ [
+ "pu",
+ -10.993056297302246
+ ],
+ [
+ "bzw",
+ -10.993196487426758
+ ],
+ [
+ "▁mixture",
+ -10.993513107299805
+ ],
+ [
+ "▁Farm",
+ -10.993715286254883
+ ],
+ [
+ "▁acces",
+ -10.993939399719238
+ ],
+ [
+ "matic",
+ -10.993950843811035
+ ],
+ [
+ "▁comparison",
+ -10.994006156921387
+ ],
+ [
+ "reich",
+ -10.994095802307129
+ ],
+ [
+ "pet",
+ -10.994502067565918
+ ],
+ [
+ "▁lit",
+ -10.994685173034668
+ ],
+ [
+ "▁organized",
+ -10.99476432800293
+ ],
+ [
+ "just",
+ -10.995564460754395
+ ],
+ [
+ "▁fellow",
+ -10.996004104614258
+ ],
+ [
+ "Ver",
+ -10.996209144592285
+ ],
+ [
+ "▁trends",
+ -10.99622631072998
+ ],
+ [
+ "▁evaluation",
+ -10.99626636505127
+ ],
+ [
+ "feld",
+ -10.99639892578125
+ ],
+ [
+ "▁Pu",
+ -10.99671459197998
+ ],
+ [
+ "▁equipped",
+ -10.99727725982666
+ ],
+ [
+ "▁catre",
+ -10.997278213500977
+ ],
+ [
+ "eck",
+ -10.997369766235352
+ ],
+ [
+ "▁facing",
+ -10.997998237609863
+ ],
+ [
+ "▁instrument",
+ -10.998361587524414
+ ],
+ [
+ "▁pleased",
+ -10.998507499694824
+ ],
+ [
+ "▁tap",
+ -10.998818397521973
+ ],
+ [
+ "dom",
+ -10.998826026916504
+ ],
+ [
+ "▁pump",
+ -10.999384880065918
+ ],
+ [
+ "▁functional",
+ -10.999429702758789
+ ],
+ [
+ "▁authority",
+ -10.999455451965332
+ ],
+ [
+ "▁experiment",
+ -10.999478340148926
+ ],
+ [
+ "LO",
+ -10.999529838562012
+ ],
+ [
+ "▁scheduled",
+ -10.999552726745605
+ ],
+ [
+ "halt",
+ -10.999604225158691
+ ],
+ [
+ "▁ceiling",
+ -10.999761581420898
+ ],
+ [
+ "▁Step",
+ -11.000310897827148
+ ],
+ [
+ "▁orders",
+ -11.00032901763916
+ ],
+ [
+ "▁speech",
+ -11.001046180725098
+ ],
+ [
+ "▁stands",
+ -11.001119613647461
+ ],
+ [
+ "▁disc",
+ -11.001920700073242
+ ],
+ [
+ "▁rec",
+ -11.001935958862305
+ ],
+ [
+ "▁Text",
+ -11.00243854522705
+ ],
+ [
+ "▁banks",
+ -11.00294017791748
+ ],
+ [
+ "▁oameni",
+ -11.003045082092285
+ ],
+ [
+ "▁communications",
+ -11.003194808959961
+ ],
+ [
+ "trag",
+ -11.003307342529297
+ ],
+ [
+ "▁trail",
+ -11.003803253173828
+ ],
+ [
+ "AN",
+ -11.00426197052002
+ ],
+ [
+ "▁Federal",
+ -11.004467964172363
+ ],
+ [
+ "▁quote",
+ -11.00455093383789
+ ],
+ [
+ "▁spus",
+ -11.004620552062988
+ ],
+ [
+ "▁managing",
+ -11.004990577697754
+ ],
+ [
+ "▁booking",
+ -11.00505256652832
+ ],
+ [
+ "▁Blog",
+ -11.005669593811035
+ ],
+ [
+ "▁tank",
+ -11.005681991577148
+ ],
+ [
+ "pon",
+ -11.005804061889648
+ ],
+ [
+ "GE",
+ -11.00582218170166
+ ],
+ [
+ "▁fiscal",
+ -11.005871772766113
+ ],
+ [
+ "▁satisfaction",
+ -11.006044387817383
+ ],
+ [
+ "cre",
+ -11.00614070892334
+ ],
+ [
+ "▁protected",
+ -11.006494522094727
+ ],
+ [
+ "▁enfants",
+ -11.006782531738281
+ ],
+ [
+ "▁dort",
+ -11.007554054260254
+ ],
+ [
+ "▁Mel",
+ -11.008041381835938
+ ],
+ [
+ "▁turns",
+ -11.00804615020752
+ ],
+ [
+ "▁savings",
+ -11.008106231689453
+ ],
+ [
+ "▁voir",
+ -11.008358001708984
+ ],
+ [
+ "▁Boston",
+ -11.008394241333008
+ ],
+ [
+ "▁debate",
+ -11.008469581604004
+ ],
+ [
+ "▁SO",
+ -11.008857727050781
+ ],
+ [
+ "▁tables",
+ -11.009193420410156
+ ],
+ [
+ "▁honest",
+ -11.009210586547852
+ ],
+ [
+ "mate",
+ -11.009283065795898
+ ],
+ [
+ "▁chart",
+ -11.0094633102417
+ ],
+ [
+ "decât",
+ -11.009682655334473
+ ],
+ [
+ "▁Radio",
+ -11.009685516357422
+ ],
+ [
+ "54",
+ -11.00986385345459
+ ],
+ [
+ "▁vol",
+ -11.010008811950684
+ ],
+ [
+ "last",
+ -11.010148048400879
+ ],
+ [
+ "▁tall",
+ -11.010408401489258
+ ],
+ [
+ "▁Should",
+ -11.010489463806152
+ ],
+ [
+ "▁sink",
+ -11.010525703430176
+ ],
+ [
+ "▁Right",
+ -11.010527610778809
+ ],
+ [
+ "▁male",
+ -11.010720252990723
+ ],
+ [
+ "▁Modern",
+ -11.010753631591797
+ ],
+ [
+ "▁indeed",
+ -11.010886192321777
+ ],
+ [
+ "▁Garden",
+ -11.011139869689941
+ ],
+ [
+ "▁Mod",
+ -11.011307716369629
+ ],
+ [
+ "▁turning",
+ -11.0115327835083
+ ],
+ [
+ "▁inches",
+ -11.011557579040527
+ ],
+ [
+ "▁Police",
+ -11.01183795928955
+ ],
+ [
+ "▁Pay",
+ -11.012016296386719
+ ],
+ [
+ "UE",
+ -11.0126371383667
+ ],
+ [
+ "mé",
+ -11.012652397155762
+ ],
+ [
+ "EE",
+ -11.013046264648438
+ ],
+ [
+ "▁cookies",
+ -11.013116836547852
+ ],
+ [
+ "rip",
+ -11.013351440429688
+ ],
+ [
+ "▁Motor",
+ -11.01352310180664
+ ],
+ [
+ "▁lung",
+ -11.01379680633545
+ ],
+ [
+ "▁Ap",
+ -11.013995170593262
+ ],
+ [
+ "▁sustainable",
+ -11.014066696166992
+ ],
+ [
+ "▁instant",
+ -11.014240264892578
+ ],
+ [
+ "▁Rose",
+ -11.014464378356934
+ ],
+ [
+ "▁Carolina",
+ -11.014906883239746
+ ],
+ [
+ "▁Help",
+ -11.014969825744629
+ ],
+ [
+ "IE",
+ -11.01535701751709
+ ],
+ [
+ "▁Jersey",
+ -11.015522956848145
+ ],
+ [
+ "▁Spanish",
+ -11.015586853027344
+ ],
+ [
+ "▁wheel",
+ -11.015660285949707
+ ],
+ [
+ "▁fishing",
+ -11.0158109664917
+ ],
+ [
+ "gram",
+ -11.015937805175781
+ ],
+ [
+ "▁ST",
+ -11.016227722167969
+ ],
+ [
+ "▁Nov",
+ -11.01632022857666
+ ],
+ [
+ "▁reporting",
+ -11.016362190246582
+ ],
+ [
+ "ked",
+ -11.016467094421387
+ ],
+ [
+ "▁Leben",
+ -11.016557693481445
+ ],
+ [
+ "▁organisation",
+ -11.016843795776367
+ ],
+ [
+ "▁tiny",
+ -11.017144203186035
+ ],
+ [
+ "▁Alex",
+ -11.017236709594727
+ ],
+ [
+ "▁obtained",
+ -11.017255783081055
+ ],
+ [
+ "▁Acest",
+ -11.017367362976074
+ ],
+ [
+ "▁dangerous",
+ -11.01749038696289
+ ],
+ [
+ "utter",
+ -11.017624855041504
+ ],
+ [
+ "▁rev",
+ -11.01801586151123
+ ],
+ [
+ "Un",
+ -11.018242835998535
+ ],
+ [
+ "▁revealed",
+ -11.018356323242188
+ ],
+ [
+ "▁decade",
+ -11.018709182739258
+ ],
+ [
+ "▁possibility",
+ -11.01945686340332
+ ],
+ [
+ "service",
+ -11.019577980041504
+ ],
+ [
+ "è",
+ -11.01966667175293
+ ],
+ [
+ "▁Chief",
+ -11.019674301147461
+ ],
+ [
+ "▁Durch",
+ -11.019795417785645
+ ],
+ [
+ "▁cadre",
+ -11.019843101501465
+ ],
+ [
+ "▁wearing",
+ -11.019845008850098
+ ],
+ [
+ "sized",
+ -11.01988410949707
+ ],
+ [
+ "LY",
+ -11.01989459991455
+ ],
+ [
+ "▁unser",
+ -11.019963264465332
+ ],
+ [
+ "▁2016,",
+ -11.019988059997559
+ ],
+ [
+ "▁fail",
+ -11.020028114318848
+ ],
+ [
+ "iques",
+ -11.020115852355957
+ ],
+ [
+ "▁Angel",
+ -11.020315170288086
+ ],
+ [
+ "▁transportation",
+ -11.020364761352539
+ ],
+ [
+ "▁dates",
+ -11.020395278930664
+ ],
+ [
+ "▁danger",
+ -11.020731925964355
+ ],
+ [
+ "▁forum",
+ -11.020828247070312
+ ],
+ [
+ "zug",
+ -11.020885467529297
+ ],
+ [
+ "▁filed",
+ -11.021199226379395
+ ],
+ [
+ "loc",
+ -11.021201133728027
+ ],
+ [
+ "éri",
+ -11.021234512329102
+ ],
+ [
+ "tribu",
+ -11.021393775939941
+ ],
+ [
+ "▁entered",
+ -11.021639823913574
+ ],
+ [
+ "▁porte",
+ -11.021928787231445
+ ],
+ [
+ "▁arts",
+ -11.021979331970215
+ ],
+ [
+ "▁reform",
+ -11.022001266479492
+ ],
+ [
+ "▁Main",
+ -11.022101402282715
+ ],
+ [
+ "▁dir",
+ -11.022111892700195
+ ],
+ [
+ "▁approval",
+ -11.022465705871582
+ ],
+ [
+ "▁juice",
+ -11.022750854492188
+ ],
+ [
+ "vier",
+ -11.022771835327148
+ ],
+ [
+ "▁nivel",
+ -11.02318000793457
+ ],
+ [
+ "▁returns",
+ -11.023423194885254
+ ],
+ [
+ "▁formed",
+ -11.023723602294922
+ ],
+ [
+ "▁combine",
+ -11.02436351776123
+ ],
+ [
+ "▁cours",
+ -11.024392127990723
+ ],
+ [
+ "▁Standard",
+ -11.024463653564453
+ ],
+ [
+ "▁certification",
+ -11.024677276611328
+ ],
+ [
+ "escu",
+ -11.024996757507324
+ ],
+ [
+ "▁achieved",
+ -11.025278091430664
+ ],
+ [
+ "▁Model",
+ -11.025280952453613
+ ],
+ [
+ "rul",
+ -11.025404930114746
+ ],
+ [
+ "▁Tage",
+ -11.025530815124512
+ ],
+ [
+ "▁injuries",
+ -11.02560806274414
+ ],
+ [
+ "▁Sal",
+ -11.025671005249023
+ ],
+ [
+ "▁expenses",
+ -11.025887489318848
+ ],
+ [
+ "▁cet",
+ -11.026009559631348
+ ],
+ [
+ "▁taxes",
+ -11.026028633117676
+ ],
+ [
+ "diesen",
+ -11.02626895904541
+ ],
+ [
+ "▁fairly",
+ -11.026638984680176
+ ],
+ [
+ "▁Access",
+ -11.026866912841797
+ ],
+ [
+ "wind",
+ -11.027122497558594
+ ],
+ [
+ "IM",
+ -11.027252197265625
+ ],
+ [
+ "ense",
+ -11.027548789978027
+ ],
+ [
+ "▁hang",
+ -11.027957916259766
+ ],
+ [
+ "▁citizens",
+ -11.028020858764648
+ ],
+ [
+ "3%",
+ -11.028101921081543
+ ],
+ [
+ "lum",
+ -11.028268814086914
+ ],
+ [
+ "▁discussed",
+ -11.028326034545898
+ ],
+ [
+ "AC",
+ -11.02841854095459
+ ],
+ [
+ "‘",
+ -11.0286865234375
+ ],
+ [
+ "▁Sol",
+ -11.028698921203613
+ ],
+ [
+ "06",
+ -11.028816223144531
+ ],
+ [
+ "stellen",
+ -11.029170989990234
+ ],
+ [
+ "▁participation",
+ -11.02917194366455
+ ],
+ [
+ "▁Box",
+ -11.029200553894043
+ ],
+ [
+ "▁bieten",
+ -11.029687881469727
+ ],
+ [
+ "▁Louis",
+ -11.029730796813965
+ ],
+ [
+ "▁lessons",
+ -11.029789924621582
+ ],
+ [
+ "▁visible",
+ -11.029966354370117
+ ],
+ [
+ "▁Cam",
+ -11.030128479003906
+ ],
+ [
+ "▁Ban",
+ -11.03053092956543
+ ],
+ [
+ "▁Far",
+ -11.03060245513916
+ ],
+ [
+ "▁travers",
+ -11.030759811401367
+ ],
+ [
+ "▁telling",
+ -11.030808448791504
+ ],
+ [
+ "▁magic",
+ -11.030855178833008
+ ],
+ [
+ "▁Night",
+ -11.031316757202148
+ ],
+ [
+ "▁judge",
+ -11.031400680541992
+ ],
+ [
+ "▁Pat",
+ -11.031482696533203
+ ],
+ [
+ "▁Southern",
+ -11.031901359558105
+ ],
+ [
+ "OL",
+ -11.031929969787598
+ ],
+ [
+ "fully",
+ -11.032191276550293
+ ],
+ [
+ "▁acestea",
+ -11.03223705291748
+ ],
+ [
+ "▁Order",
+ -11.032383918762207
+ ],
+ [
+ "▁facut",
+ -11.032523155212402
+ ],
+ [
+ "▁Matt",
+ -11.032600402832031
+ ],
+ [
+ "registr",
+ -11.03278923034668
+ ],
+ [
+ "▁Yet",
+ -11.032811164855957
+ ],
+ [
+ "ß",
+ -11.033596992492676
+ ],
+ [
+ "▁făcut",
+ -11.033618927001953
+ ],
+ [
+ "▁versions",
+ -11.033780097961426
+ ],
+ [
+ "▁Force",
+ -11.03396224975586
+ ],
+ [
+ "rick",
+ -11.034153938293457
+ ],
+ [
+ "▁rund",
+ -11.034563064575195
+ ],
+ [
+ "ike",
+ -11.034658432006836
+ ],
+ [
+ "▁Young",
+ -11.034675598144531
+ ],
+ [
+ "▁ski",
+ -11.034927368164062
+ ],
+ [
+ "CU",
+ -11.035385131835938
+ ],
+ [
+ "▁Second",
+ -11.035510063171387
+ ],
+ [
+ "▁graduate",
+ -11.03554916381836
+ ],
+ [
+ "▁Bible",
+ -11.036049842834473
+ ],
+ [
+ "▁vary",
+ -11.036060333251953
+ ],
+ [
+ "▁celebration",
+ -11.036151885986328
+ ],
+ [
+ "▁risks",
+ -11.036210060119629
+ ],
+ [
+ "erii",
+ -11.036327362060547
+ ],
+ [
+ "rance",
+ -11.036577224731445
+ ],
+ [
+ "▁MP",
+ -11.036787986755371
+ ],
+ [
+ "▁tale",
+ -11.036788940429688
+ ],
+ [
+ "▁Ford",
+ -11.037044525146484
+ ],
+ [
+ "▁attached",
+ -11.037278175354004
+ ],
+ [
+ "▁Sy",
+ -11.037312507629395
+ ],
+ [
+ "▁Ly",
+ -11.03765869140625
+ ],
+ [
+ "stellung",
+ -11.037687301635742
+ ],
+ [
+ "▁trop",
+ -11.0377197265625
+ ],
+ [
+ "▁années",
+ -11.037736892700195
+ ],
+ [
+ "▁linked",
+ -11.03792667388916
+ ],
+ [
+ "pit",
+ -11.038352012634277
+ ],
+ [
+ "So",
+ -11.03835391998291
+ ],
+ [
+ "ţe",
+ -11.038473129272461
+ ],
+ [
+ "▁origin",
+ -11.038509368896484
+ ],
+ [
+ "▁boys",
+ -11.039263725280762
+ ],
+ [
+ "holder",
+ -11.039352416992188
+ ],
+ [
+ "read",
+ -11.039461135864258
+ ],
+ [
+ "▁relative",
+ -11.03950023651123
+ ],
+ [
+ "▁industries",
+ -11.03958511352539
+ ],
+ [
+ "making",
+ -11.039688110351562
+ ],
+ [
+ "▁tun",
+ -11.039917945861816
+ ],
+ [
+ "▁forced",
+ -11.041061401367188
+ ],
+ [
+ "▁Welcome",
+ -11.041086196899414
+ ],
+ [
+ "▁explained",
+ -11.041138648986816
+ ],
+ [
+ "MP",
+ -11.041389465332031
+ ],
+ [
+ "▁Three",
+ -11.041613578796387
+ ],
+ [
+ "aza",
+ -11.041768074035645
+ ],
+ [
+ "▁1999",
+ -11.041924476623535
+ ],
+ [
+ "▁erst",
+ -11.042237281799316
+ ],
+ [
+ "RS",
+ -11.042623519897461
+ ],
+ [
+ "▁attractive",
+ -11.04279899597168
+ ],
+ [
+ "▁visited",
+ -11.042805671691895
+ ],
+ [
+ "▁nom",
+ -11.042874336242676
+ ],
+ [
+ "▁drum",
+ -11.042933464050293
+ ],
+ [
+ "cast",
+ -11.043068885803223
+ ],
+ [
+ "ogen",
+ -11.043105125427246
+ ],
+ [
+ "▁tech",
+ -11.04360294342041
+ ],
+ [
+ "▁Comment",
+ -11.043664932250977
+ ],
+ [
+ "▁Little",
+ -11.04405689239502
+ ],
+ [
+ "▁suggested",
+ -11.044086456298828
+ ],
+ [
+ "▁gar",
+ -11.044205665588379
+ ],
+ [
+ "▁crack",
+ -11.04458999633789
+ ],
+ [
+ "▁shooting",
+ -11.044676780700684
+ ],
+ [
+ "▁Try",
+ -11.044759750366211
+ ],
+ [
+ "▁Remember",
+ -11.045008659362793
+ ],
+ [
+ "▁folks",
+ -11.045217514038086
+ ],
+ [
+ "▁MS",
+ -11.045512199401855
+ ],
+ [
+ "▁Dia",
+ -11.04584789276123
+ ],
+ [
+ "3)",
+ -11.046561241149902
+ ],
+ [
+ "arbeit",
+ -11.04697036743164
+ ],
+ [
+ "▁pepper",
+ -11.047065734863281
+ ],
+ [
+ "zz",
+ -11.047107696533203
+ ],
+ [
+ "▁extreme",
+ -11.047235488891602
+ ],
+ [
+ "▁extrem",
+ -11.047367095947266
+ ],
+ [
+ "▁severe",
+ -11.047768592834473
+ ],
+ [
+ "▁networks",
+ -11.047882080078125
+ ],
+ [
+ "păr",
+ -11.047910690307617
+ ],
+ [
+ "sent",
+ -11.047933578491211
+ ],
+ [
+ "▁structures",
+ -11.048048973083496
+ ],
+ [
+ "▁Join",
+ -11.048078536987305
+ ],
+ [
+ "▁privind",
+ -11.048255920410156
+ ],
+ [
+ "▁marriage",
+ -11.04865837097168
+ ],
+ [
+ "▁liegt",
+ -11.048918724060059
+ ],
+ [
+ "eben",
+ -11.048995971679688
+ ],
+ [
+ "▁produse",
+ -11.049076080322266
+ ],
+ [
+ "▁tested",
+ -11.049090385437012
+ ],
+ [
+ "▁Queen",
+ -11.049134254455566
+ ],
+ [
+ "▁Tax",
+ -11.049687385559082
+ ],
+ [
+ "rian",
+ -11.049710273742676
+ ],
+ [
+ "▁Problem",
+ -11.050151824951172
+ ],
+ [
+ "izat",
+ -11.05023193359375
+ ],
+ [
+ "udi",
+ -11.050324440002441
+ ],
+ [
+ "▁LA",
+ -11.050718307495117
+ ],
+ [
+ "▁afford",
+ -11.051108360290527
+ ],
+ [
+ "▁percentage",
+ -11.05121898651123
+ ],
+ [
+ "▁cute",
+ -11.051547050476074
+ ],
+ [
+ "▁gorgeous",
+ -11.051891326904297
+ ],
+ [
+ "▁indoor",
+ -11.05190372467041
+ ],
+ [
+ "▁configuration",
+ -11.052103042602539
+ ],
+ [
+ "▁immediate",
+ -11.052303314208984
+ ],
+ [
+ "▁exemple",
+ -11.052450180053711
+ ],
+ [
+ "▁Being",
+ -11.052550315856934
+ ],
+ [
+ "▁introduction",
+ -11.052591323852539
+ ],
+ [
+ "ella",
+ -11.053206443786621
+ ],
+ [
+ "bare",
+ -11.053521156311035
+ ],
+ [
+ "▁besser",
+ -11.053539276123047
+ ],
+ [
+ "▁Put",
+ -11.053740501403809
+ ],
+ [
+ "gon",
+ -11.054248809814453
+ ],
+ [
+ "▁Italy",
+ -11.054259300231934
+ ],
+ [
+ "▁Thus",
+ -11.05435562133789
+ ],
+ [
+ "tari",
+ -11.054437637329102
+ ],
+ [
+ "0.000",
+ -11.054460525512695
+ ],
+ [
+ "▁Price",
+ -11.054651260375977
+ ],
+ [
+ "▁Trust",
+ -11.054824829101562
+ ],
+ [
+ "▁contra",
+ -11.054863929748535
+ ],
+ [
+ "▁layout",
+ -11.05504035949707
+ ],
+ [
+ "▁Ireland",
+ -11.055187225341797
+ ],
+ [
+ "ctor",
+ -11.055344581604004
+ ],
+ [
+ "atoare",
+ -11.055540084838867
+ ],
+ [
+ "pra",
+ -11.055729866027832
+ ],
+ [
+ "rent",
+ -11.055892944335938
+ ],
+ [
+ "▁Seite",
+ -11.05605411529541
+ ],
+ [
+ "▁ori",
+ -11.056280136108398
+ ],
+ [
+ "spiel",
+ -11.056541442871094
+ ],
+ [
+ "▁Times",
+ -11.056883811950684
+ ],
+ [
+ "primarily",
+ -11.056974411010742
+ ],
+ [
+ "nov",
+ -11.05703067779541
+ ],
+ [
+ "▁desired",
+ -11.057061195373535
+ ],
+ [
+ "▁Would",
+ -11.057072639465332
+ ],
+ [
+ "PL",
+ -11.057225227355957
+ ],
+ [
+ "▁originally",
+ -11.057367324829102
+ ],
+ [
+ "▁Ana",
+ -11.057463645935059
+ ],
+ [
+ "EN",
+ -11.05754566192627
+ ],
+ [
+ "▁occasion",
+ -11.05755615234375
+ ],
+ [
+ "▁grant",
+ -11.057572364807129
+ ],
+ [
+ "igkeit",
+ -11.057975769042969
+ ],
+ [
+ "▁scheme",
+ -11.058146476745605
+ ],
+ [
+ "▁2015.",
+ -11.058621406555176
+ ],
+ [
+ "izare",
+ -11.058778762817383
+ ],
+ [
+ "gate",
+ -11.058792114257812
+ ],
+ [
+ "▁poker",
+ -11.058899879455566
+ ],
+ [
+ "pping",
+ -11.058998107910156
+ ],
+ [
+ "▁Wild",
+ -11.059511184692383
+ ],
+ [
+ "▁YouTube",
+ -11.059995651245117
+ ],
+ [
+ "▁assume",
+ -11.060284614562988
+ ],
+ [
+ "с",
+ -11.060614585876465
+ ],
+ [
+ "▁rapport",
+ -11.060623168945312
+ ],
+ [
+ "▁labor",
+ -11.060996055603027
+ ],
+ [
+ "teur",
+ -11.061041831970215
+ ],
+ [
+ "▁genre",
+ -11.06116008758545
+ ],
+ [
+ "▁plat",
+ -11.061745643615723
+ ],
+ [
+ "▁listening",
+ -11.061750411987305
+ ],
+ [
+ "sky",
+ -11.061777114868164
+ ],
+ [
+ "▁neighborhood",
+ -11.061782836914062
+ ],
+ [
+ "▁3-",
+ -11.062150001525879
+ ],
+ [
+ "▁Library",
+ -11.062162399291992
+ ],
+ [
+ "agit",
+ -11.062249183654785
+ ],
+ [
+ "▁platforms",
+ -11.062849998474121
+ ],
+ [
+ "bei",
+ -11.062882423400879
+ ],
+ [
+ "AB",
+ -11.062897682189941
+ ],
+ [
+ "▁manufacturers",
+ -11.06295394897461
+ ],
+ [
+ "▁printing",
+ -11.063141822814941
+ ],
+ [
+ "▁crisis",
+ -11.063326835632324
+ ],
+ [
+ "▁Smart",
+ -11.06335163116455
+ ],
+ [
+ "▁drawing",
+ -11.063406944274902
+ ],
+ [
+ "MO",
+ -11.06348991394043
+ ],
+ [
+ "▁durable",
+ -11.063569068908691
+ ],
+ [
+ "chant",
+ -11.0636625289917
+ ],
+ [
+ "▁chemical",
+ -11.063764572143555
+ ],
+ [
+ "▁savoir",
+ -11.063776016235352
+ ],
+ [
+ "▁Max",
+ -11.063802719116211
+ ],
+ [
+ "gestellt",
+ -11.06380844116211
+ ],
+ [
+ "▁rural",
+ -11.063854217529297
+ ],
+ [
+ "52",
+ -11.064105033874512
+ ],
+ [
+ "▁invited",
+ -11.064169883728027
+ ],
+ [
+ "▁fil",
+ -11.0642728805542
+ ],
+ [
+ "▁Rob",
+ -11.064284324645996
+ ],
+ [
+ "▁Bell",
+ -11.064387321472168
+ ],
+ [
+ "▁neck",
+ -11.064831733703613
+ ],
+ [
+ "pac",
+ -11.064879417419434
+ ],
+ [
+ "wal",
+ -11.06491470336914
+ ],
+ [
+ "▁là",
+ -11.064922332763672
+ ],
+ [
+ "▁Virginia",
+ -11.065081596374512
+ ],
+ [
+ "▁applicable",
+ -11.06509017944336
+ ],
+ [
+ "▁abuse",
+ -11.065153121948242
+ ],
+ [
+ "aide",
+ -11.065321922302246
+ ],
+ [
+ "▁increases",
+ -11.065396308898926
+ ],
+ [
+ "▁moi",
+ -11.065568923950195
+ ],
+ [
+ "▁Non",
+ -11.065577507019043
+ ],
+ [
+ "▁Produkt",
+ -11.065627098083496
+ ],
+ [
+ "FC",
+ -11.065644264221191
+ ],
+ [
+ "▁shops",
+ -11.065677642822266
+ ],
+ [
+ "▁prendre",
+ -11.065923690795898
+ ],
+ [
+ "atul",
+ -11.065990447998047
+ ],
+ [
+ "▁sal",
+ -11.066137313842773
+ ],
+ [
+ "▁société",
+ -11.06627082824707
+ ],
+ [
+ "▁Hot",
+ -11.066329002380371
+ ],
+ [
+ "rim",
+ -11.066587448120117
+ ],
+ [
+ "gue",
+ -11.06661605834961
+ ],
+ [
+ "▁enterprise",
+ -11.066624641418457
+ ],
+ [
+ "▁33",
+ -11.067329406738281
+ ],
+ [
+ "mittel",
+ -11.067395210266113
+ ],
+ [
+ "ged",
+ -11.067439079284668
+ ],
+ [
+ "▁formula",
+ -11.06777286529541
+ ],
+ [
+ "▁spin",
+ -11.067784309387207
+ ],
+ [
+ "als",
+ -11.067826271057129
+ ],
+ [
+ "2%",
+ -11.06785774230957
+ ],
+ [
+ "bon",
+ -11.068192481994629
+ ],
+ [
+ "▁Executive",
+ -11.068323135375977
+ ],
+ [
+ "▁wirklich",
+ -11.068427085876465
+ ],
+ [
+ "îl",
+ -11.068608283996582
+ ],
+ [
+ "1.",
+ -11.068917274475098
+ ],
+ [
+ "▁Arm",
+ -11.069157600402832
+ ],
+ [
+ "▁rid",
+ -11.069358825683594
+ ],
+ [
+ "aries",
+ -11.069727897644043
+ ],
+ [
+ "▁incident",
+ -11.06982421875
+ ],
+ [
+ "▁copii",
+ -11.070008277893066
+ ],
+ [
+ "▁Charles",
+ -11.070141792297363
+ ],
+ [
+ "▁meals",
+ -11.070147514343262
+ ],
+ [
+ "▁wireless",
+ -11.070237159729004
+ ],
+ [
+ "Ex",
+ -11.070364952087402
+ ],
+ [
+ "▁Financial",
+ -11.070540428161621
+ ],
+ [
+ "▁AM",
+ -11.070615768432617
+ ],
+ [
+ "▁fest",
+ -11.070645332336426
+ ],
+ [
+ "▁Ol",
+ -11.071410179138184
+ ],
+ [
+ "oir",
+ -11.071447372436523
+ ],
+ [
+ "300",
+ -11.071893692016602
+ ],
+ [
+ "▁punct",
+ -11.072138786315918
+ ],
+ [
+ "▁Mad",
+ -11.07283878326416
+ ],
+ [
+ "▁Ali",
+ -11.072907447814941
+ ],
+ [
+ "lag",
+ -11.073214530944824
+ ],
+ [
+ "▁ocean",
+ -11.073314666748047
+ ],
+ [
+ "▁mirror",
+ -11.073326110839844
+ ],
+ [
+ "▁Additionally",
+ -11.073869705200195
+ ],
+ [
+ "alia",
+ -11.073884963989258
+ ],
+ [
+ "▁county",
+ -11.073899269104004
+ ],
+ [
+ "▁hip",
+ -11.074305534362793
+ ],
+ [
+ "dale",
+ -11.074395179748535
+ ],
+ [
+ "▁Stra",
+ -11.074429512023926
+ ],
+ [
+ "▁drag",
+ -11.074575424194336
+ ],
+ [
+ "▁Sand",
+ -11.074851036071777
+ ],
+ [
+ "▁historic",
+ -11.074980735778809
+ ],
+ [
+ "ière",
+ -11.075427055358887
+ ],
+ [
+ "▁examine",
+ -11.075624465942383
+ ],
+ [
+ "soci",
+ -11.075634002685547
+ ],
+ [
+ "ime",
+ -11.076088905334473
+ ],
+ [
+ "▁Insurance",
+ -11.07621955871582
+ ],
+ [
+ "▁crime",
+ -11.076736450195312
+ ],
+ [
+ "▁pare",
+ -11.076945304870605
+ ],
+ [
+ "▁craft",
+ -11.077105522155762
+ ],
+ [
+ "▁Building",
+ -11.077279090881348
+ ],
+ [
+ "mission",
+ -11.077534675598145
+ ],
+ [
+ "▁Americans",
+ -11.077573776245117
+ ],
+ [
+ "▁mg",
+ -11.077799797058105
+ ],
+ [
+ "▁passage",
+ -11.077938079833984
+ ],
+ [
+ "▁deposit",
+ -11.078346252441406
+ ],
+ [
+ "▁widely",
+ -11.078444480895996
+ ],
+ [
+ "nch",
+ -11.078453063964844
+ ],
+ [
+ "▁Coast",
+ -11.078756332397461
+ ],
+ [
+ "▁recipes",
+ -11.078784942626953
+ ],
+ [
+ "▁Ziel",
+ -11.07951545715332
+ ],
+ [
+ "▁duty",
+ -11.079646110534668
+ ],
+ [
+ "▁gerne",
+ -11.079704284667969
+ ],
+ [
+ "most",
+ -11.080034255981445
+ ],
+ [
+ "▁argument",
+ -11.080158233642578
+ ],
+ [
+ "▁root",
+ -11.08021354675293
+ ],
+ [
+ "▁consult",
+ -11.08024787902832
+ ],
+ [
+ "▁muscle",
+ -11.080255508422852
+ ],
+ [
+ "▁spoke",
+ -11.08038330078125
+ ],
+ [
+ "▁Cum",
+ -11.080950736999512
+ ],
+ [
+ "▁orange",
+ -11.081033706665039
+ ],
+ [
+ "▁reader",
+ -11.081123352050781
+ ],
+ [
+ "schw",
+ -11.081151008605957
+ ],
+ [
+ "▁commission",
+ -11.081332206726074
+ ],
+ [
+ "histoire",
+ -11.081811904907227
+ ],
+ [
+ "▁represents",
+ -11.082064628601074
+ ],
+ [
+ "▁meilleur",
+ -11.082343101501465
+ ],
+ [
+ "▁10.",
+ -11.082358360290527
+ ],
+ [
+ "HA",
+ -11.082427024841309
+ ],
+ [
+ "▁Systems",
+ -11.082573890686035
+ ],
+ [
+ "▁blind",
+ -11.082603454589844
+ ],
+ [
+ "▁HP",
+ -11.083221435546875
+ ],
+ [
+ "▁doi",
+ -11.083307266235352
+ ],
+ [
+ "▁signature",
+ -11.083404541015625
+ ],
+ [
+ "▁invite",
+ -11.083505630493164
+ ],
+ [
+ "▁Samsung",
+ -11.083802223205566
+ ],
+ [
+ "▁liber",
+ -11.083942413330078
+ ],
+ [
+ "▁letters",
+ -11.0840482711792
+ ],
+ [
+ "▁primul",
+ -11.084186553955078
+ ],
+ [
+ "▁losing",
+ -11.084328651428223
+ ],
+ [
+ "resulting",
+ -11.084467887878418
+ ],
+ [
+ "▁Computer",
+ -11.08474063873291
+ ],
+ [
+ "▁poll",
+ -11.0847749710083
+ ],
+ [
+ "rile",
+ -11.085102081298828
+ ],
+ [
+ "TI",
+ -11.085142135620117
+ ],
+ [
+ "▁cur",
+ -11.08566951751709
+ ],
+ [
+ "▁fonction",
+ -11.085833549499512
+ ],
+ [
+ "gat",
+ -11.086359977722168
+ ],
+ [
+ "AA",
+ -11.086480140686035
+ ],
+ [
+ "tiv",
+ -11.086692810058594
+ ],
+ [
+ "▁Str",
+ -11.087076187133789
+ ],
+ [
+ "ești",
+ -11.087677955627441
+ ],
+ [
+ "▁officer",
+ -11.0877046585083
+ ],
+ [
+ "reducing",
+ -11.08772087097168
+ ],
+ [
+ "▁gifts",
+ -11.08780288696289
+ ],
+ [
+ "▁performing",
+ -11.08788776397705
+ ],
+ [
+ "▁»,",
+ -11.088349342346191
+ ],
+ [
+ "▁guitar",
+ -11.08838939666748
+ ],
+ [
+ "▁segment",
+ -11.088580131530762
+ ],
+ [
+ "▁Tar",
+ -11.08861255645752
+ ],
+ [
+ "▁ultimately",
+ -11.088805198669434
+ ],
+ [
+ "▁cam",
+ -11.088960647583008
+ ],
+ [
+ "▁Arbeit",
+ -11.089076042175293
+ ],
+ [
+ "▁accessories",
+ -11.089418411254883
+ ],
+ [
+ "bad",
+ -11.089820861816406
+ ],
+ [
+ "home",
+ -11.0899019241333
+ ],
+ [
+ "▁clip",
+ -11.08995532989502
+ ],
+ [
+ "range",
+ -11.090432167053223
+ ],
+ [
+ "CM",
+ -11.090867042541504
+ ],
+ [
+ "▁printed",
+ -11.090883255004883
+ ],
+ [
+ "▁Pet",
+ -11.091177940368652
+ ],
+ [
+ "▁attract",
+ -11.091333389282227
+ ],
+ [
+ "date",
+ -11.091501235961914
+ ],
+ [
+ "▁Senior",
+ -11.091503143310547
+ ],
+ [
+ "▁genau",
+ -11.092177391052246
+ ],
+ [
+ "num",
+ -11.092435836791992
+ ],
+ [
+ "▁attended",
+ -11.092674255371094
+ ],
+ [
+ "▁Turn",
+ -11.092824935913086
+ ],
+ [
+ "▁History",
+ -11.092830657958984
+ ],
+ [
+ "some",
+ -11.092852592468262
+ ],
+ [
+ "▁describe",
+ -11.09308910369873
+ ],
+ [
+ "▁Lee",
+ -11.093143463134766
+ ],
+ [
+ "▁Fre",
+ -11.093314170837402
+ ],
+ [
+ "▁league",
+ -11.093345642089844
+ ],
+ [
+ "new",
+ -11.093505859375
+ ],
+ [
+ "tors",
+ -11.093535423278809
+ ],
+ [
+ "▁storm",
+ -11.094005584716797
+ ],
+ [
+ "▁Beispiel",
+ -11.094197273254395
+ ],
+ [
+ "▁index",
+ -11.094344139099121
+ ],
+ [
+ "▁awarded",
+ -11.094613075256348
+ ],
+ [
+ "state",
+ -11.094625473022461
+ ],
+ [
+ "▁1990",
+ -11.094874382019043
+ ],
+ [
+ "▁ends",
+ -11.094902992248535
+ ],
+ [
+ "kor",
+ -11.095070838928223
+ ],
+ [
+ "far",
+ -11.095418930053711
+ ],
+ [
+ "▁Page",
+ -11.095541000366211
+ ],
+ [
+ "▁promotion",
+ -11.095610618591309
+ ],
+ [
+ "▁weekly",
+ -11.095726013183594
+ ],
+ [
+ "400",
+ -11.095966339111328
+ ],
+ [
+ "iuni",
+ -11.096365928649902
+ ],
+ [
+ "▁Summer",
+ -11.096376419067383
+ ],
+ [
+ "▁thin",
+ -11.096627235412598
+ ],
+ [
+ "▁dafür",
+ -11.09669303894043
+ ],
+ [
+ "51",
+ -11.096769332885742
+ ],
+ [
+ "PR",
+ -11.096978187561035
+ ],
+ [
+ "▁Hy",
+ -11.097001075744629
+ ],
+ [
+ "gas",
+ -11.097013473510742
+ ],
+ [
+ "▁atat",
+ -11.097166061401367
+ ],
+ [
+ "▁mining",
+ -11.097347259521484
+ ],
+ [
+ "▁principles",
+ -11.09741497039795
+ ],
+ [
+ "gent",
+ -11.097545623779297
+ ],
+ [
+ "ika",
+ -11.097685813903809
+ ],
+ [
+ "▁religion",
+ -11.097787857055664
+ ],
+ [
+ "▁ordered",
+ -11.098284721374512
+ ],
+ [
+ "▁developers",
+ -11.098298072814941
+ ],
+ [
+ "▁pleasure",
+ -11.098456382751465
+ ],
+ [
+ "vit",
+ -11.098505020141602
+ ],
+ [
+ "mers",
+ -11.0988130569458
+ ],
+ [
+ "▁Section",
+ -11.098873138427734
+ ],
+ [
+ "▁por",
+ -11.098960876464844
+ ],
+ [
+ "▁Name",
+ -11.099200248718262
+ ],
+ [
+ "▁pink",
+ -11.099260330200195
+ ],
+ [
+ "dig",
+ -11.09934139251709
+ ],
+ [
+ "▁eligible",
+ -11.099397659301758
+ ],
+ [
+ "▁Happy",
+ -11.09941577911377
+ ],
+ [
+ "▁fo",
+ -11.099480628967285
+ ],
+ [
+ "▁availability",
+ -11.099541664123535
+ ],
+ [
+ "GO",
+ -11.099583625793457
+ ],
+ [
+ "▁Europa",
+ -11.099637985229492
+ ],
+ [
+ "▁Unit",
+ -11.099656105041504
+ ],
+ [
+ "▁1000",
+ -11.099837303161621
+ ],
+ [
+ "▁Berg",
+ -11.099846839904785
+ ],
+ [
+ "fini",
+ -11.099853515625
+ ],
+ [
+ "▁$3",
+ -11.100565910339355
+ ],
+ [
+ "iza",
+ -11.100749969482422
+ ],
+ [
+ "▁promo",
+ -11.100830078125
+ ],
+ [
+ "▁Low",
+ -11.101234436035156
+ ],
+ [
+ "abord",
+ -11.101326942443848
+ ],
+ [
+ "äh",
+ -11.101485252380371
+ ],
+ [
+ "▁Professor",
+ -11.101570129394531
+ ],
+ [
+ "▁array",
+ -11.101579666137695
+ ],
+ [
+ "▁hate",
+ -11.101594924926758
+ ],
+ [
+ "▁recording",
+ -11.101601600646973
+ ],
+ [
+ "RI",
+ -11.101649284362793
+ ],
+ [
+ "▁proof",
+ -11.101710319519043
+ ],
+ [
+ "lay",
+ -11.10185718536377
+ ],
+ [
+ "DE",
+ -11.102007865905762
+ ],
+ [
+ "▁surprised",
+ -11.102066040039062
+ ],
+ [
+ "▁boxes",
+ -11.102193832397461
+ ],
+ [
+ "▁noastre",
+ -11.102386474609375
+ ],
+ [
+ "zie",
+ -11.102387428283691
+ ],
+ [
+ "▁însă",
+ -11.10254192352295
+ ],
+ [
+ "▁ajuta",
+ -11.102783203125
+ ],
+ [
+ "▁weil",
+ -11.1028413772583
+ ],
+ [
+ "▁whenever",
+ -11.103026390075684
+ ],
+ [
+ "shi",
+ -11.103194236755371
+ ],
+ [
+ "satz",
+ -11.103605270385742
+ ],
+ [
+ "▁remind",
+ -11.10401725769043
+ ],
+ [
+ "▁consist",
+ -11.10412311553955
+ ],
+ [
+ "▁motiv",
+ -11.104240417480469
+ ],
+ [
+ "▁PS",
+ -11.1043062210083
+ ],
+ [
+ "▁trois",
+ -11.104543685913086
+ ],
+ [
+ "pad",
+ -11.10477352142334
+ ],
+ [
+ "▁besten",
+ -11.104904174804688
+ ],
+ [
+ "▁Stone",
+ -11.105140686035156
+ ],
+ [
+ "itz",
+ -11.105157852172852
+ ],
+ [
+ "fit",
+ -11.105164527893066
+ ],
+ [
+ "▁Mountain",
+ -11.105178833007812
+ ],
+ [
+ "OC",
+ -11.10519027709961
+ ],
+ [
+ "▁depends",
+ -11.105228424072266
+ ],
+ [
+ "▁Cover",
+ -11.105387687683105
+ ],
+ [
+ "▁bags",
+ -11.106058120727539
+ ],
+ [
+ "▁Bel",
+ -11.106199264526367
+ ],
+ [
+ "▁Engineering",
+ -11.106304168701172
+ ],
+ [
+ "▁flower",
+ -11.106647491455078
+ ],
+ [
+ "▁gratuit",
+ -11.106670379638672
+ ],
+ [
+ "▁smartphone",
+ -11.106780052185059
+ ],
+ [
+ "stan",
+ -11.107197761535645
+ ],
+ [
+ "spect",
+ -11.10726261138916
+ ],
+ [
+ "SL",
+ -11.107282638549805
+ ],
+ [
+ "sho",
+ -11.10738754272461
+ ],
+ [
+ "▁Ser",
+ -11.10791301727295
+ ],
+ [
+ "▁Perhaps",
+ -11.108247756958008
+ ],
+ [
+ "▁codes",
+ -11.108342170715332
+ ],
+ [
+ "▁Wind",
+ -11.10849666595459
+ ],
+ [
+ "aient",
+ -11.108757019042969
+ ],
+ [
+ "▁Prin",
+ -11.108802795410156
+ ],
+ [
+ "▁(1)",
+ -11.109090805053711
+ ],
+ [
+ "▁figures",
+ -11.109450340270996
+ ],
+ [
+ "▁ausge",
+ -11.10972785949707
+ ],
+ [
+ "▁episode",
+ -11.110050201416016
+ ],
+ [
+ "▁Spa",
+ -11.110370635986328
+ ],
+ [
+ "▁Silver",
+ -11.110386848449707
+ ],
+ [
+ "▁Sky",
+ -11.110396385192871
+ ],
+ [
+ "▁capabilities",
+ -11.1107177734375
+ ],
+ [
+ "▁Uni",
+ -11.11073112487793
+ ],
+ [
+ "▁încă",
+ -11.110876083374023
+ ],
+ [
+ "TO",
+ -11.111289978027344
+ ],
+ [
+ "▁Hal",
+ -11.111358642578125
+ ],
+ [
+ "ghi",
+ -11.111414909362793
+ ],
+ [
+ "▁sofa",
+ -11.111438751220703
+ ],
+ [
+ "hard",
+ -11.11150074005127
+ ],
+ [
+ "▁FOR",
+ -11.111587524414062
+ ],
+ [
+ "▁Ber",
+ -11.111820220947266
+ ],
+ [
+ "▁firms",
+ -11.11187744140625
+ ],
+ [
+ "▁memories",
+ -11.111883163452148
+ ],
+ [
+ "▁lift",
+ -11.11214542388916
+ ],
+ [
+ "▁sending",
+ -11.11214542388916
+ ],
+ [
+ "▁narrow",
+ -11.112646102905273
+ ],
+ [
+ "▁Steve",
+ -11.112784385681152
+ ],
+ [
+ "▁integration",
+ -11.112905502319336
+ ],
+ [
+ "known",
+ -11.113122940063477
+ ],
+ [
+ "▁nostru",
+ -11.113237380981445
+ ],
+ [
+ "iţi",
+ -11.113422393798828
+ ],
+ [
+ "▁Georgia",
+ -11.113759994506836
+ ],
+ [
+ "▁slowly",
+ -11.114026069641113
+ ],
+ [
+ "iere",
+ -11.114028930664062
+ ],
+ [
+ "aka",
+ -11.114255905151367
+ ],
+ [
+ "PE",
+ -11.114320755004883
+ ],
+ [
+ "▁venue",
+ -11.11468505859375
+ ],
+ [
+ "jar",
+ -11.11474609375
+ ],
+ [
+ "buch",
+ -11.114755630493164
+ ],
+ [
+ "rad",
+ -11.114858627319336
+ ],
+ [
+ "▁resistance",
+ -11.114899635314941
+ ],
+ [
+ "▁stehen",
+ -11.114914894104004
+ ],
+ [
+ "chin",
+ -11.11504077911377
+ ],
+ [
+ "▁weak",
+ -11.11535358428955
+ ],
+ [
+ "▁DVD",
+ -11.115598678588867
+ ],
+ [
+ "▁bodies",
+ -11.115856170654297
+ ],
+ [
+ "▁split",
+ -11.115884780883789
+ ],
+ [
+ "What",
+ -11.116231918334961
+ ],
+ [
+ "setzen",
+ -11.116467475891113
+ ],
+ [
+ "▁loves",
+ -11.116561889648438
+ ],
+ [
+ "▁kleine",
+ -11.117077827453613
+ ],
+ [
+ "▁increasingly",
+ -11.11746883392334
+ ],
+ [
+ "▁alert",
+ -11.117583274841309
+ ],
+ [
+ "▁AC",
+ -11.117647171020508
+ ],
+ [
+ "▁partir",
+ -11.117974281311035
+ ],
+ [
+ "▁ratio",
+ -11.11807918548584
+ ],
+ [
+ "▁keeps",
+ -11.118539810180664
+ ],
+ [
+ "▁Area",
+ -11.118544578552246
+ ],
+ [
+ "▁données",
+ -11.119071960449219
+ ],
+ [
+ "▁flag",
+ -11.119254112243652
+ ],
+ [
+ "▁NO",
+ -11.119277000427246
+ ],
+ [
+ "▁hotels",
+ -11.119336128234863
+ ],
+ [
+ "▁debut",
+ -11.119365692138672
+ ],
+ [
+ "▁suffer",
+ -11.119368553161621
+ ],
+ [
+ "▁hidden",
+ -11.119810104370117
+ ],
+ [
+ "▁clothing",
+ -11.120074272155762
+ ],
+ [
+ "▁household",
+ -11.120235443115234
+ ],
+ [
+ "medi",
+ -11.120268821716309
+ ],
+ [
+ "▁reste",
+ -11.120274543762207
+ ],
+ [
+ "bro",
+ -11.120381355285645
+ ],
+ [
+ "▁Bus",
+ -11.120405197143555
+ ],
+ [
+ "▁Ken",
+ -11.120572090148926
+ ],
+ [
+ "IR",
+ -11.120758056640625
+ ],
+ [
+ "▁suffering",
+ -11.121212005615234
+ ],
+ [
+ "▁publication",
+ -11.121246337890625
+ ],
+ [
+ "▁Mat",
+ -11.121360778808594
+ ],
+ [
+ "▁impression",
+ -11.121509552001953
+ ],
+ [
+ "▁founded",
+ -11.121562957763672
+ ],
+ [
+ "▁stable",
+ -11.121566772460938
+ ],
+ [
+ "▁promise",
+ -11.121719360351562
+ ],
+ [
+ "▁Cloud",
+ -11.121770858764648
+ ],
+ [
+ "▁prison",
+ -11.122099876403809
+ ],
+ [
+ "cor",
+ -11.122355461120605
+ ],
+ [
+ "▁Sports",
+ -11.122716903686523
+ ],
+ [
+ "▁erste",
+ -11.122745513916016
+ ],
+ [
+ "shire",
+ -11.122757911682129
+ ],
+ [
+ "▁recommendations",
+ -11.122916221618652
+ ],
+ [
+ "▁permit",
+ -11.123100280761719
+ ],
+ [
+ "▁tomorrow",
+ -11.123126983642578
+ ],
+ [
+ "▁lucky",
+ -11.123422622680664
+ ],
+ [
+ "▁realized",
+ -11.123449325561523
+ ],
+ [
+ "▁famille",
+ -11.123473167419434
+ ],
+ [
+ "▁Zealand",
+ -11.123542785644531
+ ],
+ [
+ "▁wooden",
+ -11.123601913452148
+ ],
+ [
+ "▁east",
+ -11.124269485473633
+ ],
+ [
+ "▁Bereich",
+ -11.12458324432373
+ ],
+ [
+ "während",
+ -11.124653816223145
+ ],
+ [
+ "rite",
+ -11.124836921691895
+ ],
+ [
+ "▁fla",
+ -11.124902725219727
+ ],
+ [
+ "platz",
+ -11.124991416931152
+ ],
+ [
+ "▁zero",
+ -11.125292778015137
+ ],
+ [
+ "▁priority",
+ -11.12535572052002
+ ],
+ [
+ "▁Airport",
+ -11.125506401062012
+ ],
+ [
+ "▁Kauf",
+ -11.125590324401855
+ ],
+ [
+ "▁ultimate",
+ -11.12601375579834
+ ],
+ [
+ "▁chest",
+ -11.126175880432129
+ ],
+ [
+ "▁tone",
+ -11.126376152038574
+ ],
+ [
+ "▁Kal",
+ -11.126431465148926
+ ],
+ [
+ "▁supposed",
+ -11.12669849395752
+ ],
+ [
+ "▁vedere",
+ -11.126846313476562
+ ],
+ [
+ "▁50%",
+ -11.126872062683105
+ ],
+ [
+ "▁Ger",
+ -11.127785682678223
+ ],
+ [
+ "pack",
+ -11.127849578857422
+ ],
+ [
+ "▁priv",
+ -11.128241539001465
+ ],
+ [
+ "▁Kit",
+ -11.128263473510742
+ ],
+ [
+ "▁tent",
+ -11.128457069396973
+ ],
+ [
+ "▁guidelines",
+ -11.128461837768555
+ ],
+ [
+ "▁Republic",
+ -11.128824234008789
+ ],
+ [
+ "including",
+ -11.129239082336426
+ ],
+ [
+ "▁chief",
+ -11.129615783691406
+ ],
+ [
+ "▁Living",
+ -11.129766464233398
+ ],
+ [
+ "keit",
+ -11.1298189163208
+ ],
+ [
+ "▁convert",
+ -11.129831314086914
+ ],
+ [
+ "tail",
+ -11.129928588867188
+ ],
+ [
+ "orient",
+ -11.129960060119629
+ ],
+ [
+ "eigenen",
+ -11.130245208740234
+ ],
+ [
+ "▁soup",
+ -11.130587577819824
+ ],
+ [
+ "▁zona",
+ -11.130661010742188
+ ],
+ [
+ "▁composition",
+ -11.130690574645996
+ ],
+ [
+ "▁Bob",
+ -11.130831718444824
+ ],
+ [
+ "▁exception",
+ -11.131170272827148
+ ],
+ [
+ "▁cr",
+ -11.131287574768066
+ ],
+ [
+ "▁str",
+ -11.131482124328613
+ ],
+ [
+ "▁Fl",
+ -11.13178825378418
+ ],
+ [
+ "AT",
+ -11.131909370422363
+ ],
+ [
+ "kel",
+ -11.132002830505371
+ ],
+ [
+ "▁pricing",
+ -11.132189750671387
+ ],
+ [
+ "▁Mass",
+ -11.132258415222168
+ ],
+ [
+ "vir",
+ -11.132333755493164
+ ],
+ [
+ "leg",
+ -11.132448196411133
+ ],
+ [
+ "▁rating",
+ -11.132455825805664
+ ],
+ [
+ "▁Sale",
+ -11.132628440856934
+ ],
+ [
+ "▁somewhere",
+ -11.132866859436035
+ ],
+ [
+ "▁submitted",
+ -11.133084297180176
+ ],
+ [
+ "▁Pop",
+ -11.133296012878418
+ ],
+ [
+ "▁papers",
+ -11.13330364227295
+ ],
+ [
+ "▁authorities",
+ -11.133326530456543
+ ],
+ [
+ "▁Person",
+ -11.133381843566895
+ ],
+ [
+ "▁kill",
+ -11.133512496948242
+ ],
+ [
+ "▁suggestions",
+ -11.133548736572266
+ ],
+ [
+ "-6",
+ -11.133644104003906
+ ],
+ [
+ "▁dust",
+ -11.133750915527344
+ ],
+ [
+ "taire",
+ -11.133805274963379
+ ],
+ [
+ "▁recognition",
+ -11.133870124816895
+ ],
+ [
+ "3.",
+ -11.134047508239746
+ ],
+ [
+ "▁Mont",
+ -11.134230613708496
+ ],
+ [
+ "▁produit",
+ -11.13430118560791
+ ],
+ [
+ "▁transmission",
+ -11.134340286254883
+ ],
+ [
+ "▁Th",
+ -11.13475513458252
+ ],
+ [
+ "▁passing",
+ -11.134928703308105
+ ],
+ [
+ "▁Partner",
+ -11.135161399841309
+ ],
+ [
+ "▁dire",
+ -11.135205268859863
+ ],
+ [
+ "▁DC",
+ -11.135432243347168
+ ],
+ [
+ "▁sky",
+ -11.135659217834473
+ ],
+ [
+ "▁Kitchen",
+ -11.135890007019043
+ ],
+ [
+ "▁fluid",
+ -11.135929107666016
+ ],
+ [
+ "▁scored",
+ -11.136005401611328
+ ],
+ [
+ "▁chapter",
+ -11.136100769042969
+ ],
+ [
+ "If",
+ -11.136231422424316
+ ],
+ [
+ "letzten",
+ -11.136275291442871
+ ],
+ [
+ "▁officers",
+ -11.13641357421875
+ ],
+ [
+ "▁avem",
+ -11.136631965637207
+ ],
+ [
+ "ister",
+ -11.136666297912598
+ ],
+ [
+ "▁involves",
+ -11.136688232421875
+ ],
+ [
+ "ico",
+ -11.136898040771484
+ ],
+ [
+ "bur",
+ -11.137056350708008
+ ],
+ [
+ "▁mieux",
+ -11.137064933776855
+ ],
+ [
+ "▁Photo",
+ -11.1371431350708
+ ],
+ [
+ "▁Cro",
+ -11.137228012084961
+ ],
+ [
+ "▁professor",
+ -11.137245178222656
+ ],
+ [
+ "▁besonders",
+ -11.137313842773438
+ ],
+ [
+ "д",
+ -11.137367248535156
+ ],
+ [
+ "▁alongside",
+ -11.137382507324219
+ ],
+ [
+ "▁stored",
+ -11.13770580291748
+ ],
+ [
+ "▁activ",
+ -11.137849807739258
+ ],
+ [
+ "▁setup",
+ -11.138169288635254
+ ],
+ [
+ "▁extract",
+ -11.138627052307129
+ ],
+ [
+ "▁accent",
+ -11.138633728027344
+ ],
+ [
+ "▁replaced",
+ -11.138638496398926
+ ],
+ [
+ "tec",
+ -11.138800621032715
+ ],
+ [
+ "▁Natur",
+ -11.138848304748535
+ ],
+ [
+ "▁Pacific",
+ -11.138887405395508
+ ],
+ [
+ "▁NY",
+ -11.139485359191895
+ ],
+ [
+ "▁Capital",
+ -11.139583587646484
+ ],
+ [
+ "▁forest",
+ -11.13969898223877
+ ],
+ [
+ "incredibly",
+ -11.14006233215332
+ ],
+ [
+ "▁choix",
+ -11.14021110534668
+ ],
+ [
+ "▁seriously",
+ -11.140281677246094
+ ],
+ [
+ "▁konnte",
+ -11.14030933380127
+ ],
+ [
+ "▁2014.",
+ -11.140443801879883
+ ],
+ [
+ "ensuring",
+ -11.140534400939941
+ ],
+ [
+ "▁handling",
+ -11.140661239624023
+ ],
+ [
+ "▁9.",
+ -11.140715599060059
+ ],
+ [
+ "▁relations",
+ -11.140876770019531
+ ],
+ [
+ "▁Kom",
+ -11.141045570373535
+ ],
+ [
+ "▁Hol",
+ -11.141282081604004
+ ],
+ [
+ "▁none",
+ -11.141515731811523
+ ],
+ [
+ "rob",
+ -11.141718864440918
+ ],
+ [
+ "▁Forum",
+ -11.141759872436523
+ ],
+ [
+ "hour",
+ -11.141776084899902
+ ],
+ [
+ "ème",
+ -11.141809463500977
+ ],
+ [
+ "▁Space",
+ -11.141986846923828
+ ],
+ [
+ "▁Ham",
+ -11.142992973327637
+ ],
+ [
+ "rap",
+ -11.143169403076172
+ ],
+ [
+ "▁Michigan",
+ -11.14317512512207
+ ],
+ [
+ "km",
+ -11.143202781677246
+ ],
+ [
+ "▁utilize",
+ -11.143548965454102
+ ],
+ [
+ "lov",
+ -11.143775939941406
+ ],
+ [
+ "▁luck",
+ -11.144388198852539
+ ],
+ [
+ "lä",
+ -11.144824981689453
+ ],
+ [
+ "▁healing",
+ -11.145010948181152
+ ],
+ [
+ "▁neu",
+ -11.145182609558105
+ ],
+ [
+ "aging",
+ -11.145251274108887
+ ],
+ [
+ "▁compliance",
+ -11.145583152770996
+ ],
+ [
+ "▁vertical",
+ -11.145675659179688
+ ],
+ [
+ "▁FREE",
+ -11.145729064941406
+ ],
+ [
+ "▁differences",
+ -11.146014213562012
+ ],
+ [
+ "▁Server",
+ -11.146252632141113
+ ],
+ [
+ "▁estimated",
+ -11.146378517150879
+ ],
+ [
+ "schutz",
+ -11.146692276000977
+ ],
+ [
+ "▁notamment",
+ -11.146736145019531
+ ],
+ [
+ "▁120",
+ -11.146919250488281
+ ],
+ [
+ "72",
+ -11.147282600402832
+ ],
+ [
+ "▁heating",
+ -11.147347450256348
+ ],
+ [
+ "late",
+ -11.14756965637207
+ ],
+ [
+ "▁younger",
+ -11.14783000946045
+ ],
+ [
+ "▁Intel",
+ -11.148171424865723
+ ],
+ [
+ "▁salad",
+ -11.148362159729004
+ ],
+ [
+ "▁commonly",
+ -11.148563385009766
+ ],
+ [
+ "▁treatments",
+ -11.148682594299316
+ ],
+ [
+ "▁speaker",
+ -11.148770332336426
+ ],
+ [
+ "▁producing",
+ -11.149120330810547
+ ],
+ [
+ "▁eggs",
+ -11.149367332458496
+ ],
+ [
+ "▁Spirit",
+ -11.149892807006836
+ ],
+ [
+ "▁beide",
+ -11.149918556213379
+ ],
+ [
+ "▁transaction",
+ -11.150283813476562
+ ],
+ [
+ "▁Machine",
+ -11.150464057922363
+ ],
+ [
+ "▁Games",
+ -11.150527000427246
+ ],
+ [
+ "▁niveau",
+ -11.150687217712402
+ ],
+ [
+ "▁Need",
+ -11.15082836151123
+ ],
+ [
+ "radi",
+ -11.150959968566895
+ ],
+ [
+ "mir",
+ -11.15096664428711
+ ],
+ [
+ "causing",
+ -11.151000022888184
+ ],
+ [
+ "▁début",
+ -11.151042938232422
+ ],
+ [
+ "▁rencontre",
+ -11.151063919067383
+ ],
+ [
+ "▁threat",
+ -11.151153564453125
+ ],
+ [
+ "▁enjoying",
+ -11.151320457458496
+ ],
+ [
+ "Com",
+ -11.151386260986328
+ ],
+ [
+ "▁Johnson",
+ -11.151555061340332
+ ],
+ [
+ "▁tournament",
+ -11.15156364440918
+ ],
+ [
+ "▁Micro",
+ -11.151582717895508
+ ],
+ [
+ "▁Drive",
+ -11.151667594909668
+ ],
+ [
+ "▁Cre",
+ -11.151866912841797
+ ],
+ [
+ "▁Lebens",
+ -11.151930809020996
+ ],
+ [
+ "▁categories",
+ -11.152358055114746
+ ],
+ [
+ "5,000",
+ -11.15261173248291
+ ],
+ [
+ "▁confirmed",
+ -11.152617454528809
+ ],
+ [
+ "pli",
+ -11.152763366699219
+ ],
+ [
+ "▁Francisco",
+ -11.153139114379883
+ ],
+ [
+ "▁raw",
+ -11.153157234191895
+ ],
+ [
+ "▁managers",
+ -11.153223991394043
+ ],
+ [
+ "ţie",
+ -11.153365135192871
+ ],
+ [
+ "UR",
+ -11.153368949890137
+ ],
+ [
+ "▁aproape",
+ -11.154065132141113
+ ],
+ [
+ "via",
+ -11.154606819152832
+ ],
+ [
+ "▁engaged",
+ -11.154646873474121
+ ],
+ [
+ "▁parti",
+ -11.154741287231445
+ ],
+ [
+ "▁posting",
+ -11.15517807006836
+ ],
+ [
+ "CO",
+ -11.155484199523926
+ ],
+ [
+ "▁bois",
+ -11.155815124511719
+ ],
+ [
+ "▁inch",
+ -11.15590763092041
+ ],
+ [
+ "vie",
+ -11.156068801879883
+ ],
+ [
+ "▁aside",
+ -11.156314849853516
+ ],
+ [
+ "▁exceptional",
+ -11.15658950805664
+ ],
+ [
+ "▁vintage",
+ -11.156668663024902
+ ],
+ [
+ "▁Him",
+ -11.156795501708984
+ ],
+ [
+ "▁expansion",
+ -11.156806945800781
+ ],
+ [
+ "▁Weg",
+ -11.157122611999512
+ ],
+ [
+ "▁authors",
+ -11.157535552978516
+ ],
+ [
+ "▁deine",
+ -11.15764045715332
+ ],
+ [
+ "▁Prime",
+ -11.158016204833984
+ ],
+ [
+ "▁scan",
+ -11.158055305480957
+ ],
+ [
+ "▁reg",
+ -11.158112525939941
+ ],
+ [
+ "ția",
+ -11.158141136169434
+ ],
+ [
+ "riv",
+ -11.158258438110352
+ ],
+ [
+ "selon",
+ -11.158440589904785
+ ],
+ [
+ "▁Studio",
+ -11.158571243286133
+ ],
+ [
+ "▁dich",
+ -11.158658027648926
+ ],
+ [
+ "▁vi",
+ -11.158745765686035
+ ],
+ [
+ "▁sequence",
+ -11.159016609191895
+ ],
+ [
+ "▁Four",
+ -11.159046173095703
+ ],
+ [
+ "RT",
+ -11.159050941467285
+ ],
+ [
+ "▁ihn",
+ -11.159072875976562
+ ],
+ [
+ "▁employ",
+ -11.159223556518555
+ ],
+ [
+ "umb",
+ -11.159659385681152
+ ],
+ [
+ "ită",
+ -11.159818649291992
+ ],
+ [
+ "▁Station",
+ -11.159950256347656
+ ],
+ [
+ "▁upload",
+ -11.159972190856934
+ ],
+ [
+ "▁upgrade",
+ -11.160445213317871
+ ],
+ [
+ "▁exterior",
+ -11.160528182983398
+ ],
+ [
+ "▁writers",
+ -11.160531997680664
+ ],
+ [
+ "▁plot",
+ -11.160543441772461
+ ],
+ [
+ "▁Gen",
+ -11.16068172454834
+ ],
+ [
+ "TER",
+ -11.160821914672852
+ ],
+ [
+ "-12",
+ -11.160930633544922
+ ],
+ [
+ "http",
+ -11.162168502807617
+ ],
+ [
+ "▁smell",
+ -11.1621732711792
+ ],
+ [
+ "post",
+ -11.162522315979004
+ ],
+ [
+ "von",
+ -11.162790298461914
+ ],
+ [
+ "mili",
+ -11.16280746459961
+ ],
+ [
+ "8%",
+ -11.162972450256348
+ ],
+ [
+ "▁Andrew",
+ -11.163065910339355
+ ],
+ [
+ "▁spun",
+ -11.16321086883545
+ ],
+ [
+ "▁grass",
+ -11.163444519042969
+ ],
+ [
+ "unter",
+ -11.163474082946777
+ ],
+ [
+ "▁burn",
+ -11.16356086730957
+ ],
+ [
+ "▁Gegen",
+ -11.163601875305176
+ ],
+ [
+ "fest",
+ -11.163721084594727
+ ],
+ [
+ "▁Northern",
+ -11.163738250732422
+ ],
+ [
+ "▁consumption",
+ -11.163775444030762
+ ],
+ [
+ "▁bird",
+ -11.164069175720215
+ ],
+ [
+ "▁Miss",
+ -11.164369583129883
+ ],
+ [
+ "anti",
+ -11.16447925567627
+ ],
+ [
+ "▁viata",
+ -11.164583206176758
+ ],
+ [
+ "bereich",
+ -11.164602279663086
+ ],
+ [
+ "▁Change",
+ -11.164871215820312
+ ],
+ [
+ "▁pouvoir",
+ -11.165255546569824
+ ],
+ [
+ "▁demonstrate",
+ -11.165435791015625
+ ],
+ [
+ "▁requirement",
+ -11.165483474731445
+ ],
+ [
+ "BI",
+ -11.16577434539795
+ ],
+ [
+ "ied",
+ -11.166099548339844
+ ],
+ [
+ "▁spray",
+ -11.166358947753906
+ ],
+ [
+ "▁calitate",
+ -11.166379928588867
+ ],
+ [
+ "▁souvent",
+ -11.1665620803833
+ ],
+ [
+ "▁samples",
+ -11.166682243347168
+ ],
+ [
+ "▁compete",
+ -11.166930198669434
+ ],
+ [
+ "ank",
+ -11.166946411132812
+ ],
+ [
+ "année",
+ -11.167037963867188
+ ],
+ [
+ "wick",
+ -11.167183876037598
+ ],
+ [
+ "iff",
+ -11.167254447937012
+ ],
+ [
+ "noi",
+ -11.167255401611328
+ ],
+ [
+ "ography",
+ -11.167450904846191
+ ],
+ [
+ "▁SE",
+ -11.167508125305176
+ ],
+ [
+ "▁250",
+ -11.16779899597168
+ ],
+ [
+ "▁wealth",
+ -11.167884826660156
+ ],
+ [
+ "4%",
+ -11.168235778808594
+ ],
+ [
+ "▁swimming",
+ -11.168269157409668
+ ],
+ [
+ "enne",
+ -11.168338775634766
+ ],
+ [
+ "Qu",
+ -11.168400764465332
+ ],
+ [
+ "▁connections",
+ -11.168476104736328
+ ],
+ [
+ "onne",
+ -11.16852855682373
+ ],
+ [
+ "▁Way",
+ -11.168676376342773
+ ],
+ [
+ "voll",
+ -11.168793678283691
+ ],
+ [
+ "▁extent",
+ -11.169041633605957
+ ],
+ [
+ "▁objective",
+ -11.169572830200195
+ ],
+ [
+ "▁clinic",
+ -11.169581413269043
+ ],
+ [
+ "NA",
+ -11.169848442077637
+ ],
+ [
+ "▁Hope",
+ -11.170098304748535
+ ],
+ [
+ "▁coat",
+ -11.170331954956055
+ ],
+ [
+ "▁depend",
+ -11.170393943786621
+ ],
+ [
+ "▁tine",
+ -11.170463562011719
+ ],
+ [
+ "acc",
+ -11.170486450195312
+ ],
+ [
+ "▁editor",
+ -11.170598983764648
+ ],
+ [
+ "▁Jim",
+ -11.170690536499023
+ ],
+ [
+ "600",
+ -11.171262741088867
+ ],
+ [
+ "▁module",
+ -11.171302795410156
+ ],
+ [
+ "▁deja",
+ -11.171821594238281
+ ],
+ [
+ "atur",
+ -11.171841621398926
+ ],
+ [
+ "▁maintaining",
+ -11.171918869018555
+ ],
+ [
+ "▁hoch",
+ -11.172059059143066
+ ],
+ [
+ "▁covering",
+ -11.17239761352539
+ ],
+ [
+ "vielen",
+ -11.172450065612793
+ ],
+ [
+ "hem",
+ -11.172531127929688
+ ],
+ [
+ "▁illegal",
+ -11.172656059265137
+ ],
+ [
+ "▁certificate",
+ -11.17329216003418
+ ],
+ [
+ "▁collective",
+ -11.173357963562012
+ ],
+ [
+ "▁blow",
+ -11.17343807220459
+ ],
+ [
+ "▁programming",
+ -11.17343807220459
+ ],
+ [
+ "HE",
+ -11.173727989196777
+ ],
+ [
+ "▁Division",
+ -11.173842430114746
+ ],
+ [
+ "▁ceux",
+ -11.174081802368164
+ ],
+ [
+ "▁saved",
+ -11.174202919006348
+ ],
+ [
+ "▁worst",
+ -11.17426586151123
+ ],
+ [
+ "▁arms",
+ -11.17430305480957
+ ],
+ [
+ "▁Officer",
+ -11.17463493347168
+ ],
+ [
+ "▁association",
+ -11.174838066101074
+ ],
+ [
+ "ington",
+ -11.1749906539917
+ ],
+ [
+ "▁belle",
+ -11.175024032592773
+ ],
+ [
+ "tting",
+ -11.17537784576416
+ ],
+ [
+ "▁attacks",
+ -11.175446510314941
+ ],
+ [
+ "▁vei",
+ -11.17546558380127
+ ],
+ [
+ "▁gerade",
+ -11.175470352172852
+ ],
+ [
+ "▁strain",
+ -11.175748825073242
+ ],
+ [
+ "▁offices",
+ -11.1759672164917
+ ],
+ [
+ "EM",
+ -11.17627239227295
+ ],
+ [
+ "EST",
+ -11.176509857177734
+ ],
+ [
+ "-8",
+ -11.176758766174316
+ ],
+ [
+ "▁faculty",
+ -11.176998138427734
+ ],
+ [
+ "▁Plant",
+ -11.177046775817871
+ ],
+ [
+ "pla",
+ -11.177295684814453
+ ],
+ [
+ "card",
+ -11.177618980407715
+ ],
+ [
+ "▁loose",
+ -11.177982330322266
+ ],
+ [
+ "▁PR",
+ -11.178044319152832
+ ],
+ [
+ "profit",
+ -11.178071022033691
+ ],
+ [
+ "▁channels",
+ -11.178119659423828
+ ],
+ [
+ "ATE",
+ -11.178257942199707
+ ],
+ [
+ "atic",
+ -11.178304672241211
+ ],
+ [
+ "wegen",
+ -11.178404808044434
+ ],
+ [
+ "word",
+ -11.178621292114258
+ ],
+ [
+ "▁sehen",
+ -11.178659439086914
+ ],
+ [
+ "▁nombre",
+ -11.178744316101074
+ ],
+ [
+ "▁DO",
+ -11.178763389587402
+ ],
+ [
+ "▁hoping",
+ -11.178949356079102
+ ],
+ [
+ "▁wollen",
+ -11.179091453552246
+ ],
+ [
+ "▁decat",
+ -11.179244995117188
+ ],
+ [
+ "IF",
+ -11.179386138916016
+ ],
+ [
+ "▁permission",
+ -11.179396629333496
+ ],
+ [
+ "▁Williams",
+ -11.179936408996582
+ ],
+ [
+ "▁beer",
+ -11.179962158203125
+ ],
+ [
+ "▁dernière",
+ -11.180052757263184
+ ],
+ [
+ "▁purchasing",
+ -11.18025016784668
+ ],
+ [
+ "▁pride",
+ -11.180416107177734
+ ],
+ [
+ "solv",
+ -11.180598258972168
+ ],
+ [
+ "ego",
+ -11.180691719055176
+ ],
+ [
+ "▁Oil",
+ -11.18079662322998
+ ],
+ [
+ "▁dishes",
+ -11.18102741241455
+ ],
+ [
+ "▁Baby",
+ -11.181109428405762
+ ],
+ [
+ "▁Roll",
+ -11.181137084960938
+ ],
+ [
+ "vez",
+ -11.18134593963623
+ ],
+ [
+ "▁drept",
+ -11.181367874145508
+ ],
+ [
+ "lly",
+ -11.18148136138916
+ ],
+ [
+ "▁potrivit",
+ -11.181495666503906
+ ],
+ [
+ "person",
+ -11.181961059570312
+ ],
+ [
+ "▁interactive",
+ -11.182269096374512
+ ],
+ [
+ "▁brilliant",
+ -11.182304382324219
+ ],
+ [
+ "▁000",
+ -11.182357788085938
+ ],
+ [
+ "▁giant",
+ -11.182657241821289
+ ],
+ [
+ "▁plain",
+ -11.182945251464844
+ ],
+ [
+ "▁lock",
+ -11.183197975158691
+ ],
+ [
+ "▁inspection",
+ -11.183762550354004
+ ],
+ [
+ "▁symbol",
+ -11.18392276763916
+ ],
+ [
+ "▁Gal",
+ -11.183953285217285
+ ],
+ [
+ "▁concepts",
+ -11.1840181350708
+ ],
+ [
+ "▁venture",
+ -11.18411922454834
+ ],
+ [
+ "▁Tr",
+ -11.184402465820312
+ ],
+ [
+ "▁Color",
+ -11.184469223022461
+ ],
+ [
+ "▁behalf",
+ -11.184635162353516
+ ],
+ [
+ "ink",
+ -11.184715270996094
+ ],
+ [
+ "atii",
+ -11.1848726272583
+ ],
+ [
+ "wie",
+ -11.184907913208008
+ ],
+ [
+ "▁stream",
+ -11.18514347076416
+ ],
+ [
+ "▁buyers",
+ -11.185192108154297
+ ],
+ [
+ "legen",
+ -11.185526847839355
+ ],
+ [
+ "iness",
+ -11.18578815460205
+ ],
+ [
+ "▁absolute",
+ -11.185945510864258
+ ],
+ [
+ "▁council",
+ -11.186067581176758
+ ],
+ [
+ "▁displayed",
+ -11.186172485351562
+ ],
+ [
+ "▁Bun",
+ -11.186405181884766
+ ],
+ [
+ "▁darauf",
+ -11.186585426330566
+ ],
+ [
+ "▁rod",
+ -11.186829566955566
+ ],
+ [
+ "▁repeat",
+ -11.186898231506348
+ ],
+ [
+ "quelle",
+ -11.187023162841797
+ ],
+ [
+ "lation",
+ -11.187433242797852
+ ],
+ [
+ "gul",
+ -11.18774700164795
+ ],
+ [
+ "▁compensation",
+ -11.188064575195312
+ ],
+ [
+ "▁string",
+ -11.1881685256958
+ ],
+ [
+ "▁joining",
+ -11.188251495361328
+ ],
+ [
+ "▁Pra",
+ -11.188429832458496
+ ],
+ [
+ "hab",
+ -11.188936233520508
+ ],
+ [
+ "▁plane",
+ -11.189024925231934
+ ],
+ [
+ "▁conversion",
+ -11.189078330993652
+ ],
+ [
+ "▁lesson",
+ -11.189361572265625
+ ],
+ [
+ "bound",
+ -11.1893949508667
+ ],
+ [
+ "▁seats",
+ -11.18946361541748
+ ],
+ [
+ "voc",
+ -11.189902305603027
+ ],
+ [
+ "▁Disney",
+ -11.190120697021484
+ ],
+ [
+ "esse",
+ -11.190277099609375
+ ],
+ [
+ "▁awards",
+ -11.190279006958008
+ ],
+ [
+ "▁initiative",
+ -11.190483093261719
+ ],
+ [
+ "UM",
+ -11.19050407409668
+ ],
+ [
+ "▁intelligence",
+ -11.190763473510742
+ ],
+ [
+ "▁laser",
+ -11.191128730773926
+ ],
+ [
+ "än",
+ -11.191228866577148
+ ],
+ [
+ "▁generated",
+ -11.191231727600098
+ ],
+ [
+ "▁allen",
+ -11.19186782836914
+ ],
+ [
+ "▁Aug",
+ -11.19261360168457
+ ],
+ [
+ "lini",
+ -11.192968368530273
+ ],
+ [
+ "▁Update",
+ -11.193015098571777
+ ],
+ [
+ "▁grab",
+ -11.193095207214355
+ ],
+ [
+ "▁Bridge",
+ -11.193219184875488
+ ],
+ [
+ "rock",
+ -11.193289756774902
+ ],
+ [
+ "hold",
+ -11.193461418151855
+ ],
+ [
+ "seinen",
+ -11.193643569946289
+ ],
+ [
+ "▁false",
+ -11.193758010864258
+ ],
+ [
+ "type",
+ -11.193792343139648
+ ],
+ [
+ "▁outcome",
+ -11.193906784057617
+ ],
+ [
+ "▁crazy",
+ -11.194161415100098
+ ],
+ [
+ "▁Platz",
+ -11.194281578063965
+ ],
+ [
+ "▁believed",
+ -11.194426536560059
+ ],
+ [
+ "▁adjust",
+ -11.194503784179688
+ ],
+ [
+ "▁entrance",
+ -11.194644927978516
+ ],
+ [
+ "▁Colorado",
+ -11.194751739501953
+ ],
+ [
+ "▁concentration",
+ -11.194865226745605
+ ],
+ [
+ "aid",
+ -11.194958686828613
+ ],
+ [
+ "▁regardless",
+ -11.195035934448242
+ ],
+ [
+ "▁mici",
+ -11.195063591003418
+ ],
+ [
+ "▁potentially",
+ -11.195109367370605
+ ],
+ [
+ "▁Custom",
+ -11.195867538452148
+ ],
+ [
+ "rag",
+ -11.196009635925293
+ ],
+ [
+ "▁employer",
+ -11.19604206085205
+ ],
+ [
+ "tagged",
+ -11.196158409118652
+ ],
+ [
+ "▁34",
+ -11.196271896362305
+ ],
+ [
+ "fro",
+ -11.196895599365234
+ ],
+ [
+ "▁Pas",
+ -11.197010040283203
+ ],
+ [
+ "▁AS",
+ -11.197013854980469
+ ],
+ [
+ "PP",
+ -11.197031021118164
+ ],
+ [
+ "stru",
+ -11.19741439819336
+ ],
+ [
+ "grâce",
+ -11.198037147521973
+ ],
+ [
+ "▁anyway",
+ -11.198240280151367
+ ],
+ [
+ "▁streets",
+ -11.1986083984375
+ ],
+ [
+ "▁Region",
+ -11.199190139770508
+ ],
+ [
+ "▁newly",
+ -11.199280738830566
+ ],
+ [
+ "▁assistant",
+ -11.199461936950684
+ ],
+ [
+ "▁requests",
+ -11.199618339538574
+ ],
+ [
+ "▁Ohio",
+ -11.199705123901367
+ ],
+ [
+ "▁continuing",
+ -11.200072288513184
+ ],
+ [
+ "▁îm",
+ -11.200136184692383
+ ],
+ [
+ "7%",
+ -11.20031452178955
+ ],
+ [
+ "▁basically",
+ -11.200325965881348
+ ],
+ [
+ "gabe",
+ -11.200334548950195
+ ],
+ [
+ "▁ultra",
+ -11.200355529785156
+ ],
+ [
+ "pic",
+ -11.200571060180664
+ ],
+ [
+ "▁jeder",
+ -11.200939178466797
+ ],
+ [
+ "▁Cook",
+ -11.201225280761719
+ ],
+ [
+ "▁tie",
+ -11.201227188110352
+ ],
+ [
+ "▁yard",
+ -11.20151424407959
+ ],
+ [
+ "▁wash",
+ -11.20152759552002
+ ],
+ [
+ "▁3,",
+ -11.20194149017334
+ ],
+ [
+ "▁exista",
+ -11.202128410339355
+ ],
+ [
+ "▁egg",
+ -11.202342987060547
+ ],
+ [
+ "▁marché",
+ -11.202616691589355
+ ],
+ [
+ "kommen",
+ -11.202630996704102
+ ],
+ [
+ "▁Select",
+ -11.202999114990234
+ ],
+ [
+ "geben",
+ -11.203126907348633
+ ],
+ [
+ "▁Joseph",
+ -11.203531265258789
+ ],
+ [
+ "▁Ces",
+ -11.203642845153809
+ ],
+ [
+ "▁hundred",
+ -11.203676223754883
+ ],
+ [
+ "even",
+ -11.203792572021484
+ ],
+ [
+ "gal",
+ -11.204232215881348
+ ],
+ [
+ "800",
+ -11.20443058013916
+ ],
+ [
+ "▁Jones",
+ -11.204599380493164
+ ],
+ [
+ "ova",
+ -11.204681396484375
+ ],
+ [
+ "▁careful",
+ -11.204727172851562
+ ],
+ [
+ "▁alarm",
+ -11.205070495605469
+ ],
+ [
+ "NI",
+ -11.205113410949707
+ ],
+ [
+ "▁residence",
+ -11.205327987670898
+ ],
+ [
+ "▁wäre",
+ -11.20590877532959
+ ],
+ [
+ "▁Dor",
+ -11.205986976623535
+ ],
+ [
+ "▁amounts",
+ -11.206369400024414
+ ],
+ [
+ "▁mistake",
+ -11.206687927246094
+ ],
+ [
+ "ates",
+ -11.206796646118164
+ ],
+ [
+ "▁bune",
+ -11.206951141357422
+ ],
+ [
+ "▁vegetables",
+ -11.207124710083008
+ ],
+ [
+ "▁Ann",
+ -11.207204818725586
+ ],
+ [
+ "logical",
+ -11.20776081085205
+ ],
+ [
+ "stadt",
+ -11.207806587219238
+ ],
+ [
+ "▁chances",
+ -11.207921981811523
+ ],
+ [
+ "%)",
+ -11.208030700683594
+ ],
+ [
+ "▁minimal",
+ -11.20810604095459
+ ],
+ [
+ "▁naturally",
+ -11.20817756652832
+ ],
+ [
+ "▁Geld",
+ -11.20822525024414
+ ],
+ [
+ "▁Yu",
+ -11.208361625671387
+ ],
+ [
+ "▁wrap",
+ -11.20840072631836
+ ],
+ [
+ "rest",
+ -11.208674430847168
+ ],
+ [
+ "▁legs",
+ -11.208758354187012
+ ],
+ [
+ "PM",
+ -11.208806991577148
+ ],
+ [
+ "▁Heart",
+ -11.208888053894043
+ ],
+ [
+ "▁suspect",
+ -11.209020614624023
+ ],
+ [
+ "Go",
+ -11.209098815917969
+ ],
+ [
+ "▁Fil",
+ -11.209175109863281
+ ],
+ [
+ "▁YOU",
+ -11.209175109863281
+ ],
+ [
+ "▁victory",
+ -11.209245681762695
+ ],
+ [
+ "pun",
+ -11.20960807800293
+ ],
+ [
+ "▁Zo",
+ -11.209632873535156
+ ],
+ [
+ "CT",
+ -11.209640502929688
+ ],
+ [
+ "▁trim",
+ -11.20969009399414
+ ],
+ [
+ "▁stuck",
+ -11.209836959838867
+ ],
+ [
+ "ators",
+ -11.209877014160156
+ ],
+ [
+ "▁Ideas",
+ -11.210016250610352
+ ],
+ [
+ "▁voyage",
+ -11.210166931152344
+ ],
+ [
+ "▁Restaurant",
+ -11.210205078125
+ ],
+ [
+ "▁pat",
+ -11.210234642028809
+ ],
+ [
+ "▁bond",
+ -11.210521697998047
+ ],
+ [
+ "▁Del",
+ -11.210552215576172
+ ],
+ [
+ "▁fighting",
+ -11.210705757141113
+ ],
+ [
+ "▁concerning",
+ -11.210867881774902
+ ],
+ [
+ "▁etwa",
+ -11.211141586303711
+ ],
+ [
+ "▁Thema",
+ -11.211237907409668
+ ],
+ [
+ "▁preferred",
+ -11.211423873901367
+ ],
+ [
+ "▁pitch",
+ -11.211465835571289
+ ],
+ [
+ "▁Singapore",
+ -11.211971282958984
+ ],
+ [
+ "▁tub",
+ -11.212018013000488
+ ],
+ [
+ "FT",
+ -11.212053298950195
+ ],
+ [
+ "▁Product",
+ -11.21212100982666
+ ],
+ [
+ "▁applying",
+ -11.212285995483398
+ ],
+ [
+ "▁Fr",
+ -11.212340354919434
+ ],
+ [
+ "ţa",
+ -11.212599754333496
+ ],
+ [
+ "▁iPad",
+ -11.212861061096191
+ ],
+ [
+ "PD",
+ -11.2129545211792
+ ],
+ [
+ "▁comun",
+ -11.212995529174805
+ ],
+ [
+ "▁pie",
+ -11.213286399841309
+ ],
+ [
+ "rank",
+ -11.21364688873291
+ ],
+ [
+ "tron",
+ -11.213677406311035
+ ],
+ [
+ "▁pest",
+ -11.213906288146973
+ ],
+ [
+ "▁herself",
+ -11.213936805725098
+ ],
+ [
+ "▁intense",
+ -11.213964462280273
+ ],
+ [
+ "foot",
+ -11.21413803100586
+ ],
+ [
+ "▁1998",
+ -11.2141695022583
+ ],
+ [
+ "▁anxiety",
+ -11.214616775512695
+ ],
+ [
+ "▁portable",
+ -11.214674949645996
+ ],
+ [
+ "▁harm",
+ -11.214735984802246
+ ],
+ [
+ "▁admit",
+ -11.214885711669922
+ ],
+ [
+ "sted",
+ -11.214900016784668
+ ],
+ [
+ "▁regions",
+ -11.215450286865234
+ ],
+ [
+ "cie",
+ -11.215556144714355
+ ],
+ [
+ "▁robust",
+ -11.21577262878418
+ ],
+ [
+ "▁stem",
+ -11.215982437133789
+ ],
+ [
+ "▁roles",
+ -11.216024398803711
+ ],
+ [
+ "▁Latin",
+ -11.216224670410156
+ ],
+ [
+ "▁Ré",
+ -11.216378211975098
+ ],
+ [
+ "▁ref",
+ -11.216381072998047
+ ],
+ [
+ "isme",
+ -11.216426849365234
+ ],
+ [
+ "▁contribution",
+ -11.216776847839355
+ ],
+ [
+ "▁forever",
+ -11.217447280883789
+ ],
+ [
+ "▁frei",
+ -11.21754264831543
+ ],
+ [
+ "▁mont",
+ -11.217818260192871
+ ],
+ [
+ "that",
+ -11.217999458312988
+ ],
+ [
+ "▁sensitive",
+ -11.218116760253906
+ ],
+ [
+ "▁wider",
+ -11.218175888061523
+ ],
+ [
+ "AF",
+ -11.218234062194824
+ ],
+ [
+ "▁liability",
+ -11.218748092651367
+ ],
+ [
+ "ţiei",
+ -11.219043731689453
+ ],
+ [
+ "▁Cho",
+ -11.219260215759277
+ ],
+ [
+ "aria",
+ -11.21960735321045
+ ],
+ [
+ "rang",
+ -11.21977710723877
+ ],
+ [
+ "▁Account",
+ -11.21986198425293
+ ],
+ [
+ "▁III",
+ -11.219941139221191
+ ],
+ [
+ "▁tooth",
+ -11.220222473144531
+ ],
+ [
+ "▁factory",
+ -11.220240592956543
+ ],
+ [
+ "▁dropped",
+ -11.220495223999023
+ ],
+ [
+ "horn",
+ -11.220780372619629
+ ],
+ [
+ "RP",
+ -11.221110343933105
+ ],
+ [
+ "▁container",
+ -11.22118091583252
+ ],
+ [
+ "fran",
+ -11.221474647521973
+ ],
+ [
+ "▁lawyer",
+ -11.221842765808105
+ ],
+ [
+ "▁Image",
+ -11.221907615661621
+ ],
+ [
+ "HO",
+ -11.22195816040039
+ ],
+ [
+ "▁incorporate",
+ -11.221992492675781
+ ],
+ [
+ "▁lume",
+ -11.22226333618164
+ ],
+ [
+ "GA",
+ -11.222331047058105
+ ],
+ [
+ "itati",
+ -11.222370147705078
+ ],
+ [
+ "autre",
+ -11.222665786743164
+ ],
+ [
+ "ierten",
+ -11.222688674926758
+ ],
+ [
+ "[",
+ -11.222746849060059
+ ],
+ [
+ "▁packages",
+ -11.222758293151855
+ ],
+ [
+ "▁Simon",
+ -11.22290325164795
+ ],
+ [
+ "▁somewhat",
+ -11.223734855651855
+ ],
+ [
+ "mbo",
+ -11.223737716674805
+ ],
+ [
+ "lite",
+ -11.223844528198242
+ ],
+ [
+ "▁eliminate",
+ -11.22395133972168
+ ],
+ [
+ "▁decrease",
+ -11.224117279052734
+ ],
+ [
+ "▁geben",
+ -11.224214553833008
+ ],
+ [
+ "▁approaches",
+ -11.224482536315918
+ ],
+ [
+ "▁tissue",
+ -11.224940299987793
+ ],
+ [
+ "▁personne",
+ -11.225192070007324
+ ],
+ [
+ "ional",
+ -11.225587844848633
+ ],
+ [
+ "unable",
+ -11.2256498336792
+ ],
+ [
+ "▁Case",
+ -11.225736618041992
+ ],
+ [
+ "hill",
+ -11.225744247436523
+ ],
+ [
+ "och",
+ -11.225862503051758
+ ],
+ [
+ "▁minister",
+ -11.225920677185059
+ ],
+ [
+ "▁Rad",
+ -11.226285934448242
+ ],
+ [
+ "▁yoga",
+ -11.226390838623047
+ ],
+ [
+ "▁encounter",
+ -11.22661018371582
+ ],
+ [
+ "text",
+ -11.22670841217041
+ ],
+ [
+ "▁OS",
+ -11.226719856262207
+ ],
+ [
+ "▁opera",
+ -11.22673225402832
+ ],
+ [
+ "▁loving",
+ -11.226977348327637
+ ],
+ [
+ "▁birds",
+ -11.227363586425781
+ ],
+ [
+ "▁prim",
+ -11.227389335632324
+ ],
+ [
+ "easca",
+ -11.227432250976562
+ ],
+ [
+ "park",
+ -11.227453231811523
+ ],
+ [
+ "fü",
+ -11.227797508239746
+ ],
+ [
+ "▁champion",
+ -11.227824211120605
+ ],
+ [
+ "▁warning",
+ -11.228245735168457
+ ],
+ [
+ "DC",
+ -11.228271484375
+ ],
+ [
+ "▁yield",
+ -11.228310585021973
+ ],
+ [
+ "raum",
+ -11.228334426879883
+ ],
+ [
+ "▁Student",
+ -11.228434562683105
+ ],
+ [
+ "▁Rev",
+ -11.22848892211914
+ ],
+ [
+ "▁Fu",
+ -11.228501319885254
+ ],
+ [
+ "▁intra",
+ -11.22854232788086
+ ],
+ [
+ "▁proces",
+ -11.228585243225098
+ ],
+ [
+ "▁margin",
+ -11.228621482849121
+ ],
+ [
+ "lands",
+ -11.228816986083984
+ ],
+ [
+ "04",
+ -11.228952407836914
+ ],
+ [
+ "▁Steel",
+ -11.229897499084473
+ ],
+ [
+ "▁besoin",
+ -11.230081558227539
+ ],
+ [
+ "şti",
+ -11.230561256408691
+ ],
+ [
+ "▁39",
+ -11.230635643005371
+ ],
+ [
+ "▁outcomes",
+ -11.230677604675293
+ ],
+ [
+ "wert",
+ -11.230719566345215
+ ],
+ [
+ "3,",
+ -11.23080062866211
+ ],
+ [
+ "▁hole",
+ -11.230888366699219
+ ],
+ [
+ "▁Create",
+ -11.23096752166748
+ ],
+ [
+ "▁hall",
+ -11.231266975402832
+ ],
+ [
+ "nach",
+ -11.231595039367676
+ ],
+ [
+ "▁indicate",
+ -11.232311248779297
+ ],
+ [
+ "cum",
+ -11.232604026794434
+ ],
+ [
+ "▁Mann",
+ -11.232690811157227
+ ],
+ [
+ "▁reaction",
+ -11.232828140258789
+ ],
+ [
+ "▁empty",
+ -11.23289680480957
+ ],
+ [
+ "▁Sign",
+ -11.232941627502441
+ ],
+ [
+ "▁pm",
+ -11.23300838470459
+ ],
+ [
+ "erung",
+ -11.23322582244873
+ ],
+ [
+ "▁würde",
+ -11.233592987060547
+ ],
+ [
+ "▁declarat",
+ -11.233602523803711
+ ],
+ [
+ "6%",
+ -11.23371410369873
+ ],
+ [
+ "▁Client",
+ -11.23377513885498
+ ],
+ [
+ "vil",
+ -11.234295845031738
+ ],
+ [
+ "▁electricity",
+ -11.234469413757324
+ ],
+ [
+ "▁75",
+ -11.234505653381348
+ ],
+ [
+ "▁buna",
+ -11.234505653381348
+ ],
+ [
+ "eşte",
+ -11.23473834991455
+ ],
+ [
+ "▁prop",
+ -11.234792709350586
+ ],
+ [
+ "▁journal",
+ -11.234883308410645
+ ],
+ [
+ "▁meu",
+ -11.23495101928711
+ ],
+ [
+ "▁chef",
+ -11.235034942626953
+ ],
+ [
+ "▁Ever",
+ -11.235102653503418
+ ],
+ [
+ "▁feelings",
+ -11.235466003417969
+ ],
+ [
+ "PT",
+ -11.23551082611084
+ ],
+ [
+ "▁proposal",
+ -11.235651969909668
+ ],
+ [
+ "▁Its",
+ -11.235709190368652
+ ],
+ [
+ "▁2013.",
+ -11.235795974731445
+ ],
+ [
+ "▁Bundes",
+ -11.23595142364502
+ ],
+ [
+ "▁droit",
+ -11.236333847045898
+ ],
+ [
+ "▁10%",
+ -11.236671447753906
+ ],
+ [
+ "gard",
+ -11.236772537231445
+ ],
+ [
+ "information",
+ -11.236814498901367
+ ],
+ [
+ "FE",
+ -11.237309455871582
+ ],
+ [
+ "▁Dun",
+ -11.237340927124023
+ ],
+ [
+ "▁Stock",
+ -11.237472534179688
+ ],
+ [
+ "ație",
+ -11.2374849319458
+ ],
+ [
+ "▁mag",
+ -11.237603187561035
+ ],
+ [
+ "▁br",
+ -11.237665176391602
+ ],
+ [
+ "▁sight",
+ -11.237772941589355
+ ],
+ [
+ "phone",
+ -11.237796783447266
+ ],
+ [
+ "▁Cy",
+ -11.237811088562012
+ ],
+ [
+ "▁opposite",
+ -11.238035202026367
+ ],
+ [
+ "ically",
+ -11.238235473632812
+ ],
+ [
+ "großen",
+ -11.238388061523438
+ ],
+ [
+ "▁Without",
+ -11.23845100402832
+ ],
+ [
+ "espace",
+ -11.238515853881836
+ ],
+ [
+ "▁chairs",
+ -11.238595008850098
+ ],
+ [
+ "▁matches",
+ -11.238685607910156
+ ],
+ [
+ "ateur",
+ -11.238697052001953
+ ],
+ [
+ "▁Cost",
+ -11.238699913024902
+ ],
+ [
+ "▁WordPress",
+ -11.238880157470703
+ ],
+ [
+ "▁Opera",
+ -11.239195823669434
+ ],
+ [
+ "walked",
+ -11.239234924316406
+ ],
+ [
+ "▁transactions",
+ -11.239521026611328
+ ],
+ [
+ "▁nuclear",
+ -11.239579200744629
+ ],
+ [
+ "ways",
+ -11.239594459533691
+ ],
+ [
+ "▁Oct",
+ -11.239738464355469
+ ],
+ [
+ "▁bomb",
+ -11.239835739135742
+ ],
+ [
+ "▁tracking",
+ -11.239879608154297
+ ],
+ [
+ "▁photograph",
+ -11.240066528320312
+ ],
+ [
+ "bio",
+ -11.240309715270996
+ ],
+ [
+ "▁branch",
+ -11.240363121032715
+ ],
+ [
+ "▁$5",
+ -11.240684509277344
+ ],
+ [
+ "▁diagram",
+ -11.240986824035645
+ ],
+ [
+ "▁Hard",
+ -11.241218566894531
+ ],
+ [
+ "bach",
+ -11.241232872009277
+ ],
+ [
+ "▁42",
+ -11.241249084472656
+ ],
+ [
+ "logy",
+ -11.241472244262695
+ ],
+ [
+ "▁tile",
+ -11.241593360900879
+ ],
+ [
+ "▁API",
+ -11.241833686828613
+ ],
+ [
+ "seront",
+ -11.24204158782959
+ ],
+ [
+ "ENT",
+ -11.242156982421875
+ ],
+ [
+ "▁accommodation",
+ -11.242409706115723
+ ],
+ [
+ "▁fiber",
+ -11.242438316345215
+ ],
+ [
+ "▁Give",
+ -11.242792129516602
+ ],
+ [
+ "▁Gas",
+ -11.242916107177734
+ ],
+ [
+ "▁Spain",
+ -11.243086814880371
+ ],
+ [
+ "▁listing",
+ -11.24312686920166
+ ],
+ [
+ "▁blocks",
+ -11.24349308013916
+ ],
+ [
+ "▁constitu",
+ -11.243762969970703
+ ],
+ [
+ "▁convenience",
+ -11.243797302246094
+ ],
+ [
+ "▁prize",
+ -11.243823051452637
+ ],
+ [
+ "▁aircraft",
+ -11.24404239654541
+ ],
+ [
+ "containing",
+ -11.244124412536621
+ ],
+ [
+ "▁vice",
+ -11.244247436523438
+ ],
+ [
+ "▁organisations",
+ -11.244304656982422
+ ],
+ [
+ "▁complicated",
+ -11.244588851928711
+ ],
+ [
+ "rons",
+ -11.244647979736328
+ ],
+ [
+ "▁bars",
+ -11.244670867919922
+ ],
+ [
+ "était",
+ -11.244705200195312
+ ],
+ [
+ "▁checking",
+ -11.245287895202637
+ ],
+ [
+ "vant",
+ -11.245542526245117
+ ],
+ [
+ "▁couch",
+ -11.245657920837402
+ ],
+ [
+ "▁brush",
+ -11.245870590209961
+ ],
+ [
+ "▁printer",
+ -11.245922088623047
+ ],
+ [
+ "▁Rat",
+ -11.246051788330078
+ ],
+ [
+ "▁announce",
+ -11.246057510375977
+ ],
+ [
+ "▁salari",
+ -11.246200561523438
+ ],
+ [
+ "▁Sk",
+ -11.246356964111328
+ ],
+ [
+ "pal",
+ -11.246383666992188
+ ],
+ [
+ "▁yards",
+ -11.24658203125
+ ],
+ [
+ "▁flexibility",
+ -11.246652603149414
+ ],
+ [
+ "▁jamais",
+ -11.24670696258545
+ ],
+ [
+ "UC",
+ -11.246740341186523
+ ],
+ [
+ "▁4,",
+ -11.246793746948242
+ ],
+ [
+ "▁Made",
+ -11.247078895568848
+ ],
+ [
+ "▁solche",
+ -11.247113227844238
+ ],
+ [
+ "▁tri",
+ -11.247237205505371
+ ],
+ [
+ "▁outfit",
+ -11.247243881225586
+ ],
+ [
+ "м",
+ -11.247267723083496
+ ],
+ [
+ "▁encouraged",
+ -11.247477531433105
+ ],
+ [
+ "trac",
+ -11.247552871704102
+ ],
+ [
+ "▁genetic",
+ -11.24755859375
+ ],
+ [
+ "▁beneficial",
+ -11.247747421264648
+ ],
+ [
+ "mă",
+ -11.247849464416504
+ ],
+ [
+ "involving",
+ -11.247879028320312
+ ],
+ [
+ "▁knee",
+ -11.247879028320312
+ ],
+ [
+ "▁respective",
+ -11.248316764831543
+ ],
+ [
+ "▁controlled",
+ -11.248350143432617
+ ],
+ [
+ "▁Rück",
+ -11.24837589263916
+ ],
+ [
+ "LC",
+ -11.248592376708984
+ ],
+ [
+ "▁highlight",
+ -11.248634338378906
+ ],
+ [
+ "chem",
+ -11.248797416687012
+ ],
+ [
+ "▁Bis",
+ -11.24956226348877
+ ],
+ [
+ "▁graphics",
+ -11.249592781066895
+ ],
+ [
+ "▁posibil",
+ -11.249672889709473
+ ],
+ [
+ "orul",
+ -11.249682426452637
+ ],
+ [
+ "imagin",
+ -11.249836921691895
+ ],
+ [
+ "▁draft",
+ -11.250006675720215
+ ],
+ [
+ "shaped",
+ -11.250219345092773
+ ],
+ [
+ "▁suggests",
+ -11.250221252441406
+ ],
+ [
+ "uvre",
+ -11.250509262084961
+ ],
+ [
+ "page",
+ -11.250545501708984
+ ],
+ [
+ "▁sentiment",
+ -11.250685691833496
+ ],
+ [
+ "▁loop",
+ -11.251015663146973
+ ],
+ [
+ "▁Quality",
+ -11.251839637756348
+ ],
+ [
+ "▁volunteers",
+ -11.251869201660156
+ ],
+ [
+ "▁representation",
+ -11.251923561096191
+ ],
+ [
+ "▁examination",
+ -11.252134323120117
+ ],
+ [
+ "▁(2)",
+ -11.252225875854492
+ ],
+ [
+ "assi",
+ -11.252435684204102
+ ],
+ [
+ "▁till",
+ -11.252486228942871
+ ],
+ [
+ "▁Catholic",
+ -11.252618789672852
+ ],
+ [
+ "▁2020",
+ -11.252726554870605
+ ],
+ [
+ "▁random",
+ -11.252764701843262
+ ],
+ [
+ "tage",
+ -11.253146171569824
+ ],
+ [
+ "▁baking",
+ -11.253690719604492
+ ],
+ [
+ "▁Musik",
+ -11.253852844238281
+ ],
+ [
+ "▁SC",
+ -11.253867149353027
+ ],
+ [
+ "▁möchte",
+ -11.254390716552734
+ ],
+ [
+ "▁gene",
+ -11.254411697387695
+ ],
+ [
+ "▁kam",
+ -11.254928588867188
+ ],
+ [
+ "▁inspire",
+ -11.254974365234375
+ ],
+ [
+ "unk",
+ -11.255097389221191
+ ],
+ [
+ "▁Final",
+ -11.255477905273438
+ ],
+ [
+ "▁jeden",
+ -11.255497932434082
+ ],
+ [
+ "▁LLC",
+ -11.255962371826172
+ ],
+ [
+ "▁sistem",
+ -11.25613784790039
+ ],
+ [
+ "▁stages",
+ -11.256441116333008
+ ],
+ [
+ "▁texture",
+ -11.256613731384277
+ ],
+ [
+ "rib",
+ -11.256739616394043
+ ],
+ [
+ "lung",
+ -11.256782531738281
+ ],
+ [
+ "▁breath",
+ -11.256814002990723
+ ],
+ [
+ "▁hosted",
+ -11.256844520568848
+ ],
+ [
+ "▁Kingdom",
+ -11.257079124450684
+ ],
+ [
+ "▁politics",
+ -11.257121086120605
+ ],
+ [
+ "▁mood",
+ -11.257122993469238
+ ],
+ [
+ "cam",
+ -11.257285118103027
+ ],
+ [
+ "▁liked",
+ -11.257287979125977
+ ],
+ [
+ "▁Credit",
+ -11.257304191589355
+ ],
+ [
+ "tisch",
+ -11.257527351379395
+ ],
+ [
+ "▁everywhere",
+ -11.257692337036133
+ ],
+ [
+ "▁poti",
+ -11.257915496826172
+ ],
+ [
+ "▁fruits",
+ -11.258264541625977
+ ],
+ [
+ "oire",
+ -11.258322715759277
+ ],
+ [
+ "▁mesure",
+ -11.258586883544922
+ ],
+ [
+ "▁Studies",
+ -11.258838653564453
+ ],
+ [
+ "▁provision",
+ -11.25888729095459
+ ],
+ [
+ "▁Maria",
+ -11.258927345275879
+ ],
+ [
+ "▁necessarily",
+ -11.259103775024414
+ ],
+ [
+ "▁Net",
+ -11.259212493896484
+ ],
+ [
+ "▁scar",
+ -11.259307861328125
+ ],
+ [
+ "▁tracks",
+ -11.259424209594727
+ ],
+ [
+ "▁ads",
+ -11.259856224060059
+ ],
+ [
+ "termin",
+ -11.259861946105957
+ ],
+ [
+ "▁Yo",
+ -11.26022720336914
+ ],
+ [
+ "atory",
+ -11.260252952575684
+ ],
+ [
+ "itoare",
+ -11.26025676727295
+ ],
+ [
+ "▁colours",
+ -11.260563850402832
+ ],
+ [
+ "▁correctly",
+ -11.260817527770996
+ ],
+ [
+ "▁Trade",
+ -11.26090145111084
+ ],
+ [
+ "▁Week",
+ -11.261052131652832
+ ],
+ [
+ "▁Premier",
+ -11.261499404907227
+ ],
+ [
+ "▁designers",
+ -11.261600494384766
+ ],
+ [
+ "▁BE",
+ -11.261879920959473
+ ],
+ [
+ "▁desktop",
+ -11.261929512023926
+ ],
+ [
+ "▁lifetime",
+ -11.262046813964844
+ ],
+ [
+ "▁Kind",
+ -11.26213264465332
+ ],
+ [
+ "▁divers",
+ -11.262246131896973
+ ],
+ [
+ "rain",
+ -11.262260437011719
+ ],
+ [
+ "▁Von",
+ -11.262263298034668
+ ],
+ [
+ "▁bal",
+ -11.262568473815918
+ ],
+ [
+ "▁shots",
+ -11.262624740600586
+ ],
+ [
+ "▁accommodate",
+ -11.262767791748047
+ ],
+ [
+ "▁Paper",
+ -11.263001441955566
+ ],
+ [
+ "▁interaction",
+ -11.263191223144531
+ ],
+ [
+ "▁acquisition",
+ -11.263233184814453
+ ],
+ [
+ "▁neuro",
+ -11.26378345489502
+ ],
+ [
+ "▁institution",
+ -11.26391887664795
+ ],
+ [
+ "▁automatic",
+ -11.26403522491455
+ ],
+ [
+ "▁assess",
+ -11.264177322387695
+ ],
+ [
+ "▁manifest",
+ -11.264199256896973
+ ],
+ [
+ "▁audit",
+ -11.264202117919922
+ ],
+ [
+ "▁câte",
+ -11.264406204223633
+ ],
+ [
+ "▁insight",
+ -11.264533996582031
+ ],
+ [
+ "▁lange",
+ -11.264781951904297
+ ],
+ [
+ "▁retirement",
+ -11.264795303344727
+ ],
+ [
+ "sons",
+ -11.264864921569824
+ ],
+ [
+ "▁Asian",
+ -11.26492691040039
+ ],
+ [
+ "▁rail",
+ -11.264978408813477
+ ],
+ [
+ "▁Awards",
+ -11.264982223510742
+ ],
+ [
+ "Avec",
+ -11.265035629272461
+ ],
+ [
+ "SO",
+ -11.26511287689209
+ ],
+ [
+ "para",
+ -11.265304565429688
+ ],
+ [
+ "▁tant",
+ -11.265562057495117
+ ],
+ [
+ "▁strike",
+ -11.265693664550781
+ ],
+ [
+ "▁transformation",
+ -11.265742301940918
+ ],
+ [
+ "▁leicht",
+ -11.26586627960205
+ ],
+ [
+ "л",
+ -11.265996932983398
+ ],
+ [
+ "fat",
+ -11.26629638671875
+ ],
+ [
+ "▁Qui",
+ -11.266626358032227
+ ],
+ [
+ "▁chip",
+ -11.26663589477539
+ ],
+ [
+ "titude",
+ -11.266640663146973
+ ],
+ [
+ "▁Projekt",
+ -11.266998291015625
+ ],
+ [
+ "▁statt",
+ -11.267010688781738
+ ],
+ [
+ "▁findet",
+ -11.267184257507324
+ ],
+ [
+ "▁telephone",
+ -11.267251968383789
+ ],
+ [
+ "▁staying",
+ -11.267267227172852
+ ],
+ [
+ "▁Mess",
+ -11.267353057861328
+ ],
+ [
+ "▁patio",
+ -11.267382621765137
+ ],
+ [
+ "▁afla",
+ -11.267890930175781
+ ],
+ [
+ "▁administrative",
+ -11.267910957336426
+ ],
+ [
+ "▁gemeinsam",
+ -11.268129348754883
+ ],
+ [
+ "▁suppliers",
+ -11.268136024475098
+ ],
+ [
+ "ark",
+ -11.268181800842285
+ ],
+ [
+ "▁rice",
+ -11.268397331237793
+ ],
+ [
+ "▁stretch",
+ -11.268439292907715
+ ],
+ [
+ "▁compact",
+ -11.268651008605957
+ ],
+ [
+ "fire",
+ -11.268756866455078
+ ],
+ [
+ "в",
+ -11.268963813781738
+ ],
+ [
+ "vision",
+ -11.269035339355469
+ ],
+ [
+ "▁Mag",
+ -11.269368171691895
+ ],
+ [
+ "▁dreams",
+ -11.269472122192383
+ ],
+ [
+ "▁funny",
+ -11.26968765258789
+ ],
+ [
+ "▁lässt",
+ -11.270216941833496
+ ],
+ [
+ "cade",
+ -11.270448684692383
+ ],
+ [
+ "▁drama",
+ -11.270484924316406
+ ],
+ [
+ "▁schimb",
+ -11.270767211914062
+ ],
+ [
+ "PO",
+ -11.270785331726074
+ ],
+ [
+ "▁Sim",
+ -11.270806312561035
+ ],
+ [
+ "▁motivation",
+ -11.271045684814453
+ ],
+ [
+ "▁presents",
+ -11.27138614654541
+ ],
+ [
+ "▁1997",
+ -11.271828651428223
+ ],
+ [
+ "agi",
+ -11.271883010864258
+ ],
+ [
+ "▁optimal",
+ -11.27198314666748
+ ],
+ [
+ "▁folder",
+ -11.271995544433594
+ ],
+ [
+ "stro",
+ -11.272034645080566
+ ],
+ [
+ "▁Han",
+ -11.272072792053223
+ ],
+ [
+ "▁Ei",
+ -11.27220344543457
+ ],
+ [
+ "▁pus",
+ -11.272356986999512
+ ],
+ [
+ "▁Learning",
+ -11.272531509399414
+ ],
+ [
+ "oop",
+ -11.272603034973145
+ ],
+ [
+ "▁Type",
+ -11.272658348083496
+ ],
+ [
+ "space",
+ -11.272665023803711
+ ],
+ [
+ "▁define",
+ -11.273098945617676
+ ],
+ [
+ "▁plug",
+ -11.273098945617676
+ ],
+ [
+ "yard",
+ -11.273188591003418
+ ],
+ [
+ "▁utility",
+ -11.273297309875488
+ ],
+ [
+ "über",
+ -11.273561477661133
+ ],
+ [
+ "▁commun",
+ -11.273627281188965
+ ],
+ [
+ "▁directed",
+ -11.273842811584473
+ ],
+ [
+ "▁consent",
+ -11.273893356323242
+ ],
+ [
+ "▁DNA",
+ -11.274068832397461
+ ],
+ [
+ "▁statements",
+ -11.274130821228027
+ ],
+ [
+ "real",
+ -11.274298667907715
+ ],
+ [
+ "active",
+ -11.274430274963379
+ ],
+ [
+ "school",
+ -11.274965286254883
+ ],
+ [
+ "▁mic",
+ -11.275360107421875
+ ],
+ [
+ "▁acestui",
+ -11.275467872619629
+ ],
+ [
+ "scale",
+ -11.27550220489502
+ ],
+ [
+ "▁Mid",
+ -11.275628089904785
+ ],
+ [
+ "▁Chair",
+ -11.275874137878418
+ ],
+ [
+ "к",
+ -11.275936126708984
+ ],
+ [
+ "▁Bas",
+ -11.27630615234375
+ ],
+ [
+ "▁38",
+ -11.276379585266113
+ ],
+ [
+ "erin",
+ -11.276461601257324
+ ],
+ [
+ "▁Everyone",
+ -11.27686882019043
+ ],
+ [
+ "COM",
+ -11.276907920837402
+ ],
+ [
+ "▁chronic",
+ -11.277079582214355
+ ],
+ [
+ "▁doctors",
+ -11.277222633361816
+ ],
+ [
+ "▁sh",
+ -11.277276039123535
+ ],
+ [
+ "sport",
+ -11.27740478515625
+ ],
+ [
+ "▁volunteer",
+ -11.277512550354004
+ ],
+ [
+ "▁drinking",
+ -11.277839660644531
+ ],
+ [
+ "▁Mas",
+ -11.277868270874023
+ ],
+ [
+ "▁pursue",
+ -11.2780122756958
+ ],
+ [
+ "▁exposed",
+ -11.278536796569824
+ ],
+ [
+ "exe",
+ -11.278660774230957
+ ],
+ [
+ "hung",
+ -11.278841972351074
+ ],
+ [
+ "▁Tier",
+ -11.278921127319336
+ ],
+ [
+ "▁plac",
+ -11.279121398925781
+ ],
+ [
+ "▁proiect",
+ -11.279136657714844
+ ],
+ [
+ "▁literally",
+ -11.279288291931152
+ ],
+ [
+ "▁acolo",
+ -11.279412269592285
+ ],
+ [
+ "▁User",
+ -11.279485702514648
+ ],
+ [
+ "UT",
+ -11.279598236083984
+ ],
+ [
+ "▁hyper",
+ -11.279623985290527
+ ],
+ [
+ "▁seed",
+ -11.279794692993164
+ ],
+ [
+ "▁literature",
+ -11.2802734375
+ ],
+ [
+ "▁Holy",
+ -11.280373573303223
+ ],
+ [
+ "▁jeu",
+ -11.280396461486816
+ ],
+ [
+ "▁licensed",
+ -11.280896186828613
+ ],
+ [
+ "station",
+ -11.280900955200195
+ ],
+ [
+ "▁criteria",
+ -11.281292915344238
+ ],
+ [
+ "▁sufficient",
+ -11.281292915344238
+ ],
+ [
+ "▁gestion",
+ -11.281512260437012
+ ],
+ [
+ "▁pic",
+ -11.281549453735352
+ ],
+ [
+ "▁64",
+ -11.28170108795166
+ ],
+ [
+ "▁facts",
+ -11.281905174255371
+ ],
+ [
+ "▁Bild",
+ -11.282098770141602
+ ],
+ [
+ "obi",
+ -11.28212833404541
+ ],
+ [
+ "▁nie",
+ -11.282362937927246
+ ],
+ [
+ "▁Jewish",
+ -11.282756805419922
+ ],
+ [
+ "bor",
+ -11.28281307220459
+ ],
+ [
+ "▁1980",
+ -11.28286361694336
+ ],
+ [
+ "▁Fach",
+ -11.282917976379395
+ ],
+ [
+ "craft",
+ -11.283047676086426
+ ],
+ [
+ "▁Pakistan",
+ -11.283408164978027
+ ],
+ [
+ "▁Mos",
+ -11.283621788024902
+ ],
+ [
+ "▁toilet",
+ -11.283844947814941
+ ],
+ [
+ "partea",
+ -11.28391170501709
+ ],
+ [
+ "case",
+ -11.284221649169922
+ ],
+ [
+ "▁clock",
+ -11.28430461883545
+ ],
+ [
+ "▁parc",
+ -11.284602165222168
+ ],
+ [
+ "▁legislation",
+ -11.284692764282227
+ ],
+ [
+ "▁icon",
+ -11.284933090209961
+ ],
+ [
+ "etz",
+ -11.285178184509277
+ ],
+ [
+ "ept",
+ -11.285270690917969
+ ],
+ [
+ "▁Corporation",
+ -11.28585433959961
+ ],
+ [
+ "▁requested",
+ -11.285983085632324
+ ],
+ [
+ "▁column",
+ -11.286088943481445
+ ],
+ [
+ "rier",
+ -11.286120414733887
+ ],
+ [
+ "uß",
+ -11.2861967086792
+ ],
+ [
+ "▁wohl",
+ -11.286418914794922
+ ],
+ [
+ "tell",
+ -11.286569595336914
+ ],
+ [
+ "gno",
+ -11.286608695983887
+ ],
+ [
+ "▁diseases",
+ -11.286726951599121
+ ],
+ [
+ "Sch",
+ -11.286762237548828
+ ],
+ [
+ "▁colon",
+ -11.287075996398926
+ ],
+ [
+ "▁Based",
+ -11.28709602355957
+ ],
+ [
+ "▁flu",
+ -11.28725528717041
+ ],
+ [
+ "▁vocal",
+ -11.287408828735352
+ ],
+ [
+ "▁virus",
+ -11.287693977355957
+ ],
+ [
+ "▁traveling",
+ -11.287750244140625
+ ],
+ [
+ "bul",
+ -11.287837982177734
+ ],
+ [
+ "т",
+ -11.28794002532959
+ ],
+ [
+ "city",
+ -11.287961959838867
+ ],
+ [
+ "AU",
+ -11.287991523742676
+ ],
+ [
+ "wide",
+ -11.288037300109863
+ ],
+ [
+ "▁solo",
+ -11.288061141967773
+ ],
+ [
+ "▁functionality",
+ -11.288214683532715
+ ],
+ [
+ "▁reveal",
+ -11.28831672668457
+ ],
+ [
+ "sign",
+ -11.288952827453613
+ ],
+ [
+ "▁closing",
+ -11.288971900939941
+ ],
+ [
+ "▁peak",
+ -11.289087295532227
+ ],
+ [
+ "▁practic",
+ -11.289398193359375
+ ],
+ [
+ "than",
+ -11.289473533630371
+ ],
+ [
+ "▁driven",
+ -11.289484977722168
+ ],
+ [
+ "êtes",
+ -11.289548873901367
+ ],
+ [
+ "high",
+ -11.290016174316406
+ ],
+ [
+ "power",
+ -11.290226936340332
+ ],
+ [
+ "▁Lin",
+ -11.29028606414795
+ ],
+ [
+ "▁dose",
+ -11.29034423828125
+ ],
+ [
+ "▁pocket",
+ -11.290650367736816
+ ],
+ [
+ "▁Classic",
+ -11.29067611694336
+ ],
+ [
+ "▁packaging",
+ -11.290792465209961
+ ],
+ [
+ "▁distinct",
+ -11.290800094604492
+ ],
+ [
+ "▁côté",
+ -11.291094779968262
+ ],
+ [
+ "▁breast",
+ -11.29127025604248
+ ],
+ [
+ "▁folosit",
+ -11.29133129119873
+ ],
+ [
+ "▁drinks",
+ -11.291353225708008
+ ],
+ [
+ "▁Dog",
+ -11.291529655456543
+ ],
+ [
+ "ailleurs",
+ -11.291658401489258
+ ],
+ [
+ "▁caz",
+ -11.291804313659668
+ ],
+ [
+ "▁escape",
+ -11.29188346862793
+ ],
+ [
+ "▁warranty",
+ -11.291902542114258
+ ],
+ [
+ "▁pulled",
+ -11.291996955871582
+ ],
+ [
+ "data",
+ -11.292088508605957
+ ],
+ [
+ "▁facilitate",
+ -11.292213439941406
+ ],
+ [
+ "É",
+ -11.292335510253906
+ ],
+ [
+ "▁SP",
+ -11.292403221130371
+ ],
+ [
+ "lant",
+ -11.292557716369629
+ ],
+ [
+ "AD",
+ -11.29256534576416
+ ],
+ [
+ "▁Print",
+ -11.292802810668945
+ ],
+ [
+ "mond",
+ -11.292863845825195
+ ],
+ [
+ "▁strange",
+ -11.292875289916992
+ ],
+ [
+ "▁Hor",
+ -11.293227195739746
+ ],
+ [
+ "▁Collection",
+ -11.293328285217285
+ ],
+ [
+ "arm",
+ -11.29346752166748
+ ],
+ [
+ "cas",
+ -11.293691635131836
+ ],
+ [
+ "arrow",
+ -11.29379940032959
+ ],
+ [
+ "▁carrying",
+ -11.293927192687988
+ ],
+ [
+ "▁wave",
+ -11.294661521911621
+ ],
+ [
+ "setzt",
+ -11.294907569885254
+ ],
+ [
+ "▁construct",
+ -11.29514217376709
+ ],
+ [
+ "▁acts",
+ -11.295269966125488
+ ],
+ [
+ "▁Action",
+ -11.295342445373535
+ ],
+ [
+ "▁Kim",
+ -11.295354843139648
+ ],
+ [
+ "oxid",
+ -11.295459747314453
+ ],
+ [
+ "fish",
+ -11.295519828796387
+ ],
+ [
+ "▁damaged",
+ -11.295660018920898
+ ],
+ [
+ "▁Greek",
+ -11.295747756958008
+ ],
+ [
+ "▁belt",
+ -11.295772552490234
+ ],
+ [
+ "▁Prior",
+ -11.295778274536133
+ ],
+ [
+ "▁marks",
+ -11.295936584472656
+ ],
+ [
+ "▁lumea",
+ -11.296183586120605
+ ],
+ [
+ "▁twenty",
+ -11.296196937561035
+ ],
+ [
+ "▁locul",
+ -11.296360969543457
+ ],
+ [
+ "▁Army",
+ -11.296524047851562
+ ],
+ [
+ "apt",
+ -11.296602249145508
+ ],
+ [
+ "▁limits",
+ -11.296733856201172
+ ],
+ [
+ "▁cruise",
+ -11.296966552734375
+ ],
+ [
+ "▁List",
+ -11.296998023986816
+ ],
+ [
+ "utilisation",
+ -11.29753589630127
+ ],
+ [
+ "▁personality",
+ -11.297622680664062
+ ],
+ [
+ "▁sections",
+ -11.297759056091309
+ ],
+ [
+ "▁drawn",
+ -11.29797649383545
+ ],
+ [
+ "▁mold",
+ -11.298277854919434
+ ],
+ [
+ "▁Think",
+ -11.298333168029785
+ ],
+ [
+ "▁holidays",
+ -11.298355102539062
+ ],
+ [
+ "▁critic",
+ -11.298545837402344
+ ],
+ [
+ "grade",
+ -11.298660278320312
+ ],
+ [
+ "▁sick",
+ -11.299074172973633
+ ],
+ [
+ "▁characteristics",
+ -11.299237251281738
+ ],
+ [
+ "▁echipa",
+ -11.299272537231445
+ ],
+ [
+ "▁Fast",
+ -11.29929256439209
+ ],
+ [
+ "▁Br",
+ -11.299600601196289
+ ],
+ [
+ "▁Reise",
+ -11.299734115600586
+ ],
+ [
+ "teen",
+ -11.299749374389648
+ ],
+ [
+ "uci",
+ -11.299949645996094
+ ],
+ [
+ "!”",
+ -11.300180435180664
+ ],
+ [
+ "ppe",
+ -11.300532341003418
+ ],
+ [
+ "▁talked",
+ -11.301164627075195
+ ],
+ [
+ "▁gap",
+ -11.301473617553711
+ ],
+ [
+ "homme",
+ -11.301778793334961
+ ],
+ [
+ "▁interact",
+ -11.301934242248535
+ ],
+ [
+ "▁dollar",
+ -11.302276611328125
+ ],
+ [
+ "▁bone",
+ -11.302309036254883
+ ],
+ [
+ "▁Einsatz",
+ -11.302343368530273
+ ],
+ [
+ "▁sad",
+ -11.302434921264648
+ ],
+ [
+ "any",
+ -11.302445411682129
+ ],
+ [
+ "tation",
+ -11.302666664123535
+ ],
+ [
+ "▁Haupt",
+ -11.302748680114746
+ ],
+ [
+ "iva",
+ -11.302781105041504
+ ],
+ [
+ "▁Schu",
+ -11.302916526794434
+ ],
+ [
+ "▁evaluate",
+ -11.3036470413208
+ ],
+ [
+ "▁variant",
+ -11.303807258605957
+ ],
+ [
+ "▁IS",
+ -11.303879737854004
+ ],
+ [
+ "▁PRO",
+ -11.303947448730469
+ ],
+ [
+ "▁vine",
+ -11.303959846496582
+ ],
+ [
+ "rut",
+ -11.304062843322754
+ ],
+ [
+ "▁existence",
+ -11.30443286895752
+ ],
+ [
+ "-7",
+ -11.304525375366211
+ ],
+ [
+ "ancy",
+ -11.304702758789062
+ ],
+ [
+ "▁Want",
+ -11.305023193359375
+ ],
+ [
+ "alism",
+ -11.305127143859863
+ ],
+ [
+ "ranging",
+ -11.30550765991211
+ ],
+ [
+ "preis",
+ -11.305551528930664
+ ],
+ [
+ "All",
+ -11.305620193481445
+ ],
+ [
+ "▁reception",
+ -11.30565071105957
+ ],
+ [
+ "mai",
+ -11.305730819702148
+ ],
+ [
+ "▁lease",
+ -11.30577278137207
+ ],
+ [
+ "▁finest",
+ -11.30578899383545
+ ],
+ [
+ "▁evident",
+ -11.305874824523926
+ ],
+ [
+ "▁Easy",
+ -11.306075096130371
+ ],
+ [
+ "▁gilt",
+ -11.306085586547852
+ ],
+ [
+ "▁trips",
+ -11.306344985961914
+ ],
+ [
+ "▁skilled",
+ -11.306368827819824
+ ],
+ [
+ "consists",
+ -11.306456565856934
+ ],
+ [
+ "front",
+ -11.306635856628418
+ ],
+ [
+ "rati",
+ -11.306652069091797
+ ],
+ [
+ "▁Following",
+ -11.30678653717041
+ ],
+ [
+ "▁Medicine",
+ -11.307161331176758
+ ],
+ [
+ "▁pune",
+ -11.30729866027832
+ ],
+ [
+ "▁errors",
+ -11.307354927062988
+ ],
+ [
+ "arian",
+ -11.307613372802734
+ ],
+ [
+ "lib",
+ -11.30811882019043
+ ],
+ [
+ "SR",
+ -11.308351516723633
+ ],
+ [
+ "ML",
+ -11.308568000793457
+ ],
+ [
+ "▁Safety",
+ -11.308823585510254
+ ],
+ [
+ "▁clar",
+ -11.309355735778809
+ ],
+ [
+ "New",
+ -11.309764862060547
+ ],
+ [
+ "▁37",
+ -11.309773445129395
+ ],
+ [
+ "▁Administration",
+ -11.309823036193848
+ ],
+ [
+ "▁2.0",
+ -11.310120582580566
+ ],
+ [
+ "▁obviously",
+ -11.310196876525879
+ ],
+ [
+ "▁Mitarbeiter",
+ -11.310254096984863
+ ],
+ [
+ "▁improvements",
+ -11.31043529510498
+ ],
+ [
+ "▁Cut",
+ -11.310630798339844
+ ],
+ [
+ "▁Natural",
+ -11.310672760009766
+ ],
+ [
+ "▁arrival",
+ -11.311182975769043
+ ],
+ [
+ "▁pizza",
+ -11.311339378356934
+ ],
+ [
+ "eşti",
+ -11.311570167541504
+ ],
+ [
+ "cept",
+ -11.311654090881348
+ ],
+ [
+ "▁livre",
+ -11.311686515808105
+ ],
+ [
+ "▁nombreux",
+ -11.312195777893066
+ ],
+ [
+ "▁authentic",
+ -11.312231063842773
+ ],
+ [
+ "▁gemacht",
+ -11.312472343444824
+ ],
+ [
+ "▁broadcast",
+ -11.312478065490723
+ ],
+ [
+ "▁stronger",
+ -11.312545776367188
+ ],
+ [
+ "UP",
+ -11.31257152557373
+ ],
+ [
+ "▁centers",
+ -11.312614440917969
+ ],
+ [
+ "▁petite",
+ -11.312617301940918
+ ],
+ [
+ "▁spots",
+ -11.312626838684082
+ ],
+ [
+ "▁crystal",
+ -11.312756538391113
+ ],
+ [
+ "▁salon",
+ -11.313044548034668
+ ],
+ [
+ "▁gained",
+ -11.313098907470703
+ ],
+ [
+ "▁Mus",
+ -11.313215255737305
+ ],
+ [
+ "▁lens",
+ -11.313223838806152
+ ],
+ [
+ "▁ihm",
+ -11.313231468200684
+ ],
+ [
+ "minute",
+ -11.313573837280273
+ ],
+ [
+ "▁greatly",
+ -11.313587188720703
+ ],
+ [
+ "LP",
+ -11.31361198425293
+ ],
+ [
+ "rait",
+ -11.314027786254883
+ ],
+ [
+ "▁bid",
+ -11.314154624938965
+ ],
+ [
+ "▁cit",
+ -11.314203262329102
+ ],
+ [
+ "entreprise",
+ -11.31435775756836
+ ],
+ [
+ "▁55",
+ -11.314533233642578
+ ],
+ [
+ "▁respectively",
+ -11.314536094665527
+ ],
+ [
+ "▁lo",
+ -11.314638137817383
+ ],
+ [
+ "▁cons",
+ -11.314743995666504
+ ],
+ [
+ "▁Energie",
+ -11.315169334411621
+ ],
+ [
+ "▁OK",
+ -11.31521224975586
+ ],
+ [
+ "▁grill",
+ -11.315338134765625
+ ],
+ [
+ "▁heading",
+ -11.31549072265625
+ ],
+ [
+ "▁sollten",
+ -11.315491676330566
+ ],
+ [
+ "▁Fragen",
+ -11.315528869628906
+ ],
+ [
+ "▁Poli",
+ -11.315556526184082
+ ],
+ [
+ "▁studying",
+ -11.315723419189453
+ ],
+ [
+ "▁développement",
+ -11.315882682800293
+ ],
+ [
+ "▁foam",
+ -11.316035270690918
+ ],
+ [
+ "▁1996",
+ -11.316511154174805
+ ],
+ [
+ "▁disaster",
+ -11.31662654876709
+ ],
+ [
+ "▁cafe",
+ -11.317262649536133
+ ],
+ [
+ "▁moves",
+ -11.317267417907715
+ ],
+ [
+ "focuses",
+ -11.317712783813477
+ ],
+ [
+ "▁Avenue",
+ -11.317834854125977
+ ],
+ [
+ "▁humans",
+ -11.31784439086914
+ ],
+ [
+ "▁(3",
+ -11.318021774291992
+ ],
+ [
+ "▁région",
+ -11.318347930908203
+ ],
+ [
+ "▁DJ",
+ -11.318608283996582
+ ],
+ [
+ "shop",
+ -11.318819046020508
+ ],
+ [
+ "▁acting",
+ -11.318843841552734
+ ],
+ [
+ "▁Justice",
+ -11.318967819213867
+ ],
+ [
+ "▁trouve",
+ -11.319010734558105
+ ],
+ [
+ "▁Estate",
+ -11.319040298461914
+ ],
+ [
+ "▁strict",
+ -11.319231986999512
+ ],
+ [
+ "▁talks",
+ -11.319283485412598
+ ],
+ [
+ "▁mat",
+ -11.319290161132812
+ ],
+ [
+ "▁completion",
+ -11.319327354431152
+ ],
+ [
+ "delivering",
+ -11.31943416595459
+ ],
+ [
+ "CD",
+ -11.31973934173584
+ ],
+ [
+ "0%",
+ -11.319960594177246
+ ],
+ [
+ "▁creativity",
+ -11.320253372192383
+ ],
+ [
+ "BR",
+ -11.320272445678711
+ ],
+ [
+ "▁occurred",
+ -11.320357322692871
+ ],
+ [
+ "Car",
+ -11.320590019226074
+ ],
+ [
+ "▁rising",
+ -11.320761680603027
+ ],
+ [
+ "gger",
+ -11.32086181640625
+ ],
+ [
+ "▁Gene",
+ -11.320901870727539
+ ],
+ [
+ "▁workplace",
+ -11.320914268493652
+ ],
+ [
+ "phy",
+ -11.321065902709961
+ ],
+ [
+ "▁Bla",
+ -11.32107162475586
+ ],
+ [
+ "▁trailer",
+ -11.32120418548584
+ ],
+ [
+ "▁Forest",
+ -11.321205139160156
+ ],
+ [
+ "▁profession",
+ -11.321246147155762
+ ],
+ [
+ "▁Father",
+ -11.32137680053711
+ ],
+ [
+ "flu",
+ -11.321487426757812
+ ],
+ [
+ "tone",
+ -11.321489334106445
+ ],
+ [
+ "▁sexual",
+ -11.321736335754395
+ ],
+ [
+ "▁Map",
+ -11.321805953979492
+ ],
+ [
+ "OT",
+ -11.3218412399292
+ ],
+ [
+ "▁Us",
+ -11.321878433227539
+ ],
+ [
+ "tôt",
+ -11.321892738342285
+ ],
+ [
+ "▁Wert",
+ -11.321901321411133
+ ],
+ [
+ "preparing",
+ -11.322121620178223
+ ],
+ [
+ "isé",
+ -11.322243690490723
+ ],
+ [
+ "▁lake",
+ -11.322461128234863
+ ],
+ [
+ "eed",
+ -11.32270336151123
+ ],
+ [
+ "jun",
+ -11.322888374328613
+ ],
+ [
+ "▁implemented",
+ -11.323014259338379
+ ],
+ [
+ "vid",
+ -11.323116302490234
+ ],
+ [
+ "igne",
+ -11.323201179504395
+ ],
+ [
+ "▁follows",
+ -11.323214530944824
+ ],
+ [
+ "▁Eric",
+ -11.323430061340332
+ ],
+ [
+ "body",
+ -11.323530197143555
+ ],
+ [
+ "▁contained",
+ -11.323585510253906
+ ],
+ [
+ "▁massage",
+ -11.323715209960938
+ ],
+ [
+ "AV",
+ -11.323725700378418
+ ],
+ [
+ "▁insa",
+ -11.323850631713867
+ ],
+ [
+ "▁observed",
+ -11.323892593383789
+ ],
+ [
+ "▁marque",
+ -11.324137687683105
+ ],
+ [
+ "lines",
+ -11.324451446533203
+ ],
+ [
+ "▁Frage",
+ -11.324482917785645
+ ],
+ [
+ "largely",
+ -11.324647903442383
+ ],
+ [
+ "gegeben",
+ -11.32473087310791
+ ],
+ [
+ "▁colleagues",
+ -11.324762344360352
+ ],
+ [
+ "pha",
+ -11.32494068145752
+ ],
+ [
+ "▁representative",
+ -11.325217247009277
+ ],
+ [
+ "▁shut",
+ -11.325650215148926
+ ],
+ [
+ "▁secondary",
+ -11.325779914855957
+ ],
+ [
+ "▁exhibit",
+ -11.325927734375
+ ],
+ [
+ "1)",
+ -11.325932502746582
+ ],
+ [
+ "mid",
+ -11.326109886169434
+ ],
+ [
+ "▁Due",
+ -11.326229095458984
+ ],
+ [
+ "▁initiatives",
+ -11.326457023620605
+ ],
+ [
+ "▁occurs",
+ -11.326458930969238
+ ],
+ [
+ "lent",
+ -11.326478958129883
+ ],
+ [
+ "▁façon",
+ -11.326778411865234
+ ],
+ [
+ "▁iOS",
+ -11.326803207397461
+ ],
+ [
+ "▁exploring",
+ -11.327000617980957
+ ],
+ [
+ "▁stations",
+ -11.327103614807129
+ ],
+ [
+ "nton",
+ -11.327234268188477
+ ],
+ [
+ "▁Country",
+ -11.32729721069336
+ ],
+ [
+ "▁shouldn",
+ -11.327406883239746
+ ],
+ [
+ "▁casual",
+ -11.327611923217773
+ ],
+ [
+ "-18",
+ -11.32769775390625
+ ],
+ [
+ "▁maintained",
+ -11.32772445678711
+ ],
+ [
+ "▁cart",
+ -11.327790260314941
+ ],
+ [
+ "▁propre",
+ -11.327836036682129
+ ],
+ [
+ "▁asset",
+ -11.327948570251465
+ ],
+ [
+ "firm",
+ -11.32803726196289
+ ],
+ [
+ "gla",
+ -11.328231811523438
+ ],
+ [
+ "viv",
+ -11.3282470703125
+ ],
+ [
+ "▁scientists",
+ -11.328873634338379
+ ],
+ [
+ "▁Nor",
+ -11.328936576843262
+ ],
+ [
+ "ites",
+ -11.329320907592773
+ ],
+ [
+ "▁engaging",
+ -11.329933166503906
+ ],
+ [
+ "My",
+ -11.330178260803223
+ ],
+ [
+ "▁workshops",
+ -11.330282211303711
+ ],
+ [
+ "ffer",
+ -11.3303804397583
+ ],
+ [
+ "activité",
+ -11.33047103881836
+ ],
+ [
+ "▁tension",
+ -11.330567359924316
+ ],
+ [
+ "▁dual",
+ -11.330668449401855
+ ],
+ [
+ "uer",
+ -11.33084774017334
+ ],
+ [
+ "900",
+ -11.330941200256348
+ ],
+ [
+ "SF",
+ -11.33108139038086
+ ],
+ [
+ "▁kannst",
+ -11.331146240234375
+ ],
+ [
+ "▁bur",
+ -11.33115291595459
+ ],
+ [
+ "▁visitor",
+ -11.331156730651855
+ ],
+ [
+ "▁granted",
+ -11.331178665161133
+ ],
+ [
+ "▁union",
+ -11.331355094909668
+ ],
+ [
+ "▁tablet",
+ -11.331461906433105
+ ],
+ [
+ "▁Choose",
+ -11.33146858215332
+ ],
+ [
+ "ibil",
+ -11.331551551818848
+ ],
+ [
+ "▁settlement",
+ -11.331830978393555
+ ],
+ [
+ "genommen",
+ -11.331892967224121
+ ],
+ [
+ "▁marked",
+ -11.332956314086914
+ ],
+ [
+ "▁diagnostic",
+ -11.333370208740234
+ ],
+ [
+ "▁prayer",
+ -11.333529472351074
+ ],
+ [
+ "▁Toronto",
+ -11.334035873413086
+ ],
+ [
+ "trans",
+ -11.334146499633789
+ ],
+ [
+ "▁respectiv",
+ -11.334160804748535
+ ],
+ [
+ "▁2012.",
+ -11.334207534790039
+ ],
+ [
+ "icul",
+ -11.334394454956055
+ ],
+ [
+ "▁satisfied",
+ -11.334527969360352
+ ],
+ [
+ "▁Fla",
+ -11.334596633911133
+ ],
+ [
+ "▁estimate",
+ -11.334638595581055
+ ],
+ [
+ "▁Agency",
+ -11.33466911315918
+ ],
+ [
+ "OD",
+ -11.334708213806152
+ ],
+ [
+ "▁McC",
+ -11.334746360778809
+ ],
+ [
+ "bert",
+ -11.334748268127441
+ ],
+ [
+ "▁seal",
+ -11.334771156311035
+ ],
+ [
+ "aine",
+ -11.334839820861816
+ ],
+ [
+ "▁cauza",
+ -11.334848403930664
+ ],
+ [
+ "▁wallpaper",
+ -11.335081100463867
+ ],
+ [
+ "▁alb",
+ -11.33536434173584
+ ],
+ [
+ "▁Sound",
+ -11.335681915283203
+ ],
+ [
+ "worth",
+ -11.33572769165039
+ ],
+ [
+ "chten",
+ -11.335858345031738
+ ],
+ [
+ "programm",
+ -11.335896492004395
+ ],
+ [
+ "▁pounds",
+ -11.336215019226074
+ ],
+ [
+ "▁coaching",
+ -11.336278915405273
+ ],
+ [
+ "▁Furthermore",
+ -11.336454391479492
+ ],
+ [
+ "▁Korea",
+ -11.336471557617188
+ ],
+ [
+ "▁flour",
+ -11.336530685424805
+ ],
+ [
+ "▁sommes",
+ -11.33657169342041
+ ],
+ [
+ "▁Repair",
+ -11.33661937713623
+ ],
+ [
+ "”)",
+ -11.336642265319824
+ ],
+ [
+ "itch",
+ -11.336675643920898
+ ],
+ [
+ "blu",
+ -11.336786270141602
+ ],
+ [
+ "zar",
+ -11.336882591247559
+ ],
+ [
+ "▁diferite",
+ -11.33745002746582
+ ],
+ [
+ "▁Golf",
+ -11.337685585021973
+ ],
+ [
+ "arch",
+ -11.33772087097168
+ ],
+ [
+ "▁panels",
+ -11.337799072265625
+ ],
+ [
+ "jan",
+ -11.337956428527832
+ ],
+ [
+ "“.",
+ -11.338240623474121
+ ],
+ [
+ "izarea",
+ -11.338324546813965
+ ],
+ [
+ "▁golden",
+ -11.33854866027832
+ ],
+ [
+ "▁flying",
+ -11.338550567626953
+ ],
+ [
+ "▁museum",
+ -11.338700294494629
+ ],
+ [
+ "▁equivalent",
+ -11.338759422302246
+ ],
+ [
+ "▁Lang",
+ -11.339032173156738
+ ],
+ [
+ "schi",
+ -11.339539527893066
+ ],
+ [
+ "MI",
+ -11.339595794677734
+ ],
+ [
+ "▁faci",
+ -11.339838027954102
+ ],
+ [
+ "▁Rahmen",
+ -11.339988708496094
+ ],
+ [
+ "▁attending",
+ -11.340130805969238
+ ],
+ [
+ "′′",
+ -11.340483665466309
+ ],
+ [
+ "▁Tro",
+ -11.341070175170898
+ ],
+ [
+ "▁gaming",
+ -11.341447830200195
+ ],
+ [
+ "▁aujourd",
+ -11.341479301452637
+ ],
+ [
+ "▁Wochen",
+ -11.341526985168457
+ ],
+ [
+ "▁entering",
+ -11.341535568237305
+ ],
+ [
+ "its",
+ -11.34155559539795
+ ],
+ [
+ "▁Private",
+ -11.341866493225098
+ ],
+ [
+ "▁Ocean",
+ -11.34188175201416
+ ],
+ [
+ "▁01",
+ -11.342098236083984
+ ],
+ [
+ "▁coloring",
+ -11.342188835144043
+ ],
+ [
+ "ător",
+ -11.34253215789795
+ ],
+ [
+ "▁flooring",
+ -11.342548370361328
+ ],
+ [
+ "▁downtown",
+ -11.34276294708252
+ ],
+ [
+ "rab",
+ -11.342998504638672
+ ],
+ [
+ "HI",
+ -11.343221664428711
+ ],
+ [
+ "▁illness",
+ -11.343234062194824
+ ],
+ [
+ "▁whil",
+ -11.343307495117188
+ ],
+ [
+ "▁diamond",
+ -11.34333324432373
+ ],
+ [
+ "Mail",
+ -11.343419075012207
+ ],
+ [
+ "▁Dream",
+ -11.34344482421875
+ ],
+ [
+ "▁Golden",
+ -11.344099044799805
+ ],
+ [
+ "▁rein",
+ -11.344220161437988
+ ],
+ [
+ "▁hi",
+ -11.344283103942871
+ ],
+ [
+ "▁expressed",
+ -11.344489097595215
+ ],
+ [
+ "▁luat",
+ -11.344511985778809
+ ],
+ [
+ "▁Share",
+ -11.34453010559082
+ ],
+ [
+ "▁Programm",
+ -11.344706535339355
+ ],
+ [
+ "▁Sales",
+ -11.344707489013672
+ ],
+ [
+ "▁prof",
+ -11.344890594482422
+ ],
+ [
+ "▁MO",
+ -11.34505844116211
+ ],
+ [
+ "▁Short",
+ -11.345088958740234
+ ],
+ [
+ "▁charm",
+ -11.345290184020996
+ ],
+ [
+ "▁Cer",
+ -11.345373153686523
+ ],
+ [
+ "▁Run",
+ -11.34553337097168
+ ],
+ [
+ "▁tutorial",
+ -11.345589637756348
+ ],
+ [
+ "oul",
+ -11.34561824798584
+ ],
+ [
+ "▁Fest",
+ -11.345794677734375
+ ],
+ [
+ "▁uniform",
+ -11.345929145812988
+ ],
+ [
+ "aß",
+ -11.346014976501465
+ ],
+ [
+ "▁pipe",
+ -11.346076965332031
+ ],
+ [
+ "▁Square",
+ -11.346283912658691
+ ],
+ [
+ "▁Kosten",
+ -11.346365928649902
+ ],
+ [
+ "▁checked",
+ -11.346590042114258
+ ],
+ [
+ "▁65",
+ -11.346626281738281
+ ],
+ [
+ "▁Adam",
+ -11.346686363220215
+ ],
+ [
+ "cel",
+ -11.346700668334961
+ ],
+ [
+ "ello",
+ -11.346965789794922
+ ],
+ [
+ "▁Res",
+ -11.347023963928223
+ ],
+ [
+ "▁drain",
+ -11.34708309173584
+ ],
+ [
+ "ză",
+ -11.347129821777344
+ ],
+ [
+ "▁Tech",
+ -11.34739875793457
+ ],
+ [
+ "▁strive",
+ -11.34749698638916
+ ],
+ [
+ "cycl",
+ -11.347506523132324
+ ],
+ [
+ "▁stark",
+ -11.347541809082031
+ ],
+ [
+ "load",
+ -11.34754753112793
+ ],
+ [
+ "▁Stat",
+ -11.347589492797852
+ ],
+ [
+ "▁Rec",
+ -11.347622871398926
+ ],
+ [
+ "ians",
+ -11.347716331481934
+ ],
+ [
+ "▁Tin",
+ -11.347738265991211
+ ],
+ [
+ "▁Agreement",
+ -11.347840309143066
+ ],
+ [
+ "▁pret",
+ -11.348027229309082
+ ],
+ [
+ "-9",
+ -11.348326683044434
+ ],
+ [
+ "▁sentence",
+ -11.348380088806152
+ ],
+ [
+ "▁Direct",
+ -11.348426818847656
+ ],
+ [
+ "▁Rep",
+ -11.348465919494629
+ ],
+ [
+ "▁Prozent",
+ -11.348799705505371
+ ],
+ [
+ "▁invitation",
+ -11.34882640838623
+ ],
+ [
+ "▁refund",
+ -11.349113464355469
+ ],
+ [
+ "▁Kids",
+ -11.349287986755371
+ ],
+ [
+ "stock",
+ -11.349383354187012
+ ],
+ [
+ "TP",
+ -11.349400520324707
+ ],
+ [
+ "▁tau",
+ -11.34941291809082
+ ],
+ [
+ "from",
+ -11.349421501159668
+ ],
+ [
+ "▁Ash",
+ -11.349451065063477
+ ],
+ [
+ "store",
+ -11.349535942077637
+ ],
+ [
+ "▁Common",
+ -11.34958553314209
+ ],
+ [
+ "▁Qualität",
+ -11.34968376159668
+ ],
+ [
+ "▁strongly",
+ -11.349727630615234
+ ],
+ [
+ "▁importante",
+ -11.34979248046875
+ ],
+ [
+ "ome",
+ -11.349912643432617
+ ],
+ [
+ "▁surtout",
+ -11.349946022033691
+ ],
+ [
+ "enables",
+ -11.35020637512207
+ ],
+ [
+ "▁decent",
+ -11.350221633911133
+ ],
+ [
+ "▁neutral",
+ -11.350237846374512
+ ],
+ [
+ "▁produs",
+ -11.350356101989746
+ ],
+ [
+ "bury",
+ -11.350451469421387
+ ],
+ [
+ "▁Level",
+ -11.350618362426758
+ ],
+ [
+ "▁interes",
+ -11.350699424743652
+ ],
+ [
+ "mov",
+ -11.350797653198242
+ ],
+ [
+ "▁backup",
+ -11.350939750671387
+ ],
+ [
+ "même",
+ -11.351094245910645
+ ],
+ [
+ "doc",
+ -11.351119041442871
+ ],
+ [
+ "▁#1",
+ -11.35130786895752
+ ],
+ [
+ "▁specified",
+ -11.351495742797852
+ ],
+ [
+ "▁founder",
+ -11.351655960083008
+ ],
+ [
+ "And",
+ -11.352090835571289
+ ],
+ [
+ "isten",
+ -11.352149963378906
+ ],
+ [
+ "▁lecture",
+ -11.352729797363281
+ ],
+ [
+ "▁wake",
+ -11.352895736694336
+ ],
+ [
+ "▁vraiment",
+ -11.352980613708496
+ ],
+ [
+ "▁swing",
+ -11.353188514709473
+ ],
+ [
+ "▁addresses",
+ -11.353275299072266
+ ],
+ [
+ "▁Verfügung",
+ -11.353504180908203
+ ],
+ [
+ "▁deadline",
+ -11.353761672973633
+ ],
+ [
+ "н",
+ -11.353791236877441
+ ],
+ [
+ "▁Content",
+ -11.353970527648926
+ ],
+ [
+ "▁Gre",
+ -11.354111671447754
+ ],
+ [
+ "▁Experience",
+ -11.354378700256348
+ ],
+ [
+ "tura",
+ -11.354458808898926
+ ],
+ [
+ "▁exit",
+ -11.354642868041992
+ ],
+ [
+ "▁Britain",
+ -11.354652404785156
+ ],
+ [
+ "▁Sunt",
+ -11.354684829711914
+ ],
+ [
+ "▁documentation",
+ -11.354690551757812
+ ],
+ [
+ "▁showcase",
+ -11.3547945022583
+ ],
+ [
+ "▁photographs",
+ -11.354822158813477
+ ],
+ [
+ "qué",
+ -11.35483169555664
+ ],
+ [
+ "zin",
+ -11.354909896850586
+ ],
+ [
+ "pres",
+ -11.354933738708496
+ ],
+ [
+ "▁decline",
+ -11.354955673217773
+ ],
+ [
+ "▁Large",
+ -11.355030059814453
+ ],
+ [
+ "▁bills",
+ -11.355141639709473
+ ],
+ [
+ "▁entitled",
+ -11.355222702026367
+ ],
+ [
+ "▁passionate",
+ -11.355393409729004
+ ],
+ [
+ "▁workout",
+ -11.355413436889648
+ ],
+ [
+ "▁Again",
+ -11.35560417175293
+ ],
+ [
+ "▁Haut",
+ -11.35582160949707
+ ],
+ [
+ "▁guaranteed",
+ -11.35599136352539
+ ],
+ [
+ "▁vue",
+ -11.35600471496582
+ ],
+ [
+ "▁farmers",
+ -11.356224060058594
+ ],
+ [
+ "▁admission",
+ -11.356500625610352
+ ],
+ [
+ "▁manière",
+ -11.357080459594727
+ ],
+ [
+ "▁reverse",
+ -11.357121467590332
+ ],
+ [
+ "▁FL",
+ -11.357142448425293
+ ],
+ [
+ "▁terminal",
+ -11.357206344604492
+ ],
+ [
+ "GI",
+ -11.35731029510498
+ ],
+ [
+ "▁speakers",
+ -11.35739803314209
+ ],
+ [
+ "▁responses",
+ -11.357398986816406
+ ],
+ [
+ "▁Doch",
+ -11.357457160949707
+ ],
+ [
+ "▁2013,",
+ -11.357717514038086
+ ],
+ [
+ "▁phones",
+ -11.357789993286133
+ ],
+ [
+ "ential",
+ -11.357851028442383
+ ],
+ [
+ "▁operator",
+ -11.357916831970215
+ ],
+ [
+ "▁steam",
+ -11.358036994934082
+ ],
+ [
+ "burn",
+ -11.358091354370117
+ ],
+ [
+ "▁seul",
+ -11.35815715789795
+ ],
+ [
+ "▁unusual",
+ -11.358322143554688
+ ],
+ [
+ "▁educate",
+ -11.358403205871582
+ ],
+ [
+ "▁Que",
+ -11.358680725097656
+ ],
+ [
+ "▁believes",
+ -11.359137535095215
+ ],
+ [
+ "▁succeed",
+ -11.359344482421875
+ ],
+ [
+ "▁delay",
+ -11.359533309936523
+ ],
+ [
+ "▁deeper",
+ -11.359633445739746
+ ],
+ [
+ "▁reaching",
+ -11.359890937805176
+ ],
+ [
+ "▁objectives",
+ -11.360086441040039
+ ],
+ [
+ "▁temporary",
+ -11.36028003692627
+ ],
+ [
+ "▁artistic",
+ -11.360421180725098
+ ],
+ [
+ "▁sou",
+ -11.360471725463867
+ ],
+ [
+ "▁transparent",
+ -11.36062240600586
+ ],
+ [
+ "There",
+ -11.360798835754395
+ ],
+ [
+ "ception",
+ -11.360836029052734
+ ],
+ [
+ "▁excess",
+ -11.360939979553223
+ ],
+ [
+ "▁gathering",
+ -11.361008644104004
+ ],
+ [
+ "▁Save",
+ -11.361095428466797
+ ],
+ [
+ "ază",
+ -11.361166000366211
+ ],
+ [
+ "▁français",
+ -11.361197471618652
+ ],
+ [
+ "▁laid",
+ -11.361210823059082
+ ],
+ [
+ "▁modul",
+ -11.361394882202148
+ ],
+ [
+ "avoir",
+ -11.361465454101562
+ ],
+ [
+ "under",
+ -11.362113952636719
+ ],
+ [
+ "dding",
+ -11.362226486206055
+ ],
+ [
+ "▁falls",
+ -11.362232208251953
+ ],
+ [
+ "▁Möglichkeit",
+ -11.362369537353516
+ ],
+ [
+ "▁ceremony",
+ -11.362370491027832
+ ],
+ [
+ "rai",
+ -11.36237621307373
+ ],
+ [
+ "▁Bor",
+ -11.362709045410156
+ ],
+ [
+ "▁Below",
+ -11.362750053405762
+ ],
+ [
+ "4)",
+ -11.362759590148926
+ ],
+ [
+ "▁Field",
+ -11.362833023071289
+ ],
+ [
+ "wear",
+ -11.362935066223145
+ ],
+ [
+ "motion",
+ -11.362948417663574
+ ],
+ [
+ "print",
+ -11.363311767578125
+ ],
+ [
+ "game",
+ -11.363360404968262
+ ],
+ [
+ "▁Irish",
+ -11.363458633422852
+ ],
+ [
+ "▁Las",
+ -11.363458633422852
+ ],
+ [
+ "Among",
+ -11.363570213317871
+ ],
+ [
+ "atori",
+ -11.363580703735352
+ ],
+ [
+ "▁ajuns",
+ -11.363837242126465
+ ],
+ [
+ "▁alive",
+ -11.363860130310059
+ ],
+ [
+ "▁retour",
+ -11.363900184631348
+ ],
+ [
+ "▁smoke",
+ -11.3640775680542
+ ],
+ [
+ "▁math",
+ -11.364285469055176
+ ],
+ [
+ "▁Ye",
+ -11.364337921142578
+ ],
+ [
+ "▁Denn",
+ -11.36436653137207
+ ],
+ [
+ "▁1995",
+ -11.364412307739258
+ ],
+ [
+ "▁bani",
+ -11.364644050598145
+ ],
+ [
+ "raz",
+ -11.364998817443848
+ ],
+ [
+ "world",
+ -11.365026473999023
+ ],
+ [
+ "▁engines",
+ -11.365140914916992
+ ],
+ [
+ "nehmen",
+ -11.365192413330078
+ ],
+ [
+ "stor",
+ -11.365328788757324
+ ],
+ [
+ "▁interpret",
+ -11.365403175354004
+ ],
+ [
+ "▁Ven",
+ -11.365489959716797
+ ],
+ [
+ "▁cotton",
+ -11.365622520446777
+ ],
+ [
+ "▁represented",
+ -11.366004943847656
+ ],
+ [
+ "▁fabulous",
+ -11.366166114807129
+ ],
+ [
+ "▁gender",
+ -11.366301536560059
+ ],
+ [
+ "Mar",
+ -11.366668701171875
+ ],
+ [
+ "vic",
+ -11.366991996765137
+ ],
+ [
+ "▁newsletter",
+ -11.367432594299316
+ ],
+ [
+ "sburg",
+ -11.367574691772461
+ ],
+ [
+ "pond",
+ -11.36838436126709
+ ],
+ [
+ "▁Carl",
+ -11.368454933166504
+ ],
+ [
+ "▁bunch",
+ -11.368714332580566
+ ],
+ [
+ "▁tower",
+ -11.368847846984863
+ ],
+ [
+ "▁trigger",
+ -11.368976593017578
+ ],
+ [
+ "▁explanation",
+ -11.369091033935547
+ ],
+ [
+ "Man",
+ -11.369114875793457
+ ],
+ [
+ "iunea",
+ -11.369168281555176
+ ],
+ [
+ "▁announcement",
+ -11.369492530822754
+ ],
+ [
+ "▁seeds",
+ -11.36952018737793
+ ],
+ [
+ "▁shell",
+ -11.369865417480469
+ ],
+ [
+ "▁Working",
+ -11.36989688873291
+ ],
+ [
+ "viz",
+ -11.370267868041992
+ ],
+ [
+ "▁Simply",
+ -11.370329856872559
+ ],
+ [
+ "sub",
+ -11.37037181854248
+ ],
+ [
+ "▁Village",
+ -11.37060832977295
+ ],
+ [
+ "▁falling",
+ -11.370742797851562
+ ],
+ [
+ "▁fits",
+ -11.37084674835205
+ ],
+ [
+ "▁wichtig",
+ -11.37088394165039
+ ],
+ [
+ "▁Down",
+ -11.37108039855957
+ ],
+ [
+ "bble",
+ -11.371573448181152
+ ],
+ [
+ "▁Orange",
+ -11.37165641784668
+ ],
+ [
+ "promoting",
+ -11.371932029724121
+ ],
+ [
+ "▁rapidly",
+ -11.37217903137207
+ ],
+ [
+ "▁translation",
+ -11.372330665588379
+ ],
+ [
+ "nig",
+ -11.3723726272583
+ ],
+ [
+ "fusion",
+ -11.37240982055664
+ ],
+ [
+ "kosten",
+ -11.372611045837402
+ ],
+ [
+ "2)",
+ -11.372783660888672
+ ],
+ [
+ "▁Express",
+ -11.372958183288574
+ ],
+ [
+ "▁Sw",
+ -11.373003959655762
+ ],
+ [
+ "▁frequency",
+ -11.373086929321289
+ ],
+ [
+ "▁diversity",
+ -11.373348236083984
+ ],
+ [
+ "MT",
+ -11.373452186584473
+ ],
+ [
+ "▁bekannt",
+ -11.373530387878418
+ ],
+ [
+ "lion",
+ -11.373871803283691
+ ],
+ [
+ "▁cop",
+ -11.37393856048584
+ ],
+ [
+ "▁Customer",
+ -11.374072074890137
+ ],
+ [
+ "▁demands",
+ -11.374427795410156
+ ],
+ [
+ "▁corn",
+ -11.374516487121582
+ ],
+ [
+ "▁Hamburg",
+ -11.374551773071289
+ ],
+ [
+ "SD",
+ -11.374628067016602
+ ],
+ [
+ "▁Rome",
+ -11.374677658081055
+ ],
+ [
+ "▁Pur",
+ -11.374750137329102
+ ],
+ [
+ "▁stamp",
+ -11.374885559082031
+ ],
+ [
+ "▁grateful",
+ -11.374967575073242
+ ],
+ [
+ "RM",
+ -11.37511157989502
+ ],
+ [
+ "▁Pl",
+ -11.37511920928955
+ ],
+ [
+ "▁Tele",
+ -11.375154495239258
+ ],
+ [
+ "▁plugin",
+ -11.375492095947266
+ ],
+ [
+ "▁maxim",
+ -11.375675201416016
+ ],
+ [
+ "▁Hoch",
+ -11.37574577331543
+ ],
+ [
+ "igung",
+ -11.375823020935059
+ ],
+ [
+ "▁Entwicklung",
+ -11.375858306884766
+ ],
+ [
+ "▁File",
+ -11.375931739807129
+ ],
+ [
+ "▁Eastern",
+ -11.376070022583008
+ ],
+ [
+ "▁scrap",
+ -11.376331329345703
+ ],
+ [
+ "▁acquired",
+ -11.376338958740234
+ ],
+ [
+ "sau",
+ -11.376364707946777
+ ],
+ [
+ "▁Klein",
+ -11.376452445983887
+ ],
+ [
+ "▁milioane",
+ -11.376492500305176
+ ],
+ [
+ "▁Stand",
+ -11.376693725585938
+ ],
+ [
+ "▁childhood",
+ -11.37671184539795
+ ],
+ [
+ "▁artificial",
+ -11.376752853393555
+ ],
+ [
+ "▁substantial",
+ -11.376851081848145
+ ],
+ [
+ "druck",
+ -11.377315521240234
+ ],
+ [
+ "▁Kra",
+ -11.377562522888184
+ ],
+ [
+ "▁performances",
+ -11.377645492553711
+ ],
+ [
+ "▁row",
+ -11.377824783325195
+ ],
+ [
+ "NT",
+ -11.377899169921875
+ ],
+ [
+ "mod",
+ -11.377904891967773
+ ],
+ [
+ "remained",
+ -11.378399848937988
+ ],
+ [
+ "▁nimic",
+ -11.378462791442871
+ ],
+ [
+ "▁Limited",
+ -11.378555297851562
+ ],
+ [
+ "▁cookie",
+ -11.378718376159668
+ ],
+ [
+ "▁retain",
+ -11.378816604614258
+ ],
+ [
+ "▁600",
+ -11.379144668579102
+ ],
+ [
+ "▁eigene",
+ -11.379158020019531
+ ],
+ [
+ "▁tune",
+ -11.379209518432617
+ ],
+ [
+ "NS",
+ -11.379256248474121
+ ],
+ [
+ "▁dad",
+ -11.379284858703613
+ ],
+ [
+ "Moreover",
+ -11.379415512084961
+ ],
+ [
+ "ès",
+ -11.379434585571289
+ ],
+ [
+ "▁worship",
+ -11.379439353942871
+ ],
+ [
+ "▁Material",
+ -11.3794584274292
+ ],
+ [
+ "▁verb",
+ -11.379528045654297
+ ],
+ [
+ "ziehen",
+ -11.37957763671875
+ ],
+ [
+ "lton",
+ -11.379645347595215
+ ],
+ [
+ "▁boot",
+ -11.379982948303223
+ ],
+ [
+ "plo",
+ -11.380118370056152
+ ],
+ [
+ "CF",
+ -11.380212783813477
+ ],
+ [
+ "GM",
+ -11.380215644836426
+ ],
+ [
+ "▁Mix",
+ -11.38046932220459
+ ],
+ [
+ "▁Front",
+ -11.380474090576172
+ ],
+ [
+ "▁repairs",
+ -11.380655288696289
+ ],
+ [
+ "▁proportion",
+ -11.381068229675293
+ ],
+ [
+ "▁habit",
+ -11.381132125854492
+ ],
+ [
+ "▁hide",
+ -11.38156509399414
+ ],
+ [
+ "focusing",
+ -11.381707191467285
+ ],
+ [
+ "▁Annual",
+ -11.381717681884766
+ ],
+ [
+ "▁twin",
+ -11.3817777633667
+ ],
+ [
+ "▁acord",
+ -11.381780624389648
+ ],
+ [
+ "ehr",
+ -11.381814956665039
+ ],
+ [
+ "month",
+ -11.382303237915039
+ ],
+ [
+ "venir",
+ -11.382535934448242
+ ],
+ [
+ "Or",
+ -11.38254165649414
+ ],
+ [
+ "awa",
+ -11.382600784301758
+ ],
+ [
+ "lass",
+ -11.382735252380371
+ ],
+ [
+ "ffe",
+ -11.383048057556152
+ ],
+ [
+ "iți",
+ -11.383074760437012
+ ],
+ [
+ "NO",
+ -11.3831148147583
+ ],
+ [
+ "▁scope",
+ -11.383295059204102
+ ],
+ [
+ "▁lowest",
+ -11.383527755737305
+ ],
+ [
+ "▁afraid",
+ -11.383572578430176
+ ],
+ [
+ "▁subjects",
+ -11.383578300476074
+ ],
+ [
+ "▁templates",
+ -11.383586883544922
+ ],
+ [
+ "▁jos",
+ -11.383604049682617
+ ],
+ [
+ "DM",
+ -11.383687973022461
+ ],
+ [
+ "ensemble",
+ -11.383792877197266
+ ],
+ [
+ "▁Ski",
+ -11.383941650390625
+ ],
+ [
+ "DP",
+ -11.384099960327148
+ ],
+ [
+ "▁grip",
+ -11.384171485900879
+ ],
+ [
+ "2-",
+ -11.38436222076416
+ ],
+ [
+ "▁sécurité",
+ -11.384743690490723
+ ],
+ [
+ "▁mono",
+ -11.384749412536621
+ ],
+ [
+ "▁controls",
+ -11.384854316711426
+ ],
+ [
+ "SV",
+ -11.384879112243652
+ ],
+ [
+ "install",
+ -11.384970664978027
+ ],
+ [
+ "berry",
+ -11.385042190551758
+ ],
+ [
+ "nial",
+ -11.385120391845703
+ ],
+ [
+ "shed",
+ -11.385462760925293
+ ],
+ [
+ "▁celle",
+ -11.385830879211426
+ ],
+ [
+ "FR",
+ -11.385936737060547
+ ],
+ [
+ "äng",
+ -11.385950088500977
+ ],
+ [
+ "▁gaz",
+ -11.385984420776367
+ ],
+ [
+ "êt",
+ -11.386184692382812
+ ],
+ [
+ "▁viewing",
+ -11.386412620544434
+ ],
+ [
+ "▁asigura",
+ -11.386524200439453
+ ],
+ [
+ "bling",
+ -11.3865327835083
+ ],
+ [
+ "master",
+ -11.386919975280762
+ ],
+ [
+ "▁Fin",
+ -11.387160301208496
+ ],
+ [
+ "VC",
+ -11.387365341186523
+ ],
+ [
+ "▁patent",
+ -11.387715339660645
+ ],
+ [
+ "▁Clean",
+ -11.38773250579834
+ ],
+ [
+ "▁1970",
+ -11.387789726257324
+ ],
+ [
+ "▁Char",
+ -11.387971878051758
+ ],
+ [
+ "thi",
+ -11.388010025024414
+ ],
+ [
+ "bli",
+ -11.388141632080078
+ ],
+ [
+ "▁haut",
+ -11.388307571411133
+ ],
+ [
+ "tica",
+ -11.38836669921875
+ ],
+ [
+ "▁venit",
+ -11.388578414916992
+ ],
+ [
+ "▁compatible",
+ -11.388678550720215
+ ],
+ [
+ "▁hanging",
+ -11.388690948486328
+ ],
+ [
+ "UN",
+ -11.388842582702637
+ ],
+ [
+ "▁forth",
+ -11.388911247253418
+ ],
+ [
+ "▁painted",
+ -11.388912200927734
+ ],
+ [
+ "lip",
+ -11.389031410217285
+ ],
+ [
+ "▁deeply",
+ -11.389089584350586
+ ],
+ [
+ "▁participating",
+ -11.389242172241211
+ ],
+ [
+ "▁Iran",
+ -11.38968276977539
+ ],
+ [
+ "▁conventional",
+ -11.389769554138184
+ ],
+ [
+ "ARE",
+ -11.38985824584961
+ ],
+ [
+ "▁accuracy",
+ -11.389896392822266
+ ],
+ [
+ "▁Familie",
+ -11.389955520629883
+ ],
+ [
+ "▁Dir",
+ -11.39001178741455
+ ],
+ [
+ "▁gehen",
+ -11.390127182006836
+ ],
+ [
+ "▁moderne",
+ -11.39022159576416
+ ],
+ [
+ "▁Iraq",
+ -11.39050579071045
+ ],
+ [
+ "▁vente",
+ -11.390582084655762
+ ],
+ [
+ "▁Donald",
+ -11.390998840332031
+ ],
+ [
+ "▁passer",
+ -11.391051292419434
+ ],
+ [
+ "▁mehrere",
+ -11.391267776489258
+ ],
+ [
+ "▁Everything",
+ -11.391291618347168
+ ],
+ [
+ "▁studied",
+ -11.391307830810547
+ ],
+ [
+ "▁acquire",
+ -11.391312599182129
+ ],
+ [
+ "für",
+ -11.391477584838867
+ ],
+ [
+ "▁gal",
+ -11.391502380371094
+ ],
+ [
+ "▁headed",
+ -11.391809463500977
+ ],
+ [
+ "▁screening",
+ -11.391865730285645
+ ],
+ [
+ "▁findings",
+ -11.392303466796875
+ ],
+ [
+ "▁nutrition",
+ -11.392305374145508
+ ],
+ [
+ "▁Secretary",
+ -11.392308235168457
+ ],
+ [
+ "duct",
+ -11.392431259155273
+ ],
+ [
+ "born",
+ -11.392436027526855
+ ],
+ [
+ "«",
+ -11.39261531829834
+ ],
+ [
+ "▁statistics",
+ -11.392616271972656
+ ],
+ [
+ "▁Sydney",
+ -11.392800331115723
+ ],
+ [
+ "▁Prof",
+ -11.392829895019531
+ ],
+ [
+ "▁dialogue",
+ -11.39327621459961
+ ],
+ [
+ "▁gather",
+ -11.393425941467285
+ ],
+ [
+ "valu",
+ -11.393746376037598
+ ],
+ [
+ "▁currency",
+ -11.394073486328125
+ ],
+ [
+ "▁Kat",
+ -11.394092559814453
+ ],
+ [
+ "gotten",
+ -11.394189834594727
+ ],
+ [
+ "main",
+ -11.39432144165039
+ ],
+ [
+ "▁coin",
+ -11.394340515136719
+ ],
+ [
+ "▁Nick",
+ -11.394380569458008
+ ],
+ [
+ "vă",
+ -11.394658088684082
+ ],
+ [
+ "▁Victoria",
+ -11.394832611083984
+ ],
+ [
+ "▁conclusion",
+ -11.3949613571167
+ ],
+ [
+ "▁lemon",
+ -11.394998550415039
+ ],
+ [
+ "▁Article",
+ -11.39516830444336
+ ],
+ [
+ "▁necesar",
+ -11.39516830444336
+ ],
+ [
+ "mag",
+ -11.395180702209473
+ ],
+ [
+ "▁riding",
+ -11.39537239074707
+ ],
+ [
+ "▁Eli",
+ -11.395599365234375
+ ],
+ [
+ "▁cord",
+ -11.395635604858398
+ ],
+ [
+ "wä",
+ -11.39572811126709
+ ],
+ [
+ "ußerdem",
+ -11.395737648010254
+ ],
+ [
+ "▁Bed",
+ -11.395759582519531
+ ],
+ [
+ "▁layers",
+ -11.395833015441895
+ ],
+ [
+ "▁harder",
+ -11.395975112915039
+ ],
+ [
+ "▁processor",
+ -11.396040916442871
+ ],
+ [
+ "▁Ils",
+ -11.39613151550293
+ ],
+ [
+ "▁Edition",
+ -11.39615535736084
+ ],
+ [
+ "▁Link",
+ -11.396393775939941
+ ],
+ [
+ "éré",
+ -11.396461486816406
+ ],
+ [
+ "▁nume",
+ -11.396576881408691
+ ],
+ [
+ "▁Boy",
+ -11.39659595489502
+ ],
+ [
+ "▁equally",
+ -11.396646499633789
+ ],
+ [
+ "▁Regel",
+ -11.397119522094727
+ ],
+ [
+ "▁hopes",
+ -11.397185325622559
+ ],
+ [
+ "odor",
+ -11.397311210632324
+ ],
+ [
+ "▁initially",
+ -11.397430419921875
+ ],
+ [
+ "▁$4",
+ -11.3974609375
+ ],
+ [
+ "▁exemplu",
+ -11.397537231445312
+ ],
+ [
+ "▁vari",
+ -11.397565841674805
+ ],
+ [
+ "schl",
+ -11.397698402404785
+ ],
+ [
+ "▁southern",
+ -11.39809799194336
+ ],
+ [
+ "▁mein",
+ -11.39818000793457
+ ],
+ [
+ "▁1994",
+ -11.398300170898438
+ ],
+ [
+ "▁importantly",
+ -11.398401260375977
+ ],
+ [
+ "▁succes",
+ -11.398526191711426
+ ],
+ [
+ "▁developer",
+ -11.398598670959473
+ ],
+ [
+ "▁lips",
+ -11.39889144897461
+ ],
+ [
+ "▁attitude",
+ -11.39900016784668
+ ],
+ [
+ "▁Age",
+ -11.399541854858398
+ ],
+ [
+ "▁corps",
+ -11.399713516235352
+ ],
+ [
+ "▁clicking",
+ -11.39976978302002
+ ],
+ [
+ "▁putem",
+ -11.399832725524902
+ ],
+ [
+ "▁journée",
+ -11.40003776550293
+ ],
+ [
+ "boy",
+ -11.4002103805542
+ ],
+ [
+ "▁injured",
+ -11.40028190612793
+ ],
+ [
+ "▁watched",
+ -11.400433540344238
+ ],
+ [
+ "▁flights",
+ -11.40079116821289
+ ],
+ [
+ "turn",
+ -11.400980949401855
+ ],
+ [
+ "▁stainless",
+ -11.401562690734863
+ ],
+ [
+ "▁besondere",
+ -11.40156364440918
+ ],
+ [
+ "▁Tur",
+ -11.401596069335938
+ ],
+ [
+ "▁hiring",
+ -11.401650428771973
+ ],
+ [
+ "▁roads",
+ -11.401727676391602
+ ],
+ [
+ "ificat",
+ -11.401785850524902
+ ],
+ [
+ "▁Flor",
+ -11.402045249938965
+ ],
+ [
+ "▁puternic",
+ -11.402215003967285
+ ],
+ [
+ "▁unexpected",
+ -11.40223503112793
+ ],
+ [
+ "▁Est",
+ -11.40238094329834
+ ],
+ [
+ "▁adopted",
+ -11.40253734588623
+ ],
+ [
+ "▁Fox",
+ -11.402647972106934
+ ],
+ [
+ "▁contributions",
+ -11.402870178222656
+ ],
+ [
+ "sec",
+ -11.402968406677246
+ ],
+ [
+ "IO",
+ -11.403059959411621
+ ],
+ [
+ "▁santé",
+ -11.403432846069336
+ ],
+ [
+ "▁Tree",
+ -11.403763771057129
+ ],
+ [
+ "▁scurt",
+ -11.40381908416748
+ ],
+ [
+ "▁Products",
+ -11.403848648071289
+ ],
+ [
+ "▁forecast",
+ -11.403998374938965
+ ],
+ [
+ "▁actor",
+ -11.404143333435059
+ ],
+ [
+ "▁Gallery",
+ -11.404149055480957
+ ],
+ [
+ "▁continuous",
+ -11.404163360595703
+ ],
+ [
+ "▁Hat",
+ -11.404291152954102
+ ],
+ [
+ "▁slip",
+ -11.404501914978027
+ ],
+ [
+ "9%",
+ -11.404960632324219
+ ],
+ [
+ "▁depression",
+ -11.405043601989746
+ ],
+ [
+ "UI",
+ -11.405229568481445
+ ],
+ [
+ "abile",
+ -11.405648231506348
+ ],
+ [
+ "▁merit",
+ -11.405671119689941
+ ],
+ [
+ "▁Fer",
+ -11.405805587768555
+ ],
+ [
+ "▁robot",
+ -11.405888557434082
+ ],
+ [
+ "▁gel",
+ -11.40589427947998
+ ],
+ [
+ "▁gentle",
+ -11.406017303466797
+ ],
+ [
+ "▁wanting",
+ -11.406071662902832
+ ],
+ [
+ "▁understood",
+ -11.406157493591309
+ ],
+ [
+ "▁terrain",
+ -11.406161308288574
+ ],
+ [
+ "▁associate",
+ -11.406176567077637
+ ],
+ [
+ "▁discussions",
+ -11.40632152557373
+ ],
+ [
+ "▁Job",
+ -11.406365394592285
+ ],
+ [
+ "spec",
+ -11.406440734863281
+ ],
+ [
+ "Dabei",
+ -11.406475067138672
+ ],
+ [
+ "etic",
+ -11.406517028808594
+ ],
+ [
+ "gol",
+ -11.40654468536377
+ ],
+ [
+ "▁20%",
+ -11.406584739685059
+ ],
+ [
+ "▁grup",
+ -11.406606674194336
+ ],
+ [
+ "▁Doctor",
+ -11.406813621520996
+ ],
+ [
+ "verse",
+ -11.407246589660645
+ ],
+ [
+ "▁victim",
+ -11.407258033752441
+ ],
+ [
+ "ță",
+ -11.407302856445312
+ ],
+ [
+ "▁scores",
+ -11.407544136047363
+ ],
+ [
+ "▁Policy",
+ -11.407634735107422
+ ],
+ [
+ "▁Anna",
+ -11.407736778259277
+ ],
+ [
+ "IV",
+ -11.407804489135742
+ ],
+ [
+ "▁mineral",
+ -11.408202171325684
+ ],
+ [
+ "live",
+ -11.40821647644043
+ ],
+ [
+ "▁grey",
+ -11.408368110656738
+ ],
+ [
+ "struct",
+ -11.40852165222168
+ ],
+ [
+ "▁emails",
+ -11.408738136291504
+ ],
+ [
+ "▁anymore",
+ -11.409114837646484
+ ],
+ [
+ "▁productivity",
+ -11.409387588500977
+ ],
+ [
+ "▁Dark",
+ -11.409463882446289
+ ],
+ [
+ "▁neither",
+ -11.409481048583984
+ ],
+ [
+ "▁quotes",
+ -11.409611701965332
+ ],
+ [
+ "LS",
+ -11.410368919372559
+ ],
+ [
+ "▁Arizona",
+ -11.41040325164795
+ ],
+ [
+ "night",
+ -11.410497665405273
+ ],
+ [
+ "élé",
+ -11.411019325256348
+ ],
+ [
+ "▁assigned",
+ -11.411153793334961
+ ],
+ [
+ "▁satellite",
+ -11.411328315734863
+ ],
+ [
+ "▁stability",
+ -11.411665916442871
+ ],
+ [
+ "▁networking",
+ -11.41172981262207
+ ],
+ [
+ "▁Transport",
+ -11.411847114562988
+ ],
+ [
+ "▁persons",
+ -11.411856651306152
+ ],
+ [
+ "fund",
+ -11.412043571472168
+ ],
+ [
+ "▁pratique",
+ -11.41213321685791
+ ],
+ [
+ "▁inca",
+ -11.412134170532227
+ ],
+ [
+ "iller",
+ -11.412349700927734
+ ],
+ [
+ "▁packed",
+ -11.41239070892334
+ ],
+ [
+ "▁Vegas",
+ -11.412484169006348
+ ],
+ [
+ "▁offre",
+ -11.412493705749512
+ ],
+ [
+ "▁Bin",
+ -11.412518501281738
+ ],
+ [
+ "stop",
+ -11.412609100341797
+ ],
+ [
+ "mini",
+ -11.412860870361328
+ ],
+ [
+ "▁jam",
+ -11.412877082824707
+ ],
+ [
+ "cord",
+ -11.41289234161377
+ ],
+ [
+ "▁Beautiful",
+ -11.412996292114258
+ ],
+ [
+ "▁trash",
+ -11.413012504577637
+ ],
+ [
+ "▁wise",
+ -11.413092613220215
+ ],
+ [
+ "▁accounting",
+ -11.413178443908691
+ ],
+ [
+ "▁différents",
+ -11.413182258605957
+ ],
+ [
+ "▁stil",
+ -11.413214683532715
+ ],
+ [
+ "suit",
+ -11.413951873779297
+ ],
+ [
+ "▁vier",
+ -11.414209365844727
+ ],
+ [
+ "▁permis",
+ -11.414224624633789
+ ],
+ [
+ "flow",
+ -11.414238929748535
+ ],
+ [
+ "▁col",
+ -11.414749145507812
+ ],
+ [
+ "ected",
+ -11.414960861206055
+ ],
+ [
+ "▁singer",
+ -11.414999008178711
+ ],
+ [
+ "▁GmbH",
+ -11.415038108825684
+ ],
+ [
+ "tics",
+ -11.415094375610352
+ ],
+ [
+ "▁ser",
+ -11.415159225463867
+ ],
+ [
+ "On",
+ -11.415315628051758
+ ],
+ [
+ "▁insights",
+ -11.415605545043945
+ ],
+ [
+ "BB",
+ -11.415946960449219
+ ],
+ [
+ "▁differ",
+ -11.415959358215332
+ ],
+ [
+ "▁Glass",
+ -11.416131973266602
+ ],
+ [
+ "▁Six",
+ -11.416482925415039
+ ],
+ [
+ "▁subscription",
+ -11.416584968566895
+ ],
+ [
+ "BC",
+ -11.416606903076172
+ ],
+ [
+ "▁returning",
+ -11.416664123535156
+ ],
+ [
+ "kleinen",
+ -11.416693687438965
+ ],
+ [
+ "▁advantages",
+ -11.416747093200684
+ ],
+ [
+ "omme",
+ -11.416852951049805
+ ],
+ [
+ "lus",
+ -11.417071342468262
+ ],
+ [
+ "now",
+ -11.417141914367676
+ ],
+ [
+ "▁Pack",
+ -11.417253494262695
+ ],
+ [
+ "▁leak",
+ -11.417333602905273
+ ],
+ [
+ "▁muscles",
+ -11.41748332977295
+ ],
+ [
+ "▁davon",
+ -11.417492866516113
+ ],
+ [
+ "mph",
+ -11.417858123779297
+ ],
+ [
+ "▁temple",
+ -11.417868614196777
+ ],
+ [
+ "▁Après",
+ -11.417901039123535
+ ],
+ [
+ "▁Illinois",
+ -11.41801643371582
+ ],
+ [
+ "▁variable",
+ -11.418065071105957
+ ],
+ [
+ "▁judgment",
+ -11.418389320373535
+ ],
+ [
+ "gran",
+ -11.41861629486084
+ ],
+ [
+ "▁pose",
+ -11.418621063232422
+ ],
+ [
+ "das",
+ -11.418647766113281
+ ],
+ [
+ "ures",
+ -11.418673515319824
+ ],
+ [
+ "▁Championship",
+ -11.418689727783203
+ ],
+ [
+ "ebenfalls",
+ -11.41872501373291
+ ],
+ [
+ "▁hydro",
+ -11.418753623962402
+ ],
+ [
+ "▁angle",
+ -11.419268608093262
+ ],
+ [
+ "▁5-",
+ -11.41940975189209
+ ],
+ [
+ "▁gest",
+ -11.419547080993652
+ ],
+ [
+ "▁Frau",
+ -11.420233726501465
+ ],
+ [
+ "▁knock",
+ -11.420275688171387
+ ],
+ [
+ "FS",
+ -11.420442581176758
+ ],
+ [
+ "spi",
+ -11.420577049255371
+ ],
+ [
+ "▁Regional",
+ -11.420717239379883
+ ],
+ [
+ "lets",
+ -11.421098709106445
+ ],
+ [
+ "▁Date",
+ -11.42115592956543
+ ],
+ [
+ "▁Finance",
+ -11.421211242675781
+ ],
+ [
+ "▁Dann",
+ -11.421320915222168
+ ],
+ [
+ "Star",
+ -11.421380043029785
+ ],
+ [
+ "▁Creek",
+ -11.421393394470215
+ ],
+ [
+ "▁fu",
+ -11.421648979187012
+ ],
+ [
+ "wohn",
+ -11.422141075134277
+ ],
+ [
+ "▁anniversary",
+ -11.422219276428223
+ ],
+ [
+ "▁investments",
+ -11.422292709350586
+ ],
+ [
+ "▁universal",
+ -11.422601699829102
+ ],
+ [
+ "▁pit",
+ -11.422745704650879
+ ],
+ [
+ "ște",
+ -11.422784805297852
+ ],
+ [
+ "▁lab",
+ -11.422822952270508
+ ],
+ [
+ "dienst",
+ -11.422884941101074
+ ],
+ [
+ "▁pal",
+ -11.422889709472656
+ ],
+ [
+ "▁graphic",
+ -11.42289924621582
+ ],
+ [
+ "▁bearing",
+ -11.422900199890137
+ ],
+ [
+ "▁stylish",
+ -11.423087120056152
+ ],
+ [
+ "▁mé",
+ -11.42319393157959
+ ],
+ [
+ "▁există",
+ -11.42326545715332
+ ],
+ [
+ "▁découvrir",
+ -11.423477172851562
+ ],
+ [
+ "comp",
+ -11.423606872558594
+ ],
+ [
+ "ridge",
+ -11.423667907714844
+ ],
+ [
+ "▁heads",
+ -11.423765182495117
+ ],
+ [
+ "▁consequences",
+ -11.423835754394531
+ ],
+ [
+ "self",
+ -11.423842430114746
+ ],
+ [
+ "fried",
+ -11.423870086669922
+ ],
+ [
+ "▁inventory",
+ -11.424199104309082
+ ],
+ [
+ "▁strip",
+ -11.42422866821289
+ ],
+ [
+ "▁Civil",
+ -11.42424488067627
+ ],
+ [
+ "bell",
+ -11.424307823181152
+ ],
+ [
+ "▁neben",
+ -11.424444198608398
+ ],
+ [
+ "▁Perfect",
+ -11.424470901489258
+ ],
+ [
+ "▁Notre",
+ -11.424478530883789
+ ],
+ [
+ "▁fraud",
+ -11.424630165100098
+ ],
+ [
+ "▁employers",
+ -11.424656867980957
+ ],
+ [
+ "▁Jackson",
+ -11.42470645904541
+ ],
+ [
+ "▁probleme",
+ -11.424915313720703
+ ],
+ [
+ "▁richtig",
+ -11.424957275390625
+ ],
+ [
+ "▁Method",
+ -11.425009727478027
+ ],
+ [
+ "▁tired",
+ -11.425010681152344
+ ],
+ [
+ "dies",
+ -11.425031661987305
+ ],
+ [
+ "▁Number",
+ -11.425315856933594
+ ],
+ [
+ "rland",
+ -11.425652503967285
+ ],
+ [
+ "▁latter",
+ -11.426031112670898
+ ],
+ [
+ "rendre",
+ -11.426064491271973
+ ],
+ [
+ "▁cameras",
+ -11.426095962524414
+ ],
+ [
+ "▁euch",
+ -11.426630020141602
+ ],
+ [
+ "▁Description",
+ -11.427038192749023
+ ],
+ [
+ "Spec",
+ -11.427061080932617
+ ],
+ [
+ "▁mile",
+ -11.427437782287598
+ ],
+ [
+ "▁Challenge",
+ -11.427474021911621
+ ],
+ [
+ "▁Solutions",
+ -11.427504539489746
+ ],
+ [
+ "▁trusted",
+ -11.427509307861328
+ ],
+ [
+ "▁einge",
+ -11.427515029907227
+ ],
+ [
+ "rück",
+ -11.427528381347656
+ ],
+ [
+ "▁Ober",
+ -11.427635192871094
+ ],
+ [
+ "kes",
+ -11.42764949798584
+ ],
+ [
+ "▁Log",
+ -11.427684783935547
+ ],
+ [
+ "▁dessert",
+ -11.427776336669922
+ ],
+ [
+ "▁murder",
+ -11.428033828735352
+ ],
+ [
+ "▁1/2",
+ -11.428311347961426
+ ],
+ [
+ "▁Provide",
+ -11.42872142791748
+ ],
+ [
+ "nivelul",
+ -11.428800582885742
+ ],
+ [
+ "nici",
+ -11.428818702697754
+ ],
+ [
+ "▁observe",
+ -11.42889404296875
+ ],
+ [
+ "▁prescription",
+ -11.429162979125977
+ ],
+ [
+ "▁Sau",
+ -11.429170608520508
+ ],
+ [
+ "▁genuine",
+ -11.42919635772705
+ ],
+ [
+ "▁operated",
+ -11.429231643676758
+ ],
+ [
+ "▁generous",
+ -11.429267883300781
+ ],
+ [
+ "▁weapons",
+ -11.429458618164062
+ ],
+ [
+ "▁belief",
+ -11.4295015335083
+ ],
+ [
+ "▁consum",
+ -11.429584503173828
+ ],
+ [
+ "▁unknown",
+ -11.430116653442383
+ ],
+ [
+ "deoarece",
+ -11.430135726928711
+ ],
+ [
+ "Art",
+ -11.430147171020508
+ ],
+ [
+ "▁kurz",
+ -11.430183410644531
+ ],
+ [
+ "▁Gut",
+ -11.430258750915527
+ ],
+ [
+ "▁medication",
+ -11.430522918701172
+ ],
+ [
+ "▁Mau",
+ -11.43058967590332
+ ],
+ [
+ "▁divorce",
+ -11.430678367614746
+ ],
+ [
+ "▁claimed",
+ -11.430811882019043
+ ],
+ [
+ "halten",
+ -11.430848121643066
+ ],
+ [
+ "▁Cons",
+ -11.43089485168457
+ ],
+ [
+ "▁operational",
+ -11.430975914001465
+ ],
+ [
+ "▁Hong",
+ -11.431081771850586
+ ],
+ [
+ "VI",
+ -11.431143760681152
+ ],
+ [
+ "▁Blick",
+ -11.431485176086426
+ ],
+ [
+ "▁lamp",
+ -11.431706428527832
+ ],
+ [
+ "pati",
+ -11.431853294372559
+ ],
+ [
+ "▁4-",
+ -11.43192195892334
+ ],
+ [
+ "▁interven",
+ -11.431964874267578
+ ],
+ [
+ "ques",
+ -11.43201732635498
+ ],
+ [
+ "▁Talk",
+ -11.432096481323242
+ ],
+ [
+ "▁zeigt",
+ -11.432318687438965
+ ],
+ [
+ "▁targeted",
+ -11.432390213012695
+ ],
+ [
+ "round",
+ -11.432640075683594
+ ],
+ [
+ "enfant",
+ -11.432748794555664
+ ],
+ [
+ "▁Reg",
+ -11.432836532592773
+ ],
+ [
+ "▁instruments",
+ -11.432872772216797
+ ],
+ [
+ "▁calcul",
+ -11.433363914489746
+ ],
+ [
+ "▁Henry",
+ -11.4335298538208
+ ],
+ [
+ "▁Cla",
+ -11.433616638183594
+ ],
+ [
+ "▁rack",
+ -11.433661460876465
+ ],
+ [
+ "sehen",
+ -11.43375301361084
+ ],
+ [
+ "▁ending",
+ -11.433754920959473
+ ],
+ [
+ "▁resolve",
+ -11.434130668640137
+ ],
+ [
+ "▁advise",
+ -11.434178352355957
+ ],
+ [
+ "▁sociale",
+ -11.434386253356934
+ ],
+ [
+ "▁cabin",
+ -11.434536933898926
+ ],
+ [
+ "▁involve",
+ -11.43480396270752
+ ],
+ [
+ "gă",
+ -11.434889793395996
+ ],
+ [
+ "▁automat",
+ -11.435132026672363
+ ],
+ [
+ "▁consultant",
+ -11.435258865356445
+ ],
+ [
+ "Bu",
+ -11.435370445251465
+ ],
+ [
+ "▁safely",
+ -11.435466766357422
+ ],
+ [
+ "état",
+ -11.435478210449219
+ ],
+ [
+ "▁pros",
+ -11.435657501220703
+ ],
+ [
+ "▁lies",
+ -11.435659408569336
+ ],
+ [
+ "▁Brian",
+ -11.435914993286133
+ ],
+ [
+ "▁talented",
+ -11.435954093933105
+ ],
+ [
+ "pus",
+ -11.43599796295166
+ ],
+ [
+ "▁hub",
+ -11.436060905456543
+ ],
+ [
+ "▁Ji",
+ -11.436066627502441
+ ],
+ [
+ "▁sought",
+ -11.436102867126465
+ ],
+ [
+ "▁energie",
+ -11.436210632324219
+ ],
+ [
+ "▁möchten",
+ -11.43634033203125
+ ],
+ [
+ "▁11.",
+ -11.436558723449707
+ ],
+ [
+ "▁Kong",
+ -11.436662673950195
+ ],
+ [
+ "▁grave",
+ -11.43666934967041
+ ],
+ [
+ "▁lists",
+ -11.436800956726074
+ ],
+ [
+ "tati",
+ -11.436809539794922
+ ],
+ [
+ "verschiedenen",
+ -11.43692398071289
+ ],
+ [
+ "dam",
+ -11.437061309814453
+ ],
+ [
+ "▁charity",
+ -11.437249183654785
+ ],
+ [
+ "▁breaking",
+ -11.43735122680664
+ ],
+ [
+ "kins",
+ -11.43747329711914
+ ],
+ [
+ "▁könnte",
+ -11.437517166137695
+ ],
+ [
+ "▁appointed",
+ -11.437532424926758
+ ],
+ [
+ "roc",
+ -11.4376859664917
+ ],
+ [
+ "▁Senate",
+ -11.437979698181152
+ ],
+ [
+ "wit",
+ -11.438002586364746
+ ],
+ [
+ "▁emerging",
+ -11.438162803649902
+ ],
+ [
+ "▁année",
+ -11.438288688659668
+ ],
+ [
+ "▁Cool",
+ -11.438365936279297
+ ],
+ [
+ "▁sensor",
+ -11.43842887878418
+ ],
+ [
+ "How",
+ -11.438488960266113
+ ],
+ [
+ "▁Ryan",
+ -11.438626289367676
+ ],
+ [
+ "▁computers",
+ -11.43871784210205
+ ],
+ [
+ "▁fault",
+ -11.4388427734375
+ ],
+ [
+ "▁présent",
+ -11.438843727111816
+ ],
+ [
+ "ulation",
+ -11.439149856567383
+ ],
+ [
+ "▁stir",
+ -11.439348220825195
+ ],
+ [
+ "lauf",
+ -11.439703941345215
+ ],
+ [
+ "▁AI",
+ -11.440389633178711
+ ],
+ [
+ "▁Bri",
+ -11.440438270568848
+ ],
+ [
+ "▁bain",
+ -11.441011428833008
+ ],
+ [
+ "▁5,",
+ -11.441287994384766
+ ],
+ [
+ "schein",
+ -11.44157886505127
+ ],
+ [
+ "▁weiß",
+ -11.441596031188965
+ ],
+ [
+ "▁possibilities",
+ -11.44235610961914
+ ],
+ [
+ "gur",
+ -11.442413330078125
+ ],
+ [
+ "▁hinter",
+ -11.442647933959961
+ ],
+ [
+ "Innen",
+ -11.442755699157715
+ ],
+ [
+ "▁vorba",
+ -11.442992210388184
+ ],
+ [
+ "fahren",
+ -11.443008422851562
+ ],
+ [
+ "▁Cell",
+ -11.443072319030762
+ ],
+ [
+ "univers",
+ -11.443137168884277
+ ],
+ [
+ "▁Follow",
+ -11.443424224853516
+ ],
+ [
+ "▁emotions",
+ -11.44360637664795
+ ],
+ [
+ "▁Ministry",
+ -11.443694114685059
+ ],
+ [
+ "▁curriculum",
+ -11.443694114685059
+ ],
+ [
+ "Je",
+ -11.443764686584473
+ ],
+ [
+ "▁gab",
+ -11.444080352783203
+ ],
+ [
+ "▁sigur",
+ -11.444270133972168
+ ],
+ [
+ "rise",
+ -11.444416999816895
+ ],
+ [
+ "Pri",
+ -11.44466495513916
+ ],
+ [
+ "▁stabil",
+ -11.444781303405762
+ ],
+ [
+ "▁superb",
+ -11.445100784301758
+ ],
+ [
+ "▁Oak",
+ -11.44510269165039
+ ],
+ [
+ "▁rubber",
+ -11.445286750793457
+ ],
+ [
+ "▁tag",
+ -11.445306777954102
+ ],
+ [
+ "PG",
+ -11.445361137390137
+ ],
+ [
+ "▁Heat",
+ -11.445477485656738
+ ],
+ [
+ "▁thousand",
+ -11.445504188537598
+ ],
+ [
+ "▁meets",
+ -11.445521354675293
+ ],
+ [
+ "▁faced",
+ -11.445578575134277
+ ],
+ [
+ "▁reserve",
+ -11.445640563964844
+ ],
+ [
+ "cateva",
+ -11.445767402648926
+ ],
+ [
+ "▁gym",
+ -11.445771217346191
+ ],
+ [
+ "▁vitamin",
+ -11.445960998535156
+ ],
+ [
+ "▁Rest",
+ -11.446457862854004
+ ],
+ [
+ "▁Single",
+ -11.446535110473633
+ ],
+ [
+ "▁Stephen",
+ -11.446623802185059
+ ],
+ [
+ "▁trick",
+ -11.446824073791504
+ ],
+ [
+ "DU",
+ -11.44694709777832
+ ],
+ [
+ "▁telefon",
+ -11.44711685180664
+ ],
+ [
+ "▁gând",
+ -11.447120666503906
+ ],
+ [
+ "▁primit",
+ -11.447345733642578
+ ],
+ [
+ "▁Connect",
+ -11.447351455688477
+ ],
+ [
+ "▁führt",
+ -11.447440147399902
+ ],
+ [
+ "▁Info",
+ -11.447500228881836
+ ],
+ [
+ "▁recall",
+ -11.447848320007324
+ ],
+ [
+ "▁restore",
+ -11.447885513305664
+ ],
+ [
+ "lege",
+ -11.44792652130127
+ ],
+ [
+ "▁franchise",
+ -11.448189735412598
+ ],
+ [
+ "▁seulement",
+ -11.44856071472168
+ ],
+ [
+ "reci",
+ -11.448598861694336
+ ],
+ [
+ "▁2019,",
+ -11.44864273071289
+ ],
+ [
+ "▁Ring",
+ -11.448663711547852
+ ],
+ [
+ "▁assembly",
+ -11.448678970336914
+ ],
+ [
+ "intérieur",
+ -11.448775291442871
+ ],
+ [
+ "▁shade",
+ -11.44887924194336
+ ],
+ [
+ "▁meaningful",
+ -11.448881149291992
+ ],
+ [
+ "bag",
+ -11.448989868164062
+ ],
+ [
+ "ONE",
+ -11.449249267578125
+ ],
+ [
+ "▁globe",
+ -11.449287414550781
+ ],
+ [
+ "▁WA",
+ -11.449406623840332
+ ],
+ [
+ "▁intervention",
+ -11.449495315551758
+ ],
+ [
+ "öl",
+ -11.449531555175781
+ ],
+ [
+ "▁Marine",
+ -11.45029067993164
+ ],
+ [
+ "▁Angebot",
+ -11.450512886047363
+ ],
+ [
+ "▁align",
+ -11.450618743896484
+ ],
+ [
+ "▁temperatures",
+ -11.450634956359863
+ ],
+ [
+ "ifier",
+ -11.45091724395752
+ ],
+ [
+ "▁Nigeria",
+ -11.451189041137695
+ ],
+ [
+ "▁survive",
+ -11.451216697692871
+ ],
+ [
+ "ounce",
+ -11.451275825500488
+ ],
+ [
+ "▁placement",
+ -11.451416969299316
+ ],
+ [
+ "▁deci",
+ -11.451528549194336
+ ],
+ [
+ "▁Taylor",
+ -11.451759338378906
+ ],
+ [
+ "step",
+ -11.45190715789795
+ ],
+ [
+ "▁Geschichte",
+ -11.452054023742676
+ ],
+ [
+ "▁Bet",
+ -11.452169418334961
+ ],
+ [
+ "▁Nature",
+ -11.45224380493164
+ ],
+ [
+ "▁FC",
+ -11.452256202697754
+ ],
+ [
+ "▁ownership",
+ -11.452286720275879
+ ],
+ [
+ "▁behaviour",
+ -11.452474594116211
+ ],
+ [
+ "▁deutlich",
+ -11.452532768249512
+ ],
+ [
+ "▁wondering",
+ -11.452798843383789
+ ],
+ [
+ "▁cleaner",
+ -11.453295707702637
+ ],
+ [
+ "uring",
+ -11.4534912109375
+ ],
+ [
+ "rä",
+ -11.453496932983398
+ ],
+ [
+ "▁ga",
+ -11.454296112060547
+ ],
+ [
+ "ador",
+ -11.454482078552246
+ ],
+ [
+ "▁artwork",
+ -11.454564094543457
+ ],
+ [
+ "ologic",
+ -11.45457649230957
+ ],
+ [
+ "▁eigentlich",
+ -11.454848289489746
+ ],
+ [
+ "▁hell",
+ -11.45522403717041
+ ],
+ [
+ "source",
+ -11.455251693725586
+ ],
+ [
+ "▁gem",
+ -11.455265045166016
+ ],
+ [
+ "▁boss",
+ -11.455307006835938
+ ],
+ [
+ "▁arise",
+ -11.455460548400879
+ ],
+ [
+ "about",
+ -11.455711364746094
+ ],
+ [
+ "▁SI",
+ -11.455951690673828
+ ],
+ [
+ "▁ME",
+ -11.45610237121582
+ ],
+ [
+ "akt",
+ -11.456191062927246
+ ],
+ [
+ "▁Style",
+ -11.456259727478027
+ ],
+ [
+ "▁Körper",
+ -11.456493377685547
+ ],
+ [
+ "gui",
+ -11.456799507141113
+ ],
+ [
+ "▁navigate",
+ -11.456819534301758
+ ],
+ [
+ "▁Meanwhile",
+ -11.456977844238281
+ ],
+ [
+ "▁așa",
+ -11.457111358642578
+ ],
+ [
+ "▁bulk",
+ -11.457298278808594
+ ],
+ [
+ "▁directions",
+ -11.457310676574707
+ ],
+ [
+ "▁brick",
+ -11.457747459411621
+ ],
+ [
+ "▁Poly",
+ -11.457752227783203
+ ],
+ [
+ "▁politique",
+ -11.457772254943848
+ ],
+ [
+ "▁patch",
+ -11.457777976989746
+ ],
+ [
+ "ра",
+ -11.457816123962402
+ ],
+ [
+ "commerce",
+ -11.457844734191895
+ ],
+ [
+ "▁înainte",
+ -11.457884788513184
+ ],
+ [
+ "▁intelligent",
+ -11.45823860168457
+ ],
+ [
+ "▁infection",
+ -11.458426475524902
+ ],
+ [
+ "▁Tru",
+ -11.458494186401367
+ ],
+ [
+ "▁raising",
+ -11.458504676818848
+ ],
+ [
+ "tragen",
+ -11.458539009094238
+ ],
+ [
+ "▁portrait",
+ -11.45858383178711
+ ],
+ [
+ "▁meisten",
+ -11.458783149719238
+ ],
+ [
+ "▁organize",
+ -11.45893669128418
+ ],
+ [
+ "metric",
+ -11.458962440490723
+ ],
+ [
+ "▁Season",
+ -11.459036827087402
+ ],
+ [
+ "▁enforcement",
+ -11.459259033203125
+ ],
+ [
+ "origine",
+ -11.459836959838867
+ ],
+ [
+ "▁Ros",
+ -11.460065841674805
+ ],
+ [
+ "▁Mount",
+ -11.460083961486816
+ ],
+ [
+ "have",
+ -11.460237503051758
+ ],
+ [
+ "▁romantic",
+ -11.460258483886719
+ ],
+ [
+ "▁comic",
+ -11.460810661315918
+ ],
+ [
+ "▁greu",
+ -11.461116790771484
+ ],
+ [
+ "ET",
+ -11.46133041381836
+ ],
+ [
+ "▁hook",
+ -11.461407661437988
+ ],
+ [
+ "▁mort",
+ -11.461411476135254
+ ],
+ [
+ "▁indicated",
+ -11.461583137512207
+ ],
+ [
+ "▁7,",
+ -11.461982727050781
+ ],
+ [
+ "▁Neben",
+ -11.46204662322998
+ ],
+ [
+ "yer",
+ -11.46214485168457
+ ],
+ [
+ "▁momentul",
+ -11.46214771270752
+ ],
+ [
+ "note",
+ -11.462313652038574
+ ],
+ [
+ "▁baz",
+ -11.46231460571289
+ ],
+ [
+ "▁abroad",
+ -11.462320327758789
+ ],
+ [
+ "nite",
+ -11.462464332580566
+ ],
+ [
+ "▁bass",
+ -11.462701797485352
+ ],
+ [
+ "▁norm",
+ -11.462714195251465
+ ],
+ [
+ "▁É",
+ -11.462788581848145
+ ],
+ [
+ "4.",
+ -11.462881088256836
+ ],
+ [
+ "▁province",
+ -11.463004112243652
+ ],
+ [
+ "▁merge",
+ -11.463419914245605
+ ],
+ [
+ "arbeiten",
+ -11.463438987731934
+ ],
+ [
+ "-20",
+ -11.463574409484863
+ ],
+ [
+ "▁Nicht",
+ -11.463674545288086
+ ],
+ [
+ "spo",
+ -11.463783264160156
+ ],
+ [
+ "size",
+ -11.463815689086914
+ ],
+ [
+ "▁assure",
+ -11.463849067687988
+ ],
+ [
+ "charge",
+ -11.463987350463867
+ ],
+ [
+ "▁olive",
+ -11.464017868041992
+ ],
+ [
+ "▁Pot",
+ -11.46408462524414
+ ],
+ [
+ "▁Figure",
+ -11.4642333984375
+ ],
+ [
+ "clair",
+ -11.464336395263672
+ ],
+ [
+ "▁discipline",
+ -11.464600563049316
+ ],
+ [
+ "elli",
+ -11.464639663696289
+ ],
+ [
+ "▁tackle",
+ -11.465169906616211
+ ],
+ [
+ "▁buyer",
+ -11.465237617492676
+ ],
+ [
+ "▁loud",
+ -11.465479850769043
+ ],
+ [
+ "▁180",
+ -11.465534210205078
+ ],
+ [
+ "▁căt",
+ -11.465587615966797
+ ],
+ [
+ "▁Palm",
+ -11.465738296508789
+ ],
+ [
+ "away",
+ -11.46593189239502
+ ],
+ [
+ "▁Mother",
+ -11.46607494354248
+ ],
+ [
+ "onia",
+ -11.466240882873535
+ ],
+ [
+ "▁Protection",
+ -11.466416358947754
+ ],
+ [
+ "auto",
+ -11.466547966003418
+ ],
+ [
+ "▁Version",
+ -11.466583251953125
+ ],
+ [
+ "▁Nice",
+ -11.466714859008789
+ ],
+ [
+ "▁12.",
+ -11.46682071685791
+ ],
+ [
+ "▁0,",
+ -11.466835021972656
+ ],
+ [
+ "ATION",
+ -11.466911315917969
+ ],
+ [
+ "▁Produkte",
+ -11.466955184936523
+ ],
+ [
+ "▁tube",
+ -11.467084884643555
+ ],
+ [
+ "▁Houston",
+ -11.467106819152832
+ ],
+ [
+ "chu",
+ -11.467500686645508
+ ],
+ [
+ "pas",
+ -11.467717170715332
+ ],
+ [
+ "▁Ele",
+ -11.467801094055176
+ ],
+ [
+ "▁mountains",
+ -11.467835426330566
+ ],
+ [
+ "PH",
+ -11.467937469482422
+ ],
+ [
+ "▁languages",
+ -11.468672752380371
+ ],
+ [
+ "▁servicii",
+ -11.468722343444824
+ ],
+ [
+ "▁Stay",
+ -11.468999862670898
+ ],
+ [
+ "fil",
+ -11.469138145446777
+ ],
+ [
+ "▁propos",
+ -11.469801902770996
+ ],
+ [
+ "▁coll",
+ -11.469825744628906
+ ],
+ [
+ "▁mor",
+ -11.470197677612305
+ ],
+ [
+ "▁arrange",
+ -11.470410346984863
+ ],
+ [
+ "▁sorry",
+ -11.470475196838379
+ ],
+ [
+ "▁instruction",
+ -11.470723152160645
+ ],
+ [
+ "▁holes",
+ -11.47077465057373
+ ],
+ [
+ "letting",
+ -11.471046447753906
+ ],
+ [
+ "▁wa",
+ -11.471074104309082
+ ],
+ [
+ "▁Feb",
+ -11.471227645874023
+ ],
+ [
+ "omb",
+ -11.471232414245605
+ ],
+ [
+ "▁prise",
+ -11.471290588378906
+ ],
+ [
+ "VO",
+ -11.471305847167969
+ ],
+ [
+ "week",
+ -11.471349716186523
+ ],
+ [
+ "▁Event",
+ -11.471427917480469
+ ],
+ [
+ "▁AT",
+ -11.471485137939453
+ ],
+ [
+ "ket",
+ -11.471492767333984
+ ],
+ [
+ "haft",
+ -11.471579551696777
+ ],
+ [
+ "▁hits",
+ -11.47159194946289
+ ],
+ [
+ "foli",
+ -11.471681594848633
+ ],
+ [
+ "this",
+ -11.471948623657227
+ ],
+ [
+ "GP",
+ -11.471970558166504
+ ],
+ [
+ "▁Pin",
+ -11.472332954406738
+ ],
+ [
+ "▁Stein",
+ -11.472503662109375
+ ],
+ [
+ "thing",
+ -11.472512245178223
+ ],
+ [
+ "▁emphasis",
+ -11.472556114196777
+ ],
+ [
+ "▁Mur",
+ -11.472631454467773
+ ],
+ [
+ "▁Bag",
+ -11.472647666931152
+ ],
+ [
+ "cons",
+ -11.47273063659668
+ ],
+ [
+ "tons",
+ -11.472835540771484
+ ],
+ [
+ "lash",
+ -11.472987174987793
+ ],
+ [
+ "▁Grant",
+ -11.473104476928711
+ ],
+ [
+ "▁pris",
+ -11.473175048828125
+ ],
+ [
+ "▁bună",
+ -11.47323989868164
+ ],
+ [
+ "▁buc",
+ -11.473699569702148
+ ],
+ [
+ "▁passe",
+ -11.473746299743652
+ ],
+ [
+ "▁jewelry",
+ -11.474213600158691
+ ],
+ [
+ "iens",
+ -11.474342346191406
+ ],
+ [
+ "▁forma",
+ -11.47453784942627
+ ],
+ [
+ "▁Med",
+ -11.474651336669922
+ ],
+ [
+ "laufen",
+ -11.474778175354004
+ ],
+ [
+ "▁hunt",
+ -11.474977493286133
+ ],
+ [
+ "stayed",
+ -11.475086212158203
+ ],
+ [
+ "party",
+ -11.475152015686035
+ ],
+ [
+ "▁fra",
+ -11.47529411315918
+ ],
+ [
+ "▁scenes",
+ -11.475305557250977
+ ],
+ [
+ "▁absorb",
+ -11.47535228729248
+ ],
+ [
+ "▁abilities",
+ -11.475377082824707
+ ],
+ [
+ "lug",
+ -11.475507736206055
+ ],
+ [
+ "▁Sarah",
+ -11.475693702697754
+ ],
+ [
+ "mpf",
+ -11.47570514678955
+ ],
+ [
+ "▁fle",
+ -11.4757080078125
+ ],
+ [
+ "accès",
+ -11.475872993469238
+ ],
+ [
+ "▁solicit",
+ -11.475926399230957
+ ],
+ [
+ "pie",
+ -11.476278305053711
+ ],
+ [
+ "▁Zum",
+ -11.476296424865723
+ ],
+ [
+ "▁universe",
+ -11.476390838623047
+ ],
+ [
+ "▁exists",
+ -11.476449012756348
+ ],
+ [
+ "oane",
+ -11.476597785949707
+ ],
+ [
+ "IVE",
+ -11.47668743133545
+ ],
+ [
+ "▁2011.",
+ -11.476906776428223
+ ],
+ [
+ "▁specialists",
+ -11.477072715759277
+ ],
+ [
+ "▁mess",
+ -11.477309226989746
+ ],
+ [
+ "fach",
+ -11.477402687072754
+ ],
+ [
+ "▁Recht",
+ -11.477404594421387
+ ],
+ [
+ "▁hack",
+ -11.47755241394043
+ ],
+ [
+ "▁jacket",
+ -11.477564811706543
+ ],
+ [
+ "HC",
+ -11.47769832611084
+ ],
+ [
+ "▁substance",
+ -11.477728843688965
+ ],
+ [
+ "▁signing",
+ -11.477775573730469
+ ],
+ [
+ "▁allerdings",
+ -11.478032112121582
+ ],
+ [
+ "▁publish",
+ -11.478139877319336
+ ],
+ [
+ "▁Lab",
+ -11.478157043457031
+ ],
+ [
+ "▁agenda",
+ -11.478249549865723
+ ],
+ [
+ "lane",
+ -11.478299140930176
+ ],
+ [
+ "stream",
+ -11.478620529174805
+ ],
+ [
+ "schau",
+ -11.47879409790039
+ ],
+ [
+ "▁realizat",
+ -11.478971481323242
+ ],
+ [
+ "▁supplier",
+ -11.479019165039062
+ ],
+ [
+ "▁moderate",
+ -11.47902774810791
+ ],
+ [
+ "▁tours",
+ -11.479212760925293
+ ],
+ [
+ "▁narrative",
+ -11.479220390319824
+ ],
+ [
+ "ația",
+ -11.479279518127441
+ ],
+ [
+ "▁maps",
+ -11.479423522949219
+ ],
+ [
+ "treten",
+ -11.479447364807129
+ ],
+ [
+ "▁mars",
+ -11.479706764221191
+ ],
+ [
+ "▁moon",
+ -11.479745864868164
+ ],
+ [
+ "rose",
+ -11.479751586914062
+ ],
+ [
+ "▁exp",
+ -11.479766845703125
+ ],
+ [
+ "zahl",
+ -11.480154037475586
+ ],
+ [
+ "psych",
+ -11.480195999145508
+ ],
+ [
+ "▁gehört",
+ -11.48024845123291
+ ],
+ [
+ "▁bound",
+ -11.4803466796875
+ ],
+ [
+ "▁submission",
+ -11.480451583862305
+ ],
+ [
+ "▁clubs",
+ -11.480722427368164
+ ],
+ [
+ "Am",
+ -11.480755805969238
+ ],
+ [
+ "tenir",
+ -11.480782508850098
+ ],
+ [
+ "▁boast",
+ -11.480851173400879
+ ],
+ [
+ "▁boards",
+ -11.4810791015625
+ ],
+ [
+ "▁Geschäfts",
+ -11.481216430664062
+ ],
+ [
+ "zing",
+ -11.48126220703125
+ ],
+ [
+ "wort",
+ -11.48137092590332
+ ],
+ [
+ "lid",
+ -11.481417655944824
+ ],
+ [
+ "▁contractor",
+ -11.481528282165527
+ ],
+ [
+ "▁donner",
+ -11.481672286987305
+ ],
+ [
+ "▁coupon",
+ -11.481974601745605
+ ],
+ [
+ "adresse",
+ -11.482004165649414
+ ],
+ [
+ "colo",
+ -11.48210334777832
+ ],
+ [
+ "▁perception",
+ -11.482124328613281
+ ],
+ [
+ "NC",
+ -11.48222541809082
+ ],
+ [
+ "▁abge",
+ -11.482245445251465
+ ],
+ [
+ "▁cheaper",
+ -11.482268333435059
+ ],
+ [
+ "▁grace",
+ -11.482312202453613
+ ],
+ [
+ "▁resident",
+ -11.482718467712402
+ ],
+ [
+ "kla",
+ -11.4828462600708
+ ],
+ [
+ "▁bug",
+ -11.4828462600708
+ ],
+ [
+ "▁Available",
+ -11.482893943786621
+ ],
+ [
+ "▁BA",
+ -11.483323097229004
+ ],
+ [
+ "▁Met",
+ -11.483601570129395
+ ],
+ [
+ "▁climb",
+ -11.48365592956543
+ ],
+ [
+ "▁expanded",
+ -11.484349250793457
+ ],
+ [
+ "ying",
+ -11.484426498413086
+ ],
+ [
+ "▁matching",
+ -11.484469413757324
+ ],
+ [
+ "▁suffered",
+ -11.484733581542969
+ ],
+ [
+ "▁employed",
+ -11.484755516052246
+ ],
+ [
+ "pper",
+ -11.484843254089355
+ ],
+ [
+ "▁experiencing",
+ -11.484884262084961
+ ],
+ [
+ "ddy",
+ -11.484953880310059
+ ],
+ [
+ "▁philosophy",
+ -11.484955787658691
+ ],
+ [
+ "▁utilisé",
+ -11.485008239746094
+ ],
+ [
+ "▁Jane",
+ -11.485079765319824
+ ],
+ [
+ "LI",
+ -11.485087394714355
+ ],
+ [
+ "▁elected",
+ -11.485185623168945
+ ],
+ [
+ "▁MI",
+ -11.485264778137207
+ ],
+ [
+ "▁ISO",
+ -11.485340118408203
+ ],
+ [
+ "winning",
+ -11.48537540435791
+ ],
+ [
+ "▁vot",
+ -11.485424041748047
+ ],
+ [
+ "▁generic",
+ -11.485519409179688
+ ],
+ [
+ "▁Bol",
+ -11.485650062561035
+ ],
+ [
+ "▁copies",
+ -11.48568058013916
+ ],
+ [
+ "▁mechanical",
+ -11.48568058013916
+ ],
+ [
+ "günstig",
+ -11.485682487487793
+ ],
+ [
+ "roy",
+ -11.485770225524902
+ ],
+ [
+ "Astfel",
+ -11.485808372497559
+ ],
+ [
+ "media",
+ -11.485868453979492
+ ],
+ [
+ "▁shoulder",
+ -11.4859037399292
+ ],
+ [
+ "▁directory",
+ -11.486000061035156
+ ],
+ [
+ "▁banking",
+ -11.486016273498535
+ ],
+ [
+ "▁mistakes",
+ -11.486040115356445
+ ],
+ [
+ "▁Fran",
+ -11.486425399780273
+ ],
+ [
+ "▁Jon",
+ -11.486544609069824
+ ],
+ [
+ "▁spare",
+ -11.486579895019531
+ ],
+ [
+ "metri",
+ -11.486668586730957
+ ],
+ [
+ "▁mask",
+ -11.486879348754883
+ ],
+ [
+ "▁consistently",
+ -11.48695182800293
+ ],
+ [
+ "▁Columbia",
+ -11.487278938293457
+ ],
+ [
+ "roid",
+ -11.48774242401123
+ ],
+ [
+ "essen",
+ -11.487935066223145
+ ],
+ [
+ "▁(“",
+ -11.48798656463623
+ ],
+ [
+ "▁série",
+ -11.488212585449219
+ ],
+ [
+ "▁Phil",
+ -11.488249778747559
+ ],
+ [
+ "▁usor",
+ -11.488249778747559
+ ],
+ [
+ "▁stood",
+ -11.488279342651367
+ ],
+ [
+ "▁racing",
+ -11.488335609436035
+ ],
+ [
+ "▁Comme",
+ -11.488555908203125
+ ],
+ [
+ "▁exceed",
+ -11.488565444946289
+ ],
+ [
+ "на",
+ -11.488618850708008
+ ],
+ [
+ "▁activate",
+ -11.48873233795166
+ ],
+ [
+ "▁circle",
+ -11.488836288452148
+ ],
+ [
+ "▁bold",
+ -11.488956451416016
+ ],
+ [
+ "▁handy",
+ -11.48909854888916
+ ],
+ [
+ "merely",
+ -11.489114761352539
+ ],
+ [
+ "▁Edward",
+ -11.489147186279297
+ ],
+ [
+ "▁contracts",
+ -11.489530563354492
+ ],
+ [
+ "ê",
+ -11.489595413208008
+ ],
+ [
+ "▁campaigns",
+ -11.489673614501953
+ ],
+ [
+ "▁ought",
+ -11.489733695983887
+ ],
+ [
+ "▁nursing",
+ -11.489781379699707
+ ],
+ [
+ "▁Jr",
+ -11.489917755126953
+ ],
+ [
+ "▁rarely",
+ -11.490032196044922
+ ],
+ [
+ "▁Mir",
+ -11.490050315856934
+ ],
+ [
+ "▁diagnosis",
+ -11.490379333496094
+ ],
+ [
+ "▁Theatre",
+ -11.490394592285156
+ ],
+ [
+ "▁producer",
+ -11.490407943725586
+ ],
+ [
+ "Currently",
+ -11.490492820739746
+ ],
+ [
+ "▁fitting",
+ -11.490580558776855
+ ],
+ [
+ "▁ajunge",
+ -11.490618705749512
+ ],
+ [
+ "minte",
+ -11.490754127502441
+ ],
+ [
+ "▁termen",
+ -11.490838050842285
+ ],
+ [
+ "▁Linux",
+ -11.491013526916504
+ ],
+ [
+ "▁1-",
+ -11.491068840026855
+ ],
+ [
+ "▁hätte",
+ -11.491202354431152
+ ],
+ [
+ "▁Resort",
+ -11.49129867553711
+ ],
+ [
+ "image",
+ -11.491527557373047
+ ],
+ [
+ "▁Rod",
+ -11.49189281463623
+ ],
+ [
+ "▁Fly",
+ -11.491924285888672
+ ],
+ [
+ "try",
+ -11.492317199707031
+ ],
+ [
+ "▁expense",
+ -11.49245834350586
+ ],
+ [
+ "▁Interior",
+ -11.492799758911133
+ ],
+ [
+ "▁fence",
+ -11.492920875549316
+ ],
+ [
+ "▁Kontakt",
+ -11.493063926696777
+ ],
+ [
+ "▁ALL",
+ -11.493142127990723
+ ],
+ [
+ "VA",
+ -11.493229866027832
+ ],
+ [
+ "▁Exchange",
+ -11.493316650390625
+ ],
+ [
+ "ranked",
+ -11.493558883666992
+ ],
+ [
+ "▁Performance",
+ -11.493621826171875
+ ],
+ [
+ "prim",
+ -11.493635177612305
+ ],
+ [
+ "▁basket",
+ -11.493694305419922
+ ],
+ [
+ "▁Vice",
+ -11.493703842163086
+ ],
+ [
+ "phan",
+ -11.4937105178833
+ ],
+ [
+ "▁broke",
+ -11.494003295898438
+ ],
+ [
+ "voir",
+ -11.49431324005127
+ ],
+ [
+ "arg",
+ -11.494512557983398
+ ],
+ [
+ "ART",
+ -11.494529724121094
+ ],
+ [
+ "▁floors",
+ -11.494856834411621
+ ],
+ [
+ "pression",
+ -11.495025634765625
+ ],
+ [
+ "▁possession",
+ -11.49507999420166
+ ],
+ [
+ "▁domaine",
+ -11.49510669708252
+ ],
+ [
+ "▁valeur",
+ -11.495132446289062
+ ],
+ [
+ "▁suddenly",
+ -11.495282173156738
+ ],
+ [
+ "▁mild",
+ -11.495304107666016
+ ],
+ [
+ "▁aflat",
+ -11.495431900024414
+ ],
+ [
+ "▁Tea",
+ -11.495731353759766
+ ],
+ [
+ "tritt",
+ -11.495767593383789
+ ],
+ [
+ "▁Mittel",
+ -11.495773315429688
+ ],
+ [
+ "▁regulatory",
+ -11.49580192565918
+ ],
+ [
+ "▁spectacular",
+ -11.495905876159668
+ ],
+ [
+ "fahrt",
+ -11.495949745178223
+ ],
+ [
+ "GS",
+ -11.496026039123535
+ ],
+ [
+ "MM",
+ -11.4961576461792
+ ],
+ [
+ "▁environments",
+ -11.496203422546387
+ ],
+ [
+ "▁Raum",
+ -11.496381759643555
+ ],
+ [
+ "▁lay",
+ -11.496664047241211
+ ],
+ [
+ "▁cré",
+ -11.496713638305664
+ ],
+ [
+ "▁Selbst",
+ -11.496726989746094
+ ],
+ [
+ "▁opposition",
+ -11.496821403503418
+ ],
+ [
+ "two",
+ -11.49729061126709
+ ],
+ [
+ "▁Clark",
+ -11.497822761535645
+ ],
+ [
+ "▁Netz",
+ -11.497845649719238
+ ],
+ [
+ "bald",
+ -11.497983932495117
+ ],
+ [
+ "▁Innovation",
+ -11.4982271194458
+ ],
+ [
+ "▁overcome",
+ -11.49825382232666
+ ],
+ [
+ "quot",
+ -11.499013900756836
+ ],
+ [
+ "▁Sin",
+ -11.499106407165527
+ ],
+ [
+ "▁Sto",
+ -11.499320983886719
+ ],
+ [
+ "▁grain",
+ -11.499560356140137
+ ],
+ [
+ "▁collections",
+ -11.499724388122559
+ ],
+ [
+ "▁applies",
+ -11.49986743927002
+ ],
+ [
+ "mach",
+ -11.499934196472168
+ ],
+ [
+ "▁wheels",
+ -11.499958992004395
+ ],
+ [
+ "▁universities",
+ -11.500049591064453
+ ],
+ [
+ "▁Ray",
+ -11.500182151794434
+ ],
+ [
+ "lina",
+ -11.500238418579102
+ ],
+ [
+ "▁arrangements",
+ -11.500393867492676
+ ],
+ [
+ "▁western",
+ -11.500728607177734
+ ],
+ [
+ "rous",
+ -11.500768661499023
+ ],
+ [
+ "aise",
+ -11.500784873962402
+ ],
+ [
+ "▁highlights",
+ -11.50112533569336
+ ],
+ [
+ "▁intend",
+ -11.501265525817871
+ ],
+ [
+ "aimed",
+ -11.501358032226562
+ ],
+ [
+ "▁Scotland",
+ -11.501360893249512
+ ],
+ [
+ "▁acestei",
+ -11.501466751098633
+ ],
+ [
+ "graf",
+ -11.50150203704834
+ ],
+ [
+ "duction",
+ -11.501517295837402
+ ],
+ [
+ "path",
+ -11.50156021118164
+ ],
+ [
+ "▁evil",
+ -11.501633644104004
+ ],
+ [
+ "▁scris",
+ -11.501791000366211
+ ],
+ [
+ "▁disposition",
+ -11.501927375793457
+ ],
+ [
+ "▁designing",
+ -11.5020751953125
+ ],
+ [
+ "zwar",
+ -11.502172470092773
+ ],
+ [
+ "▁Retrieve",
+ -11.50217342376709
+ ],
+ [
+ "▁aggressive",
+ -11.502374649047852
+ ],
+ [
+ "▁Glen",
+ -11.502411842346191
+ ],
+ [
+ "▁daher",
+ -11.502473831176758
+ ],
+ [
+ "▁Quick",
+ -11.502494812011719
+ ],
+ [
+ "▁recover",
+ -11.502632141113281
+ ],
+ [
+ "▁prominent",
+ -11.50288200378418
+ ],
+ [
+ "▁visits",
+ -11.503198623657227
+ ],
+ [
+ "▁Mis",
+ -11.503376960754395
+ ],
+ [
+ "▁edited",
+ -11.503456115722656
+ ],
+ [
+ "▁distributed",
+ -11.503564834594727
+ ],
+ [
+ "▁dés",
+ -11.503580093383789
+ ],
+ [
+ "▁alter",
+ -11.5035982131958
+ ],
+ [
+ "▁cooked",
+ -11.503697395324707
+ ],
+ [
+ "embl",
+ -11.503706932067871
+ ],
+ [
+ "Univers",
+ -11.503715515136719
+ ],
+ [
+ "▁Minuten",
+ -11.504156112670898
+ ],
+ [
+ "▁compris",
+ -11.504179954528809
+ ],
+ [
+ "rais",
+ -11.504182815551758
+ ],
+ [
+ "essentially",
+ -11.504199028015137
+ ],
+ [
+ "▁rel",
+ -11.504340171813965
+ ],
+ [
+ "▁appel",
+ -11.504570007324219
+ ],
+ [
+ "▁trace",
+ -11.504788398742676
+ ],
+ [
+ "relating",
+ -11.504830360412598
+ ],
+ [
+ "dès",
+ -11.504937171936035
+ ],
+ [
+ "aste",
+ -11.504961013793945
+ ],
+ [
+ "▁raison",
+ -11.504963874816895
+ ],
+ [
+ "▁frequent",
+ -11.505281448364258
+ ],
+ [
+ "▁beds",
+ -11.505316734313965
+ ],
+ [
+ "▁Miami",
+ -11.505511283874512
+ ],
+ [
+ "▁vibrant",
+ -11.50564193725586
+ ],
+ [
+ "▁Kam",
+ -11.505721092224121
+ ],
+ [
+ "▁klar",
+ -11.505861282348633
+ ],
+ [
+ "▁Tan",
+ -11.50598430633545
+ ],
+ [
+ "▁vidéo",
+ -11.506032943725586
+ ],
+ [
+ "▁Kur",
+ -11.506115913391113
+ ],
+ [
+ "▁themes",
+ -11.506134033203125
+ ],
+ [
+ "▁struggling",
+ -11.506440162658691
+ ],
+ [
+ "▁Magazine",
+ -11.506444931030273
+ ],
+ [
+ "maker",
+ -11.506476402282715
+ ],
+ [
+ "veni",
+ -11.506564140319824
+ ],
+ [
+ "▁Groß",
+ -11.506732940673828
+ ],
+ [
+ "▁streaming",
+ -11.506772994995117
+ ],
+ [
+ "▁analyze",
+ -11.506876945495605
+ ],
+ [
+ "▁titles",
+ -11.506982803344727
+ ],
+ [
+ "pier",
+ -11.507316589355469
+ ],
+ [
+ "▁participant",
+ -11.507347106933594
+ ],
+ [
+ "aims",
+ -11.507607460021973
+ ],
+ [
+ "▁convention",
+ -11.507638931274414
+ ],
+ [
+ "▁flood",
+ -11.507780075073242
+ ],
+ [
+ "▁nights",
+ -11.507842063903809
+ ],
+ [
+ "▁titre",
+ -11.50792407989502
+ ],
+ [
+ "▁voul",
+ -11.508010864257812
+ ],
+ [
+ "weit",
+ -11.50816822052002
+ ],
+ [
+ "where",
+ -11.508213996887207
+ ],
+ [
+ "▁Seiten",
+ -11.508286476135254
+ ],
+ [
+ "▁relaxing",
+ -11.508628845214844
+ ],
+ [
+ "▁piano",
+ -11.50883674621582
+ ],
+ [
+ "▁Pick",
+ -11.508842468261719
+ ],
+ [
+ "▁Sony",
+ -11.508955001831055
+ ],
+ [
+ "▁enhanced",
+ -11.509017944335938
+ ],
+ [
+ "▁visa",
+ -11.50915241241455
+ ],
+ [
+ "CH",
+ -11.50930118560791
+ ],
+ [
+ "▁instantly",
+ -11.50930404663086
+ ],
+ [
+ "▁Fan",
+ -11.509721755981445
+ ],
+ [
+ "▁diabetes",
+ -11.509988784790039
+ ],
+ [
+ "▁popul",
+ -11.50999641418457
+ ],
+ [
+ "Ang",
+ -11.510232925415039
+ ],
+ [
+ "▁Ask",
+ -11.510295867919922
+ ],
+ [
+ "cate",
+ -11.510650634765625
+ ],
+ [
+ "▁simplu",
+ -11.510666847229004
+ ],
+ [
+ "nahme",
+ -11.510685920715332
+ ],
+ [
+ "▁dentist",
+ -11.510842323303223
+ ],
+ [
+ "ubi",
+ -11.510920524597168
+ ],
+ [
+ "article",
+ -11.511030197143555
+ ],
+ [
+ "▁graph",
+ -11.511094093322754
+ ],
+ [
+ "▁rival",
+ -11.51121711730957
+ ],
+ [
+ "jahr",
+ -11.5113525390625
+ ],
+ [
+ "▁bloc",
+ -11.511370658874512
+ ],
+ [
+ "fern",
+ -11.511427879333496
+ ],
+ [
+ "▁dispar",
+ -11.511516571044922
+ ],
+ [
+ "▁servers",
+ -11.511582374572754
+ ],
+ [
+ "▁patru",
+ -11.511610984802246
+ ],
+ [
+ "▁Within",
+ -11.511634826660156
+ ],
+ [
+ "▁situated",
+ -11.511896133422852
+ ],
+ [
+ "▁HR",
+ -11.511981964111328
+ ],
+ [
+ "▁leaf",
+ -11.511981964111328
+ ],
+ [
+ "▁curs",
+ -11.512049674987793
+ ],
+ [
+ "antes",
+ -11.512325286865234
+ ],
+ [
+ "lux",
+ -11.512406349182129
+ ],
+ [
+ "▁1993",
+ -11.512463569641113
+ ],
+ [
+ "stance",
+ -11.512650489807129
+ ],
+ [
+ "▁northern",
+ -11.512683868408203
+ ],
+ [
+ "lves",
+ -11.512718200683594
+ ],
+ [
+ "▁contractors",
+ -11.512882232666016
+ ],
+ [
+ "▁dimensions",
+ -11.512920379638672
+ ],
+ [
+ "▁rolling",
+ -11.513068199157715
+ ],
+ [
+ "▁automobile",
+ -11.513211250305176
+ ],
+ [
+ "▁cru",
+ -11.51342487335205
+ ],
+ [
+ "▁displays",
+ -11.513570785522461
+ ],
+ [
+ "web",
+ -11.513812065124512
+ ],
+ [
+ "had",
+ -11.513850212097168
+ ],
+ [
+ "▁Never",
+ -11.513893127441406
+ ],
+ [
+ "▁2-",
+ -11.513932228088379
+ ],
+ [
+ "vine",
+ -11.51393985748291
+ ],
+ [
+ "▁Wahl",
+ -11.513975143432617
+ ],
+ [
+ "▁Markt",
+ -11.514166831970215
+ ],
+ [
+ "▁Double",
+ -11.514227867126465
+ ],
+ [
+ "▁acknowledge",
+ -11.514229774475098
+ ],
+ [
+ "stal",
+ -11.514288902282715
+ ],
+ [
+ "▁equity",
+ -11.514620780944824
+ ],
+ [
+ "▁ministry",
+ -11.514823913574219
+ ],
+ [
+ "▁Lor",
+ -11.514875411987305
+ ],
+ [
+ "▁sud",
+ -11.514968872070312
+ ],
+ [
+ "idée",
+ -11.515044212341309
+ ],
+ [
+ "▁measured",
+ -11.515448570251465
+ ],
+ [
+ "▁editing",
+ -11.515609741210938
+ ],
+ [
+ "▁singur",
+ -11.515620231628418
+ ],
+ [
+ "▁coal",
+ -11.515623092651367
+ ],
+ [
+ "▁dramatic",
+ -11.516212463378906
+ ],
+ [
+ "AG",
+ -11.516251564025879
+ ],
+ [
+ "asca",
+ -11.516280174255371
+ ],
+ [
+ "▁crash",
+ -11.516321182250977
+ ],
+ [
+ "ischer",
+ -11.516597747802734
+ ],
+ [
+ "▁Pla",
+ -11.516871452331543
+ ],
+ [
+ "▁psycho",
+ -11.517054557800293
+ ],
+ [
+ "piece",
+ -11.517118453979492
+ ],
+ [
+ "▁finger",
+ -11.517121315002441
+ ],
+ [
+ "▁Hollywood",
+ -11.517123222351074
+ ],
+ [
+ "▁Cr",
+ -11.517345428466797
+ ],
+ [
+ "▁locally",
+ -11.517622947692871
+ ],
+ [
+ "▁mouse",
+ -11.517792701721191
+ ],
+ [
+ "▁Base",
+ -11.517867088317871
+ ],
+ [
+ "uite",
+ -11.518095016479492
+ ],
+ [
+ "▁detect",
+ -11.518099784851074
+ ],
+ [
+ "cea",
+ -11.518150329589844
+ ],
+ [
+ "▁bull",
+ -11.518194198608398
+ ],
+ [
+ "▁curve",
+ -11.518208503723145
+ ],
+ [
+ "été",
+ -11.518218994140625
+ ],
+ [
+ "ddle",
+ -11.51839542388916
+ ],
+ [
+ "▁span",
+ -11.518523216247559
+ ],
+ [
+ "WS",
+ -11.518878936767578
+ ],
+ [
+ "CL",
+ -11.519017219543457
+ ],
+ [
+ "▁officially",
+ -11.519042015075684
+ ],
+ [
+ "▁corect",
+ -11.519168853759766
+ ],
+ [
+ "▁Artikel",
+ -11.5193510055542
+ ],
+ [
+ "▁customized",
+ -11.520099639892578
+ ],
+ [
+ "▁intellectual",
+ -11.52018928527832
+ ],
+ [
+ "▁heures",
+ -11.520334243774414
+ ],
+ [
+ "schule",
+ -11.520444869995117
+ ],
+ [
+ "▁investing",
+ -11.520585060119629
+ ],
+ [
+ "▁parallel",
+ -11.521227836608887
+ ],
+ [
+ "▁loi",
+ -11.521263122558594
+ ],
+ [
+ "ările",
+ -11.521566390991211
+ ],
+ [
+ "р",
+ -11.521679878234863
+ ],
+ [
+ "▁bench",
+ -11.521724700927734
+ ],
+ [
+ "▁principle",
+ -11.521756172180176
+ ],
+ [
+ "▁Galaxy",
+ -11.521829605102539
+ ],
+ [
+ "ța",
+ -11.522237777709961
+ ],
+ [
+ "▁(4",
+ -11.522418975830078
+ ],
+ [
+ "▁bedrooms",
+ -11.522578239440918
+ ],
+ [
+ "née",
+ -11.52273941040039
+ ],
+ [
+ "▁surely",
+ -11.52275276184082
+ ],
+ [
+ "very",
+ -11.522927284240723
+ ],
+ [
+ "stelle",
+ -11.523200988769531
+ ],
+ [
+ "activ",
+ -11.523216247558594
+ ],
+ [
+ "cite",
+ -11.523551940917969
+ ],
+ [
+ "▁Original",
+ -11.523553848266602
+ ],
+ [
+ "▁palm",
+ -11.523665428161621
+ ],
+ [
+ "▁losses",
+ -11.523934364318848
+ ],
+ [
+ "▁newspaper",
+ -11.524153709411621
+ ],
+ [
+ "ciu",
+ -11.52436351776123
+ ],
+ [
+ "▁Hold",
+ -11.524392127990723
+ ],
+ [
+ "BO",
+ -11.524422645568848
+ ],
+ [
+ "▁CON",
+ -11.524598121643066
+ ],
+ [
+ "▁modified",
+ -11.524624824523926
+ ],
+ [
+ "▁stake",
+ -11.524735450744629
+ ],
+ [
+ "▁Ton",
+ -11.524798393249512
+ ],
+ [
+ "▁luna",
+ -11.524968147277832
+ ],
+ [
+ "▁Mind",
+ -11.525094985961914
+ ],
+ [
+ "lap",
+ -11.525150299072266
+ ],
+ [
+ "▁opinions",
+ -11.525247573852539
+ ],
+ [
+ "▁Jordan",
+ -11.525351524353027
+ ],
+ [
+ "div",
+ -11.52537727355957
+ ],
+ [
+ "indi",
+ -11.525418281555176
+ ],
+ [
+ "▁Story",
+ -11.525476455688477
+ ],
+ [
+ "▁affiliate",
+ -11.52585506439209
+ ],
+ [
+ "▁matière",
+ -11.525918960571289
+ ],
+ [
+ "▁fifth",
+ -11.526399612426758
+ ],
+ [
+ "▁sheets",
+ -11.52645492553711
+ ],
+ [
+ "▁puțin",
+ -11.526909828186035
+ ],
+ [
+ "ush",
+ -11.526947021484375
+ ],
+ [
+ "geführt",
+ -11.526993751525879
+ ],
+ [
+ "▁Falls",
+ -11.527168273925781
+ ],
+ [
+ "legi",
+ -11.527295112609863
+ ],
+ [
+ "▁auction",
+ -11.527326583862305
+ ],
+ [
+ "▁cooperation",
+ -11.52735424041748
+ ],
+ [
+ "▁Fee",
+ -11.527474403381348
+ ],
+ [
+ "▁Daily",
+ -11.52774715423584
+ ],
+ [
+ "pies",
+ -11.527853965759277
+ ],
+ [
+ "▁basketball",
+ -11.527976036071777
+ ],
+ [
+ "removing",
+ -11.528056144714355
+ ],
+ [
+ "Besides",
+ -11.528294563293457
+ ],
+ [
+ "▁Body",
+ -11.528355598449707
+ ],
+ [
+ "▁AD",
+ -11.528369903564453
+ ],
+ [
+ "RU",
+ -11.528435707092285
+ ],
+ [
+ "ţia",
+ -11.52894401550293
+ ],
+ [
+ "▁Extra",
+ -11.528986930847168
+ ],
+ [
+ "▁Practice",
+ -11.52900218963623
+ ],
+ [
+ "▁Jeff",
+ -11.529017448425293
+ ],
+ [
+ "▁început",
+ -11.529253005981445
+ ],
+ [
+ "ching",
+ -11.529269218444824
+ ],
+ [
+ "▁Gift",
+ -11.529281616210938
+ ],
+ [
+ "kk",
+ -11.529295921325684
+ ],
+ [
+ "\")",
+ -11.529349327087402
+ ],
+ [
+ "▁Austin",
+ -11.529651641845703
+ ],
+ [
+ "thro",
+ -11.529766082763672
+ ],
+ [
+ "▁camping",
+ -11.529810905456543
+ ],
+ [
+ "▁theatre",
+ -11.529850959777832
+ ],
+ [
+ "école",
+ -11.529916763305664
+ ],
+ [
+ "vient",
+ -11.530159950256348
+ ],
+ [
+ "▁faces",
+ -11.530226707458496
+ ],
+ [
+ "▁constructed",
+ -11.530437469482422
+ ],
+ [
+ "▁overnight",
+ -11.530472755432129
+ ],
+ [
+ "▁locale",
+ -11.530574798583984
+ ],
+ [
+ "▁roots",
+ -11.530611038208008
+ ],
+ [
+ "▁bu",
+ -11.530662536621094
+ ],
+ [
+ "4,",
+ -11.530683517456055
+ ],
+ [
+ "▁Enterprise",
+ -11.530865669250488
+ ],
+ [
+ "screen",
+ -11.530935287475586
+ ],
+ [
+ "▁Chef",
+ -11.53096866607666
+ ],
+ [
+ "▁Along",
+ -11.531298637390137
+ ],
+ [
+ "▁MD",
+ -11.531431198120117
+ ],
+ [
+ "▁Supreme",
+ -11.531597137451172
+ ],
+ [
+ "En",
+ -11.531655311584473
+ ],
+ [
+ "▁verwendet",
+ -11.532015800476074
+ ],
+ [
+ "▁processed",
+ -11.532425880432129
+ ],
+ [
+ "▁vendors",
+ -11.532549858093262
+ ],
+ [
+ "▁FA",
+ -11.532651901245117
+ ],
+ [
+ "▁44",
+ -11.532716751098633
+ ],
+ [
+ "▁beautifully",
+ -11.532933235168457
+ ],
+ [
+ "▁eficient",
+ -11.533092498779297
+ ],
+ [
+ "▁Wil",
+ -11.533117294311523
+ ],
+ [
+ "▁Member",
+ -11.533121109008789
+ ],
+ [
+ "▁damages",
+ -11.5332670211792
+ ],
+ [
+ "▁mutual",
+ -11.533288955688477
+ ],
+ [
+ "SN",
+ -11.533506393432617
+ ],
+ [
+ "▁Dave",
+ -11.533665657043457
+ ],
+ [
+ "??",
+ -11.533998489379883
+ ],
+ [
+ "stat",
+ -11.534090995788574
+ ],
+ [
+ "▁tourist",
+ -11.534374237060547
+ ],
+ [
+ "fie",
+ -11.534425735473633
+ ],
+ [
+ "şte",
+ -11.534754753112793
+ ],
+ [
+ "▁donne",
+ -11.534764289855957
+ ],
+ [
+ "▁shadow",
+ -11.53493881225586
+ ],
+ [
+ "▁dough",
+ -11.534993171691895
+ ],
+ [
+ "▁Gro",
+ -11.535002708435059
+ ],
+ [
+ "▁Mah",
+ -11.535066604614258
+ ],
+ [
+ "RF",
+ -11.535126686096191
+ ],
+ [
+ "▁mechanism",
+ -11.535163879394531
+ ],
+ [
+ "▁2011,",
+ -11.535179138183594
+ ],
+ [
+ "▁Alter",
+ -11.53530502319336
+ ],
+ [
+ "▁opposed",
+ -11.53538990020752
+ ],
+ [
+ "▁Fri",
+ -11.535501480102539
+ ],
+ [
+ "▁remarkable",
+ -11.535572052001953
+ ],
+ [
+ "oral",
+ -11.535635948181152
+ ],
+ [
+ "▁verschiedene",
+ -11.535653114318848
+ ],
+ [
+ "▁difficulty",
+ -11.535691261291504
+ ],
+ [
+ "▁Application",
+ -11.535840034484863
+ ],
+ [
+ "▁Hay",
+ -11.535888671875
+ ],
+ [
+ "▁continua",
+ -11.535935401916504
+ ],
+ [
+ "EP",
+ -11.53609848022461
+ ],
+ [
+ "▁Pr",
+ -11.53617000579834
+ ],
+ [
+ "▁Lady",
+ -11.53631591796875
+ ],
+ [
+ "▁interval",
+ -11.536457061767578
+ ],
+ [
+ "▁Mil",
+ -11.536504745483398
+ ],
+ [
+ "▁2010.",
+ -11.537042617797852
+ ],
+ [
+ "VE",
+ -11.537074089050293
+ ],
+ [
+ "integr",
+ -11.537360191345215
+ ],
+ [
+ "▁création",
+ -11.537415504455566
+ ],
+ [
+ "weed",
+ -11.537456512451172
+ ],
+ [
+ "EG",
+ -11.53760051727295
+ ],
+ [
+ "▁6,",
+ -11.537784576416016
+ ],
+ [
+ "▁god",
+ -11.537866592407227
+ ],
+ [
+ "▁accomplish",
+ -11.537947654724121
+ ],
+ [
+ "▁thoroughly",
+ -11.538019180297852
+ ],
+ [
+ "2019",
+ -11.538228988647461
+ ],
+ [
+ "izer",
+ -11.538246154785156
+ ],
+ [
+ "▁Wal",
+ -11.538300514221191
+ ],
+ [
+ "ifying",
+ -11.538701057434082
+ ],
+ [
+ "▁Wohn",
+ -11.539227485656738
+ ],
+ [
+ "▁Holz",
+ -11.539474487304688
+ ],
+ [
+ "▁Advanced",
+ -11.539528846740723
+ ],
+ [
+ "▁honey",
+ -11.539626121520996
+ ],
+ [
+ "proof",
+ -11.539634704589844
+ ],
+ [
+ "▁saison",
+ -11.540029525756836
+ ],
+ [
+ "ându",
+ -11.540035247802734
+ ],
+ [
+ "▁Kevin",
+ -11.540116310119629
+ ],
+ [
+ "▁shelter",
+ -11.540199279785156
+ ],
+ [
+ "▁discut",
+ -11.540257453918457
+ ],
+ [
+ "▁hike",
+ -11.540257453918457
+ ],
+ [
+ "ités",
+ -11.540461540222168
+ ],
+ [
+ "▁boutique",
+ -11.540672302246094
+ ],
+ [
+ "▁Email",
+ -11.54067611694336
+ ],
+ [
+ "▁cosmetic",
+ -11.540830612182617
+ ],
+ [
+ "dian",
+ -11.540916442871094
+ ],
+ [
+ "▁hohe",
+ -11.540940284729004
+ ],
+ [
+ "▁absence",
+ -11.541071891784668
+ ],
+ [
+ "axi",
+ -11.541136741638184
+ ],
+ [
+ "nah",
+ -11.541178703308105
+ ],
+ [
+ "▁Frauen",
+ -11.541236877441406
+ ],
+ [
+ "▁actively",
+ -11.541278839111328
+ ],
+ [
+ "bind",
+ -11.541468620300293
+ ],
+ [
+ "▁everybody",
+ -11.541740417480469
+ ],
+ [
+ "▁controller",
+ -11.541802406311035
+ ],
+ [
+ "▁1.5",
+ -11.5418062210083
+ ],
+ [
+ "erau",
+ -11.541842460632324
+ ],
+ [
+ "gehen",
+ -11.541988372802734
+ ],
+ [
+ "▁scenario",
+ -11.542038917541504
+ ],
+ [
+ "▁odd",
+ -11.542083740234375
+ ],
+ [
+ "▁Ultra",
+ -11.542089462280273
+ ],
+ [
+ "▁finishing",
+ -11.542366981506348
+ ],
+ [
+ "▁cuts",
+ -11.542383193969727
+ ],
+ [
+ "▁financing",
+ -11.542515754699707
+ ],
+ [
+ "▁Chance",
+ -11.542579650878906
+ ],
+ [
+ "surrounded",
+ -11.542818069458008
+ ],
+ [
+ "▁joc",
+ -11.542903900146484
+ ],
+ [
+ "▁shelf",
+ -11.543004035949707
+ ],
+ [
+ "tief",
+ -11.54308032989502
+ ],
+ [
+ "▁Sir",
+ -11.543146133422852
+ ],
+ [
+ "▁Agent",
+ -11.543197631835938
+ ],
+ [
+ "▁scratch",
+ -11.543560981750488
+ ],
+ [
+ "2,000",
+ -11.54360294342041
+ ],
+ [
+ "nutri",
+ -11.54365348815918
+ ],
+ [
+ "nier",
+ -11.544063568115234
+ ],
+ [
+ "▁Dur",
+ -11.544175148010254
+ ],
+ [
+ "▁grid",
+ -11.544268608093262
+ ],
+ [
+ "road",
+ -11.544413566589355
+ ],
+ [
+ "▁pets",
+ -11.544429779052734
+ ],
+ [
+ "stud",
+ -11.54448127746582
+ ],
+ [
+ "OM",
+ -11.544569969177246
+ ],
+ [
+ "Die",
+ -11.544877052307129
+ ],
+ [
+ "▁800",
+ -11.54496955871582
+ ],
+ [
+ "▁arrangement",
+ -11.545088768005371
+ ],
+ [
+ "▁Sri",
+ -11.545185089111328
+ ],
+ [
+ "▁Patrick",
+ -11.545187950134277
+ ],
+ [
+ "ava",
+ -11.545212745666504
+ ],
+ [
+ "▁pension",
+ -11.54523754119873
+ ],
+ [
+ "dung",
+ -11.545353889465332
+ ],
+ [
+ "▁Chapter",
+ -11.545475006103516
+ ],
+ [
+ "▁Property",
+ -11.545475006103516
+ ],
+ [
+ "▁structural",
+ -11.545571327209473
+ ],
+ [
+ "▁overview",
+ -11.545731544494629
+ ],
+ [
+ "2015",
+ -11.545917510986328
+ ],
+ [
+ "▁lawn",
+ -11.545924186706543
+ ],
+ [
+ "▁Vin",
+ -11.546219825744629
+ ],
+ [
+ "lik",
+ -11.546402931213379
+ ],
+ [
+ "dus",
+ -11.546418190002441
+ ],
+ [
+ "Several",
+ -11.54654598236084
+ ],
+ [
+ "▁Bou",
+ -11.546670913696289
+ ],
+ [
+ "▁copper",
+ -11.546703338623047
+ ],
+ [
+ "▁duration",
+ -11.546867370605469
+ ],
+ [
+ "inate",
+ -11.546982765197754
+ ],
+ [
+ "▁podcast",
+ -11.547204971313477
+ ],
+ [
+ "▁Self",
+ -11.547208786010742
+ ],
+ [
+ "▁Construction",
+ -11.547491073608398
+ ],
+ [
+ "achat",
+ -11.54768180847168
+ ],
+ [
+ "???",
+ -11.547683715820312
+ ],
+ [
+ "▁Electric",
+ -11.547974586486816
+ ],
+ [
+ "▁Mrs",
+ -11.54799747467041
+ ],
+ [
+ "▁CT",
+ -11.548019409179688
+ ],
+ [
+ "▁proceed",
+ -11.548324584960938
+ ],
+ [
+ "▁Course",
+ -11.548333168029785
+ ],
+ [
+ "▁Frei",
+ -11.548699378967285
+ ],
+ [
+ "▁heavily",
+ -11.548868179321289
+ ],
+ [
+ "rique",
+ -11.548872947692871
+ ],
+ [
+ "version",
+ -11.549016952514648
+ ],
+ [
+ "▁representatives",
+ -11.549118041992188
+ ],
+ [
+ "▁tourism",
+ -11.549182891845703
+ ],
+ [
+ "▁shirt",
+ -11.5494966506958
+ ],
+ [
+ "▁rough",
+ -11.549507141113281
+ ],
+ [
+ "▁weniger",
+ -11.549735069274902
+ ],
+ [
+ "▁keyboard",
+ -11.550058364868164
+ ],
+ [
+ "▁heritage",
+ -11.550149917602539
+ ],
+ [
+ "kat",
+ -11.550535202026367
+ ],
+ [
+ "assez",
+ -11.550567626953125
+ ],
+ [
+ "▁cabinets",
+ -11.550591468811035
+ ],
+ [
+ "▁Komm",
+ -11.550762176513672
+ ],
+ [
+ "▁impressed",
+ -11.55078411102295
+ ],
+ [
+ "▁Oregon",
+ -11.550788879394531
+ ],
+ [
+ "▁Davis",
+ -11.55081558227539
+ ],
+ [
+ "specialized",
+ -11.55097770690918
+ ],
+ [
+ "▁gross",
+ -11.550999641418457
+ ],
+ [
+ "Located",
+ -11.551044464111328
+ ],
+ [
+ "ttle",
+ -11.551044464111328
+ ],
+ [
+ "▁2010,",
+ -11.551224708557129
+ ],
+ [
+ "chan",
+ -11.551253318786621
+ ],
+ [
+ "mine",
+ -11.551305770874023
+ ],
+ [
+ "▁aduce",
+ -11.551637649536133
+ ],
+ [
+ "▁subsequent",
+ -11.551729202270508
+ ],
+ [
+ "▁demo",
+ -11.551851272583008
+ ],
+ [
+ "aba",
+ -11.552209854125977
+ ],
+ [
+ "▁shock",
+ -11.552389144897461
+ ],
+ [
+ "▁theater",
+ -11.552854537963867
+ ],
+ [
+ "▁engineers",
+ -11.55294418334961
+ ],
+ [
+ "▁feu",
+ -11.553037643432617
+ ],
+ [
+ "▁Rot",
+ -11.553058624267578
+ ],
+ [
+ "▁addressed",
+ -11.553155899047852
+ ],
+ [
+ "▁Letter",
+ -11.553431510925293
+ ],
+ [
+ "gré",
+ -11.553448677062988
+ ],
+ [
+ "▁quantity",
+ -11.553449630737305
+ ],
+ [
+ "▁Seit",
+ -11.553640365600586
+ ],
+ [
+ "▁bacteria",
+ -11.553681373596191
+ ],
+ [
+ "kg",
+ -11.55408000946045
+ ],
+ [
+ "▁conservation",
+ -11.554191589355469
+ ],
+ [
+ "▁entreprises",
+ -11.55420207977295
+ ],
+ [
+ "▁pleasant",
+ -11.554207801818848
+ ],
+ [
+ "armed",
+ -11.554228782653809
+ ],
+ [
+ "dorf",
+ -11.554286003112793
+ ],
+ [
+ "fact",
+ -11.554320335388184
+ ],
+ [
+ "▁Much",
+ -11.554388046264648
+ ],
+ [
+ "▁laugh",
+ -11.55482006072998
+ ],
+ [
+ "▁blade",
+ -11.554835319519043
+ ],
+ [
+ "amine",
+ -11.554838180541992
+ ],
+ [
+ "▁insert",
+ -11.55493450164795
+ ],
+ [
+ "▁toys",
+ -11.555326461791992
+ ],
+ [
+ "▁в",
+ -11.555726051330566
+ ],
+ [
+ "cell",
+ -11.555747985839844
+ ],
+ [
+ "▁strengthen",
+ -11.555864334106445
+ ],
+ [
+ "GR",
+ -11.555882453918457
+ ],
+ [
+ "▁autor",
+ -11.556114196777344
+ ],
+ [
+ "▁LI",
+ -11.556147575378418
+ ],
+ [
+ "▁oamenii",
+ -11.556184768676758
+ ],
+ [
+ "▁Modell",
+ -11.556222915649414
+ ],
+ [
+ "▁sophisticated",
+ -11.556225776672363
+ ],
+ [
+ "▁Write",
+ -11.556283950805664
+ ],
+ [
+ "eți",
+ -11.556295394897461
+ ],
+ [
+ "say",
+ -11.556641578674316
+ ],
+ [
+ "▁nutzen",
+ -11.556783676147461
+ ],
+ [
+ "▁amenities",
+ -11.556979179382324
+ ],
+ [
+ "chel",
+ -11.557068824768066
+ ],
+ [
+ "Unlike",
+ -11.55720043182373
+ ],
+ [
+ "▁Bilder",
+ -11.557208061218262
+ ],
+ [
+ "fertig",
+ -11.55722713470459
+ ],
+ [
+ "PER",
+ -11.557244300842285
+ ],
+ [
+ "▁apparently",
+ -11.557282447814941
+ ],
+ [
+ "▁pointed",
+ -11.557332992553711
+ ],
+ [
+ "lop",
+ -11.557435989379883
+ ],
+ [
+ "▁commande",
+ -11.557848930358887
+ ],
+ [
+ "▁NEW",
+ -11.557923316955566
+ ],
+ [
+ "▁primi",
+ -11.55798625946045
+ ],
+ [
+ "▁aluminum",
+ -11.558046340942383
+ ],
+ [
+ "ificare",
+ -11.558063507080078
+ ],
+ [
+ "open",
+ -11.55815315246582
+ ],
+ [
+ "▁establishment",
+ -11.558305740356445
+ ],
+ [
+ "▁blanc",
+ -11.558349609375
+ ],
+ [
+ "▁1960",
+ -11.558454513549805
+ ],
+ [
+ "▁parameters",
+ -11.55856990814209
+ ],
+ [
+ "schluss",
+ -11.558685302734375
+ ],
+ [
+ "▁jet",
+ -11.55879020690918
+ ],
+ [
+ "gam",
+ -11.55902099609375
+ ],
+ [
+ "▁oral",
+ -11.559290885925293
+ ],
+ [
+ "▁tons",
+ -11.559348106384277
+ ],
+ [
+ "▁AL",
+ -11.55935001373291
+ ],
+ [
+ "▁intention",
+ -11.55947494506836
+ ],
+ [
+ "ives",
+ -11.55974292755127
+ ],
+ [
+ "▁BMW",
+ -11.559837341308594
+ ],
+ [
+ "gun",
+ -11.559967041015625
+ ],
+ [
+ "leben",
+ -11.560046195983887
+ ],
+ [
+ "▁Fresh",
+ -11.56010913848877
+ ],
+ [
+ "▁tuturor",
+ -11.560193061828613
+ ],
+ [
+ "▁marine",
+ -11.560208320617676
+ ],
+ [
+ "mile",
+ -11.560260772705078
+ ],
+ [
+ "▁alta",
+ -11.560271263122559
+ ],
+ [
+ "nnen",
+ -11.56050968170166
+ ],
+ [
+ "▁courts",
+ -11.560530662536621
+ ],
+ [
+ "▁Hello",
+ -11.560791015625
+ ],
+ [
+ "BL",
+ -11.560895919799805
+ ],
+ [
+ "▁reply",
+ -11.560962677001953
+ ],
+ [
+ "environnement",
+ -11.560975074768066
+ ],
+ [
+ "American",
+ -11.560995101928711
+ ],
+ [
+ "▁Tell",
+ -11.561040878295898
+ ],
+ [
+ "▁chic",
+ -11.56148624420166
+ ],
+ [
+ "bir",
+ -11.561542510986328
+ ],
+ [
+ "▁singing",
+ -11.561788558959961
+ ],
+ [
+ "▁earnings",
+ -11.561819076538086
+ ],
+ [
+ "▁ensemble",
+ -11.562082290649414
+ ],
+ [
+ "▁($",
+ -11.562169075012207
+ ],
+ [
+ "▁Tout",
+ -11.562192916870117
+ ],
+ [
+ "▁Abs",
+ -11.562264442443848
+ ],
+ [
+ "▁describes",
+ -11.562322616577148
+ ],
+ [
+ "▁navigation",
+ -11.5625
+ ],
+ [
+ "▁destul",
+ -11.562532424926758
+ ],
+ [
+ "legate",
+ -11.562586784362793
+ ],
+ [
+ "tral",
+ -11.562599182128906
+ ],
+ [
+ "aţie",
+ -11.562753677368164
+ ],
+ [
+ "▁supplied",
+ -11.562775611877441
+ ],
+ [
+ "▁paar",
+ -11.562911987304688
+ ],
+ [
+ "ionat",
+ -11.563241958618164
+ ],
+ [
+ "9.",
+ -11.563263893127441
+ ],
+ [
+ "▁41",
+ -11.563348770141602
+ ],
+ [
+ "▁Track",
+ -11.563451766967773
+ ],
+ [
+ "▁happiness",
+ -11.563636779785156
+ ],
+ [
+ "▁Personen",
+ -11.563680648803711
+ ],
+ [
+ "▁sac",
+ -11.56373119354248
+ ],
+ [
+ "▁shapes",
+ -11.563774108886719
+ ],
+ [
+ "eld",
+ -11.56393051147461
+ ],
+ [
+ "bett",
+ -11.563963890075684
+ ],
+ [
+ "tile",
+ -11.56400203704834
+ ],
+ [
+ "▁divided",
+ -11.564035415649414
+ ],
+ [
+ "▁13.",
+ -11.56403923034668
+ ],
+ [
+ "market",
+ -11.564109802246094
+ ],
+ [
+ "crafted",
+ -11.564115524291992
+ ],
+ [
+ "▁periods",
+ -11.564120292663574
+ ],
+ [
+ "uş",
+ -11.564568519592285
+ ],
+ [
+ "▁trainer",
+ -11.56460952758789
+ ],
+ [
+ "▁Licht",
+ -11.564871788024902
+ ],
+ [
+ "▁advisor",
+ -11.564948081970215
+ ],
+ [
+ "▁Herr",
+ -11.564980506896973
+ ],
+ [
+ "▁Halloween",
+ -11.565147399902344
+ ],
+ [
+ "alter",
+ -11.565154075622559
+ ],
+ [
+ "▁radical",
+ -11.565155029296875
+ ],
+ [
+ "▁nose",
+ -11.56527042388916
+ ],
+ [
+ "▁Sat",
+ -11.565323829650879
+ ],
+ [
+ "▁Mom",
+ -11.565372467041016
+ ],
+ [
+ "moni",
+ -11.565377235412598
+ ],
+ [
+ "▁semn",
+ -11.565397262573242
+ ],
+ [
+ "vé",
+ -11.565672874450684
+ ],
+ [
+ "identifie",
+ -11.56570053100586
+ ],
+ [
+ "▁hatten",
+ -11.565957069396973
+ ],
+ [
+ "completing",
+ -11.565959930419922
+ ],
+ [
+ "▁gust",
+ -11.565963745117188
+ ],
+ [
+ "▁creat",
+ -11.56601333618164
+ ],
+ [
+ "ché",
+ -11.566075325012207
+ ],
+ [
+ "pay",
+ -11.566216468811035
+ ],
+ [
+ "▁Money",
+ -11.566229820251465
+ ],
+ [
+ "IG",
+ -11.566243171691895
+ ],
+ [
+ "▁Cash",
+ -11.566327095031738
+ ],
+ [
+ "altă",
+ -11.566420555114746
+ ],
+ [
+ "▁bekommen",
+ -11.566620826721191
+ ],
+ [
+ "▁43",
+ -11.56662654876709
+ ],
+ [
+ "▁supplement",
+ -11.566637992858887
+ ],
+ [
+ "▁Early",
+ -11.566754341125488
+ ],
+ [
+ "▁mattress",
+ -11.56692123413086
+ ],
+ [
+ "▁worn",
+ -11.567182540893555
+ ],
+ [
+ "rov",
+ -11.567197799682617
+ ],
+ [
+ "▁pray",
+ -11.56733226776123
+ ],
+ [
+ "▁beans",
+ -11.567673683166504
+ ],
+ [
+ "▁passé",
+ -11.567782402038574
+ ],
+ [
+ "▁facilit",
+ -11.56782054901123
+ ],
+ [
+ "▁meters",
+ -11.56784439086914
+ ],
+ [
+ "cke",
+ -11.568163871765137
+ ],
+ [
+ "▁Villa",
+ -11.568199157714844
+ ],
+ [
+ "▁Diego",
+ -11.568217277526855
+ ],
+ [
+ "▁chips",
+ -11.568244934082031
+ ],
+ [
+ "▁mes",
+ -11.568349838256836
+ ],
+ [
+ "▁Seattle",
+ -11.568421363830566
+ ],
+ [
+ "BU",
+ -11.568621635437012
+ ],
+ [
+ "▁nevoi",
+ -11.568714141845703
+ ],
+ [
+ "▁lets",
+ -11.568737030029297
+ ],
+ [
+ "▁hopefully",
+ -11.56894302368164
+ ],
+ [
+ "▁AG",
+ -11.568954467773438
+ ],
+ [
+ "liable",
+ -11.568999290466309
+ ],
+ [
+ "pound",
+ -11.569067001342773
+ ],
+ [
+ "près",
+ -11.569085121154785
+ ],
+ [
+ "arul",
+ -11.56920337677002
+ ],
+ [
+ "isiert",
+ -11.569281578063965
+ ],
+ [
+ "▁Expert",
+ -11.569297790527344
+ ],
+ [
+ "▁particulier",
+ -11.569367408752441
+ ],
+ [
+ "stoff",
+ -11.569952964782715
+ ],
+ [
+ "▁interpretation",
+ -11.56999397277832
+ ],
+ [
+ "După",
+ -11.57007884979248
+ ],
+ [
+ "sait",
+ -11.57011604309082
+ ],
+ [
+ "▁nouvelles",
+ -11.570173263549805
+ ],
+ [
+ "▁Ok",
+ -11.570175170898438
+ ],
+ [
+ "tap",
+ -11.570301055908203
+ ],
+ [
+ "▁targets",
+ -11.570327758789062
+ ],
+ [
+ "rung",
+ -11.57052230834961
+ ],
+ [
+ "▁stare",
+ -11.570576667785645
+ ],
+ [
+ "▁efficiently",
+ -11.570908546447754
+ ],
+ [
+ "EV",
+ -11.571003913879395
+ ],
+ [
+ "évit",
+ -11.571310997009277
+ ],
+ [
+ "▁Moldova",
+ -11.571542739868164
+ ],
+ [
+ "▁Face",
+ -11.571663856506348
+ ],
+ [
+ "▁flo",
+ -11.57168960571289
+ ],
+ [
+ "▁acestora",
+ -11.5717134475708
+ ],
+ [
+ "▁Victor",
+ -11.57183837890625
+ ],
+ [
+ "▁breed",
+ -11.57198429107666
+ ],
+ [
+ "morph",
+ -11.572230339050293
+ ],
+ [
+ "sley",
+ -11.572274208068848
+ ],
+ [
+ "mot",
+ -11.57234001159668
+ ],
+ [
+ "▁URL",
+ -11.572395324707031
+ ],
+ [
+ "ellen",
+ -11.572502136230469
+ ],
+ [
+ "▁resist",
+ -11.572781562805176
+ ],
+ [
+ "zon",
+ -11.57282829284668
+ ],
+ [
+ "ndel",
+ -11.572967529296875
+ ],
+ [
+ "will",
+ -11.572989463806152
+ ],
+ [
+ "▁alege",
+ -11.573076248168945
+ ],
+ [
+ "▁Easter",
+ -11.573114395141602
+ ],
+ [
+ "▁Bat",
+ -11.573190689086914
+ ],
+ [
+ "▁Höhe",
+ -11.573223114013672
+ ],
+ [
+ "▁fascinating",
+ -11.573387145996094
+ ],
+ [
+ "▁Know",
+ -11.5735445022583
+ ],
+ [
+ "illon",
+ -11.573602676391602
+ ],
+ [
+ "flex",
+ -11.57363224029541
+ ],
+ [
+ "who",
+ -11.573701858520508
+ ],
+ [
+ "▁Always",
+ -11.573729515075684
+ ],
+ [
+ "▁Bush",
+ -11.573777198791504
+ ],
+ [
+ "ICE",
+ -11.574009895324707
+ ],
+ [
+ "verein",
+ -11.57448673248291
+ ],
+ [
+ "▁später",
+ -11.57448959350586
+ ],
+ [
+ "▁cherch",
+ -11.574575424194336
+ ],
+ [
+ "makers",
+ -11.574753761291504
+ ],
+ [
+ "versus",
+ -11.574790954589844
+ ],
+ [
+ "▁Clear",
+ -11.574846267700195
+ ],
+ [
+ "▁Pennsylvania",
+ -11.574912071228027
+ ],
+ [
+ "Dieser",
+ -11.575041770935059
+ ],
+ [
+ "▁picking",
+ -11.575072288513184
+ ],
+ [
+ "▁restoration",
+ -11.57513427734375
+ ],
+ [
+ "▁interviews",
+ -11.575201988220215
+ ],
+ [
+ "pressed",
+ -11.575210571289062
+ ],
+ [
+ "nnerhalb",
+ -11.575674057006836
+ ],
+ [
+ "▁connecting",
+ -11.575834274291992
+ ],
+ [
+ "jou",
+ -11.575943946838379
+ ],
+ [
+ "▁react",
+ -11.576189041137695
+ ],
+ [
+ "▁Merci",
+ -11.576223373413086
+ ],
+ [
+ "▁Phone",
+ -11.576356887817383
+ ],
+ [
+ "▁1)",
+ -11.57652473449707
+ ],
+ [
+ "▁victims",
+ -11.576618194580078
+ ],
+ [
+ "▁Spo",
+ -11.576685905456543
+ ],
+ [
+ "atului",
+ -11.576735496520996
+ ],
+ [
+ "▁Harry",
+ -11.576837539672852
+ ],
+ [
+ "▁Sala",
+ -11.576875686645508
+ ],
+ [
+ "Pol",
+ -11.577075958251953
+ ],
+ [
+ "▁Clo",
+ -11.577167510986328
+ ],
+ [
+ "▁Erfolg",
+ -11.577211380004883
+ ],
+ [
+ "autour",
+ -11.577308654785156
+ ],
+ [
+ "▁Template",
+ -11.577314376831055
+ ],
+ [
+ "▁invention",
+ -11.57754898071289
+ ],
+ [
+ "▁schwer",
+ -11.57761287689209
+ ],
+ [
+ "vac",
+ -11.577625274658203
+ ],
+ [
+ "▁Trail",
+ -11.577627182006836
+ ],
+ [
+ "▁Vietnam",
+ -11.577638626098633
+ ],
+ [
+ "▁Size",
+ -11.577689170837402
+ ],
+ [
+ "▁Bern",
+ -11.577783584594727
+ ],
+ [
+ "▁emp",
+ -11.577845573425293
+ ],
+ [
+ "▁shake",
+ -11.57787799835205
+ ],
+ [
+ "▁Ave",
+ -11.57794189453125
+ ],
+ [
+ "▁productive",
+ -11.578009605407715
+ ],
+ [
+ "▁apple",
+ -11.578015327453613
+ ],
+ [
+ "▁portal",
+ -11.578052520751953
+ ],
+ [
+ "▁ceramic",
+ -11.578082084655762
+ ],
+ [
+ "▁pad",
+ -11.578110694885254
+ ],
+ [
+ "▁Syn",
+ -11.578316688537598
+ ],
+ [
+ "Ab",
+ -11.57845401763916
+ ],
+ [
+ "▁syn",
+ -11.578761100769043
+ ],
+ [
+ "find",
+ -11.578888893127441
+ ],
+ [
+ "▁settle",
+ -11.578909873962402
+ ],
+ [
+ "▁général",
+ -11.578965187072754
+ ],
+ [
+ "▁okay",
+ -11.579032897949219
+ ],
+ [
+ "▁receipt",
+ -11.57906436920166
+ ],
+ [
+ "orii",
+ -11.579117774963379
+ ],
+ [
+ "▁Mission",
+ -11.579122543334961
+ ],
+ [
+ "entrée",
+ -11.579304695129395
+ ],
+ [
+ "▁besteht",
+ -11.579394340515137
+ ],
+ [
+ "▁wisdom",
+ -11.57950210571289
+ ],
+ [
+ "▁heraus",
+ -11.579645156860352
+ ],
+ [
+ "▁balanced",
+ -11.579753875732422
+ ],
+ [
+ "▁habits",
+ -11.579773902893066
+ ],
+ [
+ "tang",
+ -11.579888343811035
+ ],
+ [
+ "ură",
+ -11.580151557922363
+ ],
+ [
+ "▁winners",
+ -11.580182075500488
+ ],
+ [
+ "ç",
+ -11.580215454101562
+ ],
+ [
+ "▁folosi",
+ -11.580242156982422
+ ],
+ [
+ "aliment",
+ -11.5802583694458
+ ],
+ [
+ "▁fiction",
+ -11.580373764038086
+ ],
+ [
+ "▁Spe",
+ -11.580534934997559
+ ],
+ [
+ "▁elsewhere",
+ -11.580663681030273
+ ],
+ [
+ "▁dependent",
+ -11.580808639526367
+ ],
+ [
+ "▁Anne",
+ -11.581167221069336
+ ],
+ [
+ "▁excellence",
+ -11.581695556640625
+ ],
+ [
+ "▁Feel",
+ -11.581753730773926
+ ],
+ [
+ "lieb",
+ -11.581811904907227
+ ],
+ [
+ "▁sectors",
+ -11.581865310668945
+ ],
+ [
+ "▁expir",
+ -11.581886291503906
+ ],
+ [
+ "▁surfaces",
+ -11.58191204071045
+ ],
+ [
+ "▁minim",
+ -11.581937789916992
+ ],
+ [
+ "▁tumor",
+ -11.58204460144043
+ ],
+ [
+ "▁paragraph",
+ -11.582289695739746
+ ],
+ [
+ "▁disk",
+ -11.58232307434082
+ ],
+ [
+ "▁tonight",
+ -11.582379341125488
+ ],
+ [
+ "▁precious",
+ -11.582794189453125
+ ],
+ [
+ "▁console",
+ -11.58288288116455
+ ],
+ [
+ "Th",
+ -11.582939147949219
+ ],
+ [
+ "neu",
+ -11.583020210266113
+ ],
+ [
+ "effective",
+ -11.5839262008667
+ ],
+ [
+ "▁Republican",
+ -11.583944320678711
+ ],
+ [
+ "format",
+ -11.584297180175781
+ ],
+ [
+ "▁preserve",
+ -11.58436107635498
+ ],
+ [
+ "▁wiring",
+ -11.584599494934082
+ ],
+ [
+ "▁exercises",
+ -11.584757804870605
+ ],
+ [
+ "▁pregnancy",
+ -11.584774017333984
+ ],
+ [
+ "tries",
+ -11.58481502532959
+ ],
+ [
+ "▁jeunes",
+ -11.584883689880371
+ ],
+ [
+ "▁publishing",
+ -11.584932327270508
+ ],
+ [
+ "▁nehmen",
+ -11.584935188293457
+ ],
+ [
+ "▁capability",
+ -11.5849609375
+ ],
+ [
+ "▁prompt",
+ -11.584965705871582
+ ],
+ [
+ "▁Further",
+ -11.58497428894043
+ ],
+ [
+ "▁semaine",
+ -11.585173606872559
+ ],
+ [
+ "abo",
+ -11.585216522216797
+ ],
+ [
+ "▁evolution",
+ -11.585319519042969
+ ],
+ [
+ "▁Sud",
+ -11.585403442382812
+ ],
+ [
+ "▁frais",
+ -11.585525512695312
+ ],
+ [
+ "LT",
+ -11.585619926452637
+ ],
+ [
+ "▁stack",
+ -11.58581829071045
+ ],
+ [
+ "▁Inside",
+ -11.585854530334473
+ ],
+ [
+ "▁programmes",
+ -11.585997581481934
+ ],
+ [
+ "▁passes",
+ -11.586196899414062
+ ],
+ [
+ "mü",
+ -11.586474418640137
+ ],
+ [
+ "▁progressive",
+ -11.586518287658691
+ ],
+ [
+ "▁calculator",
+ -11.58658218383789
+ ],
+ [
+ "▁Core",
+ -11.586655616760254
+ ],
+ [
+ "BT",
+ -11.586956977844238
+ ],
+ [
+ "core",
+ -11.586996078491211
+ ],
+ [
+ "▁Moon",
+ -11.587004661560059
+ ],
+ [
+ "▁tender",
+ -11.587040901184082
+ ],
+ [
+ "durch",
+ -11.58721923828125
+ ],
+ [
+ "▁commune",
+ -11.587453842163086
+ ],
+ [
+ "▁Prince",
+ -11.587594032287598
+ ],
+ [
+ "▁demonstrated",
+ -11.587693214416504
+ ],
+ [
+ "▁conversations",
+ -11.587890625
+ ],
+ [
+ "▁fri",
+ -11.587984085083008
+ ],
+ [
+ "igh",
+ -11.587992668151855
+ ],
+ [
+ "being",
+ -11.588334083557129
+ ],
+ [
+ "pause",
+ -11.58853530883789
+ ],
+ [
+ "▁Bear",
+ -11.58871841430664
+ ],
+ [
+ "ayant",
+ -11.588875770568848
+ ],
+ [
+ "▁Industry",
+ -11.588967323303223
+ ],
+ [
+ "▁sponsor",
+ -11.589012145996094
+ ],
+ [
+ "▁numele",
+ -11.589098930358887
+ ],
+ [
+ "▁VA",
+ -11.589167594909668
+ ],
+ [
+ "▁Sommer",
+ -11.589366912841797
+ ],
+ [
+ "TB",
+ -11.589380264282227
+ ],
+ [
+ "▁optional",
+ -11.589505195617676
+ ],
+ [
+ "▁Landes",
+ -11.589812278747559
+ ],
+ [
+ "coli",
+ -11.589963912963867
+ ],
+ [
+ "empt",
+ -11.59018325805664
+ ],
+ [
+ "▁Iron",
+ -11.590620040893555
+ ],
+ [
+ "▁1992",
+ -11.59090518951416
+ ],
+ [
+ "▁attempts",
+ -11.59090518951416
+ ],
+ [
+ "halb",
+ -11.590960502624512
+ ],
+ [
+ "▁photographer",
+ -11.59097671508789
+ ],
+ [
+ "▁witness",
+ -11.59097957611084
+ ],
+ [
+ "bru",
+ -11.591073989868164
+ ],
+ [
+ "▁Ras",
+ -11.59107780456543
+ ],
+ [
+ "▁burden",
+ -11.591142654418945
+ ],
+ [
+ "▁kaufen",
+ -11.591256141662598
+ ],
+ [
+ "▁vu",
+ -11.591362953186035
+ ],
+ [
+ "▁Wedding",
+ -11.591601371765137
+ ],
+ [
+ "▁Kla",
+ -11.591604232788086
+ ],
+ [
+ "occasion",
+ -11.591915130615234
+ ],
+ [
+ "▁keys",
+ -11.592131614685059
+ ],
+ [
+ "▁oferi",
+ -11.592279434204102
+ ],
+ [
+ "▁puzzle",
+ -11.592302322387695
+ ],
+ [
+ "eaux",
+ -11.59254264831543
+ ],
+ [
+ "▁Eco",
+ -11.592805862426758
+ ],
+ [
+ "▁52",
+ -11.592817306518555
+ ],
+ [
+ "▁Elizabeth",
+ -11.59284496307373
+ ],
+ [
+ "▁dispose",
+ -11.593144416809082
+ ],
+ [
+ "▁cluster",
+ -11.59326171875
+ ],
+ [
+ "iki",
+ -11.593283653259277
+ ],
+ [
+ "▁Guys",
+ -11.593595504760742
+ ],
+ [
+ "▁Economic",
+ -11.593632698059082
+ ],
+ [
+ "▁apar",
+ -11.593677520751953
+ ],
+ [
+ "▁ziua",
+ -11.593688011169434
+ ],
+ [
+ "▁integral",
+ -11.593740463256836
+ ],
+ [
+ "▁tac",
+ -11.59376335144043
+ ],
+ [
+ "▁restrictions",
+ -11.593778610229492
+ ],
+ [
+ "▁nerve",
+ -11.593794822692871
+ ],
+ [
+ "▁Stop",
+ -11.59386157989502
+ ],
+ [
+ "burger",
+ -11.593897819519043
+ ],
+ [
+ "explo",
+ -11.593944549560547
+ ],
+ [
+ "lö",
+ -11.593958854675293
+ ],
+ [
+ "NP",
+ -11.594077110290527
+ ],
+ [
+ "▁Brook",
+ -11.59418773651123
+ ],
+ [
+ "▁Close",
+ -11.594278335571289
+ ],
+ [
+ "▁representing",
+ -11.59446907043457
+ ],
+ [
+ "▁certaine",
+ -11.594767570495605
+ ],
+ [
+ "▁discovery",
+ -11.594836235046387
+ ],
+ [
+ "▁rece",
+ -11.594964981079102
+ ],
+ [
+ "FF",
+ -11.594970703125
+ ],
+ [
+ "▁salary",
+ -11.595069885253906
+ ],
+ [
+ "▁Wolf",
+ -11.595137596130371
+ ],
+ [
+ "▁deserve",
+ -11.595166206359863
+ ],
+ [
+ "ţele",
+ -11.595417976379395
+ ],
+ [
+ "gathered",
+ -11.595934867858887
+ ],
+ [
+ "▁comply",
+ -11.59599494934082
+ ],
+ [
+ "lagen",
+ -11.596034049987793
+ ],
+ [
+ "ătoare",
+ -11.596192359924316
+ ],
+ [
+ "▁relate",
+ -11.596410751342773
+ ],
+ [
+ "▁Roger",
+ -11.59656810760498
+ ],
+ [
+ "▁blame",
+ -11.596575736999512
+ ],
+ [
+ "▁Jen",
+ -11.596914291381836
+ ],
+ [
+ "▁army",
+ -11.596936225891113
+ ],
+ [
+ "▁$10",
+ -11.597129821777344
+ ],
+ [
+ "▁Cabinet",
+ -11.597185134887695
+ ],
+ [
+ "Gu",
+ -11.597367286682129
+ ],
+ [
+ "▁wildlife",
+ -11.597452163696289
+ ],
+ [
+ "▁Memorial",
+ -11.597643852233887
+ ],
+ [
+ "▁Holiday",
+ -11.597742080688477
+ ],
+ [
+ "▁curat",
+ -11.598291397094727
+ ],
+ [
+ "iilor",
+ -11.598299026489258
+ ],
+ [
+ "▁fleet",
+ -11.598408699035645
+ ],
+ [
+ "▁reviewed",
+ -11.59843635559082
+ ],
+ [
+ "cet",
+ -11.598450660705566
+ ],
+ [
+ "▁virtually",
+ -11.598487854003906
+ ],
+ [
+ "▁Crusher",
+ -11.59852409362793
+ ],
+ [
+ "▁slide",
+ -11.59858226776123
+ ],
+ [
+ "▁générale",
+ -11.598604202270508
+ ],
+ [
+ "▁sensation",
+ -11.598630905151367
+ ],
+ [
+ "▁garlic",
+ -11.598638534545898
+ ],
+ [
+ "5)",
+ -11.598657608032227
+ ],
+ [
+ "▁batteries",
+ -11.598756790161133
+ ],
+ [
+ "SH",
+ -11.59876823425293
+ ],
+ [
+ "▁seller",
+ -11.59882926940918
+ ],
+ [
+ "design",
+ -11.598871231079102
+ ],
+ [
+ "5.",
+ -11.598944664001465
+ ],
+ [
+ "▁Overall",
+ -11.598969459533691
+ ],
+ [
+ "▁investigate",
+ -11.599058151245117
+ ],
+ [
+ "max",
+ -11.599064826965332
+ ],
+ [
+ "▁attach",
+ -11.599166870117188
+ ],
+ [
+ "▁Future",
+ -11.599209785461426
+ ],
+ [
+ "OUR",
+ -11.599284172058105
+ ],
+ [
+ "▁LE",
+ -11.59968090057373
+ ],
+ [
+ "▁bite",
+ -11.599811553955078
+ ],
+ [
+ "tige",
+ -11.599874496459961
+ ],
+ [
+ "▁twist",
+ -11.59987735748291
+ ],
+ [
+ "hole",
+ -11.600180625915527
+ ],
+ [
+ "▁Tony",
+ -11.600510597229004
+ ],
+ [
+ "LU",
+ -11.600598335266113
+ ],
+ [
+ "▁Organization",
+ -11.600617408752441
+ ],
+ [
+ "▁invit",
+ -11.600632667541504
+ ],
+ [
+ "▁Ant",
+ -11.600739479064941
+ ],
+ [
+ "NR",
+ -11.600788116455078
+ ],
+ [
+ "sorgt",
+ -11.600854873657227
+ ],
+ [
+ "▁Lan",
+ -11.600860595703125
+ ],
+ [
+ "▁Manchester",
+ -11.60091495513916
+ ],
+ [
+ "schrift",
+ -11.601066589355469
+ ],
+ [
+ "▁kg",
+ -11.601150512695312
+ ],
+ [
+ "▁aroma",
+ -11.60132884979248
+ ],
+ [
+ "▁Source",
+ -11.601388931274414
+ ],
+ [
+ "▁permite",
+ -11.601445198059082
+ ],
+ [
+ "▁Consider",
+ -11.601457595825195
+ ],
+ [
+ "▁Artist",
+ -11.601627349853516
+ ],
+ [
+ "▁transmit",
+ -11.601783752441406
+ ],
+ [
+ "oasa",
+ -11.601834297180176
+ ],
+ [
+ "▁Zen",
+ -11.60198974609375
+ ],
+ [
+ "ANT",
+ -11.602235794067383
+ ],
+ [
+ "▁consulting",
+ -11.602404594421387
+ ],
+ [
+ "▁commence",
+ -11.6025390625
+ ],
+ [
+ "▁quilt",
+ -11.60261058807373
+ ],
+ [
+ "owned",
+ -11.602642059326172
+ ],
+ [
+ "▁bro",
+ -11.602689743041992
+ ],
+ [
+ "▁integrate",
+ -11.602715492248535
+ ],
+ [
+ "▁Ontario",
+ -11.602775573730469
+ ],
+ [
+ "TF",
+ -11.602832794189453
+ ],
+ [
+ "▁Study",
+ -11.602887153625488
+ ],
+ [
+ "▁ensuite",
+ -11.603155136108398
+ ],
+ [
+ "itatii",
+ -11.603180885314941
+ ],
+ [
+ "Mon",
+ -11.603235244750977
+ ],
+ [
+ "-11",
+ -11.603299140930176
+ ],
+ [
+ "what",
+ -11.603384017944336
+ ],
+ [
+ "▁Things",
+ -11.60361385345459
+ ],
+ [
+ "▁Eye",
+ -11.603819847106934
+ ],
+ [
+ "▁présente",
+ -11.603828430175781
+ ],
+ [
+ "tention",
+ -11.603915214538574
+ ],
+ [
+ "|",
+ -11.603957176208496
+ ],
+ [
+ "stall",
+ -11.603963851928711
+ ],
+ [
+ "▁beef",
+ -11.603992462158203
+ ],
+ [
+ "figur",
+ -11.604005813598633
+ ],
+ [
+ "▁cancel",
+ -11.604146003723145
+ ],
+ [
+ "▁domeniul",
+ -11.604252815246582
+ ],
+ [
+ "▁360",
+ -11.604290008544922
+ ],
+ [
+ "▁sleeping",
+ -11.6045560836792
+ ],
+ [
+ "▁traitement",
+ -11.604580879211426
+ ],
+ [
+ "ühl",
+ -11.604769706726074
+ ],
+ [
+ "▁Environmental",
+ -11.604835510253906
+ ],
+ [
+ "cier",
+ -11.604894638061523
+ ],
+ [
+ "▁NC",
+ -11.604907035827637
+ ],
+ [
+ "pub",
+ -11.604925155639648
+ ],
+ [
+ "▁addiction",
+ -11.605071067810059
+ ],
+ [
+ "▁nest",
+ -11.605128288269043
+ ],
+ [
+ "▁ON",
+ -11.605395317077637
+ ],
+ [
+ "▁discrimin",
+ -11.605396270751953
+ ],
+ [
+ "▁proved",
+ -11.605517387390137
+ ],
+ [
+ "▁occasions",
+ -11.605864524841309
+ ],
+ [
+ "OH",
+ -11.606184959411621
+ ],
+ [
+ "▁lawyers",
+ -11.606203079223633
+ ],
+ [
+ "own",
+ -11.606290817260742
+ ],
+ [
+ "▁Meeting",
+ -11.606596946716309
+ ],
+ [
+ "▁Industrial",
+ -11.606704711914062
+ ],
+ [
+ "owed",
+ -11.606736183166504
+ ],
+ [
+ "▁Cel",
+ -11.606793403625488
+ ],
+ [
+ "legt",
+ -11.60706615447998
+ ],
+ [
+ "ily",
+ -11.607085227966309
+ ],
+ [
+ "▁wins",
+ -11.607155799865723
+ ],
+ [
+ "▁strap",
+ -11.607367515563965
+ ],
+ [
+ "digit",
+ -11.607441902160645
+ ],
+ [
+ "▁hinaus",
+ -11.607504844665527
+ ],
+ [
+ "mple",
+ -11.607712745666504
+ ],
+ [
+ "▁(5",
+ -11.607797622680664
+ ],
+ [
+ "▁pdf",
+ -11.607894897460938
+ ],
+ [
+ "▁eco",
+ -11.607915878295898
+ ],
+ [
+ "▁junior",
+ -11.608172416687012
+ ],
+ [
+ "DB",
+ -11.608556747436523
+ ],
+ [
+ "gelegt",
+ -11.608636856079102
+ ],
+ [
+ "ION",
+ -11.608678817749023
+ ],
+ [
+ "▁competitors",
+ -11.60880184173584
+ ],
+ [
+ "▁Arab",
+ -11.60898208618164
+ ],
+ [
+ "▁Secret",
+ -11.609148979187012
+ ],
+ [
+ "▁Kunst",
+ -11.609283447265625
+ ],
+ [
+ "▁worried",
+ -11.609297752380371
+ ],
+ [
+ "meiner",
+ -11.609378814697266
+ ],
+ [
+ "▁Magic",
+ -11.609450340270996
+ ],
+ [
+ "▁groß",
+ -11.609537124633789
+ ],
+ [
+ "▁travaux",
+ -11.609748840332031
+ ],
+ [
+ "▁sollen",
+ -11.609772682189941
+ ],
+ [
+ "▁Sciences",
+ -11.609850883483887
+ ],
+ [
+ "▁athletes",
+ -11.610055923461914
+ ],
+ [
+ "▁discounts",
+ -11.610079765319824
+ ],
+ [
+ "kit",
+ -11.610211372375488
+ ],
+ [
+ "lind",
+ -11.610305786132812
+ ],
+ [
+ "▁enjoyable",
+ -11.610421180725098
+ ],
+ [
+ "ground",
+ -11.610489845275879
+ ],
+ [
+ "▁Tat",
+ -11.610529899597168
+ ],
+ [
+ "▁passengers",
+ -11.610576629638672
+ ],
+ [
+ "▁Dami",
+ -11.610677719116211
+ ],
+ [
+ "▁Major",
+ -11.61070728302002
+ ],
+ [
+ "watch",
+ -11.610796928405762
+ ],
+ [
+ "working",
+ -11.610908508300781
+ ],
+ [
+ "arrêt",
+ -11.610923767089844
+ ],
+ [
+ "▁subtle",
+ -11.611069679260254
+ ],
+ [
+ "▁epi",
+ -11.611197471618652
+ ],
+ [
+ "▁Jahres",
+ -11.61128044128418
+ ],
+ [
+ "▁cooling",
+ -11.61141586303711
+ ],
+ [
+ "▁makeup",
+ -11.611427307128906
+ ],
+ [
+ "jet",
+ -11.611495018005371
+ ],
+ [
+ "▁Given",
+ -11.611519813537598
+ ],
+ [
+ "plex",
+ -11.61158275604248
+ ],
+ [
+ "▁exploit",
+ -11.611590385437012
+ ],
+ [
+ "rine",
+ -11.611604690551758
+ ],
+ [
+ "▁delivers",
+ -11.612122535705566
+ ],
+ [
+ "▁summary",
+ -11.612236022949219
+ ],
+ [
+ "▁beaches",
+ -11.612459182739258
+ ],
+ [
+ "lift",
+ -11.612550735473633
+ ],
+ [
+ "▁Suite",
+ -11.612554550170898
+ ],
+ [
+ "▁Assistant",
+ -11.612688064575195
+ ],
+ [
+ "▁taxi",
+ -11.61273193359375
+ ],
+ [
+ "▁peaceful",
+ -11.612805366516113
+ ],
+ [
+ "▁Mode",
+ -11.612980842590332
+ ],
+ [
+ "▁Fun",
+ -11.613059043884277
+ ],
+ [
+ "▁diameter",
+ -11.613142967224121
+ ],
+ [
+ "▁phrase",
+ -11.613150596618652
+ ],
+ [
+ "ACT",
+ -11.613265037536621
+ ],
+ [
+ "▁différentes",
+ -11.613322257995605
+ ],
+ [
+ "▁14.",
+ -11.613417625427246
+ ],
+ [
+ "▁CE",
+ -11.61352825164795
+ ],
+ [
+ "▁2)",
+ -11.613739013671875
+ ],
+ [
+ "▁Nat",
+ -11.613785743713379
+ ],
+ [
+ "▁delete",
+ -11.61388111114502
+ ],
+ [
+ "other",
+ -11.613930702209473
+ ],
+ [
+ "hang",
+ -11.613985061645508
+ ],
+ [
+ "▁sujet",
+ -11.614117622375488
+ ],
+ [
+ "▁precise",
+ -11.614212989807129
+ ],
+ [
+ "▁Total",
+ -11.614290237426758
+ ],
+ [
+ "▁chambre",
+ -11.614483833312988
+ ],
+ [
+ "sati",
+ -11.614666938781738
+ ],
+ [
+ "▁Metal",
+ -11.614995956420898
+ ],
+ [
+ "rust",
+ -11.615038871765137
+ ],
+ [
+ "▁Brazil",
+ -11.615508079528809
+ ],
+ [
+ "▁hybrid",
+ -11.615636825561523
+ ],
+ [
+ "ops",
+ -11.615691184997559
+ ],
+ [
+ "▁electro",
+ -11.615789413452148
+ ],
+ [
+ "utz",
+ -11.61608600616455
+ ],
+ [
+ "▁quoi",
+ -11.616246223449707
+ ],
+ [
+ "▁adoption",
+ -11.616331100463867
+ ],
+ [
+ "3.5",
+ -11.616518020629883
+ ],
+ [
+ "50,000",
+ -11.616599082946777
+ ],
+ [
+ "veti",
+ -11.616630554199219
+ ],
+ [
+ "hir",
+ -11.616957664489746
+ ],
+ [
+ "▁adequate",
+ -11.617067337036133
+ ],
+ [
+ "ologist",
+ -11.617109298706055
+ ],
+ [
+ "torii",
+ -11.617295265197754
+ ],
+ [
+ "wasser",
+ -11.617355346679688
+ ],
+ [
+ "▁Authority",
+ -11.617362976074219
+ ],
+ [
+ "▁donation",
+ -11.617364883422852
+ ],
+ [
+ "700",
+ -11.617375373840332
+ ],
+ [
+ "▁somehow",
+ -11.617375373840332
+ ],
+ [
+ "▁kostenlos",
+ -11.617425918579102
+ ],
+ [
+ "▁generations",
+ -11.617537498474121
+ ],
+ [
+ "▁Turkey",
+ -11.617711067199707
+ ],
+ [
+ "rata",
+ -11.617819786071777
+ ],
+ [
+ "▁animation",
+ -11.618206024169922
+ ],
+ [
+ "▁CH",
+ -11.618281364440918
+ ],
+ [
+ "ending",
+ -11.618317604064941
+ ],
+ [
+ "welt",
+ -11.618376731872559
+ ],
+ [
+ "bac",
+ -11.618380546569824
+ ],
+ [
+ "MG",
+ -11.618460655212402
+ ],
+ [
+ "▁parks",
+ -11.618468284606934
+ ],
+ [
+ "▁placing",
+ -11.618870735168457
+ ],
+ [
+ "sort",
+ -11.61915111541748
+ ],
+ [
+ "▁Bitcoin",
+ -11.619163513183594
+ ],
+ [
+ "▁disorder",
+ -11.619282722473145
+ ],
+ [
+ "MAN",
+ -11.619302749633789
+ ],
+ [
+ "aught",
+ -11.619412422180176
+ ],
+ [
+ "▁guides",
+ -11.61956787109375
+ ],
+ [
+ "▁circul",
+ -11.619651794433594
+ ],
+ [
+ "▁Steven",
+ -11.619954109191895
+ ],
+ [
+ "rrière",
+ -11.619976997375488
+ ],
+ [
+ "▁Arch",
+ -11.61999225616455
+ ],
+ [
+ "▁plates",
+ -11.620091438293457
+ ],
+ [
+ "MR",
+ -11.620118141174316
+ ],
+ [
+ "▁cow",
+ -11.620142936706543
+ ],
+ [
+ "▁integrity",
+ -11.620210647583008
+ ],
+ [
+ "▁(18",
+ -11.620217323303223
+ ],
+ [
+ "▁totul",
+ -11.62024211883545
+ ],
+ [
+ "jack",
+ -11.620373725891113
+ ],
+ [
+ "▁privire",
+ -11.620588302612305
+ ],
+ [
+ "▁terme",
+ -11.620752334594727
+ ],
+ [
+ "▁execution",
+ -11.620781898498535
+ ],
+ [
+ "▁organism",
+ -11.620838165283203
+ ],
+ [
+ "▁führen",
+ -11.620853424072266
+ ],
+ [
+ "▁patron",
+ -11.620940208435059
+ ],
+ [
+ "▁appreciated",
+ -11.62096881866455
+ ],
+ [
+ "liant",
+ -11.62100601196289
+ ],
+ [
+ "▁Solar",
+ -11.621055603027344
+ ],
+ [
+ "▁vinyl",
+ -11.621134757995605
+ ],
+ [
+ "▁treasure",
+ -11.621137619018555
+ ],
+ [
+ "▁retro",
+ -11.621167182922363
+ ],
+ [
+ "▁bout",
+ -11.621174812316895
+ ],
+ [
+ "lab",
+ -11.621183395385742
+ ],
+ [
+ "▁dimension",
+ -11.621394157409668
+ ],
+ [
+ "called",
+ -11.62146282196045
+ ],
+ [
+ "▁intern",
+ -11.621479034423828
+ ],
+ [
+ "issement",
+ -11.62173843383789
+ ],
+ [
+ "▁Erst",
+ -11.621837615966797
+ ],
+ [
+ "▁stellen",
+ -11.621920585632324
+ ],
+ [
+ "▁familia",
+ -11.622069358825684
+ ],
+ [
+ "▁notion",
+ -11.622176170349121
+ ],
+ [
+ "▁Could",
+ -11.622322082519531
+ ],
+ [
+ "Getting",
+ -11.622323036193848
+ ],
+ [
+ "▁drives",
+ -11.622397422790527
+ ],
+ [
+ "▁Israeli",
+ -11.622520446777344
+ ],
+ [
+ "▁nations",
+ -11.622546195983887
+ ],
+ [
+ "▁duties",
+ -11.622700691223145
+ ],
+ [
+ "▁personalized",
+ -11.622788429260254
+ ],
+ [
+ "▁weren",
+ -11.62282657623291
+ ],
+ [
+ "▁chemicals",
+ -11.622847557067871
+ ],
+ [
+ "▁killing",
+ -11.622913360595703
+ ],
+ [
+ "▁masa",
+ -11.622994422912598
+ ],
+ [
+ "▁parce",
+ -11.623026847839355
+ ],
+ [
+ "▁lady",
+ -11.623178482055664
+ ],
+ [
+ "ides",
+ -11.623221397399902
+ ],
+ [
+ "▁execut",
+ -11.62340259552002
+ ],
+ [
+ "▁floral",
+ -11.62341594696045
+ ],
+ [
+ "▁Child",
+ -11.623428344726562
+ ],
+ [
+ "▁medal",
+ -11.623503684997559
+ ],
+ [
+ "▁casa",
+ -11.623603820800781
+ ],
+ [
+ "▁enabled",
+ -11.623650550842285
+ ],
+ [
+ "12.",
+ -11.624239921569824
+ ],
+ [
+ "nger",
+ -11.624266624450684
+ ],
+ [
+ "▁vent",
+ -11.624297142028809
+ ],
+ [
+ "▁urmă",
+ -11.624727249145508
+ ],
+ [
+ "▁Herz",
+ -11.624835968017578
+ ],
+ [
+ "▁Jay",
+ -11.624916076660156
+ ],
+ [
+ ".....",
+ -11.624942779541016
+ ],
+ [
+ "▁Kris",
+ -11.62499713897705
+ ],
+ [
+ "kenn",
+ -11.625001907348633
+ ],
+ [
+ "ress",
+ -11.625027656555176
+ ],
+ [
+ "weight",
+ -11.62519359588623
+ ],
+ [
+ "▁indicates",
+ -11.625198364257812
+ ],
+ [
+ "▁mentor",
+ -11.625328063964844
+ ],
+ [
+ "using",
+ -11.625386238098145
+ ],
+ [
+ "▁femmes",
+ -11.625460624694824
+ ],
+ [
+ "▁Jung",
+ -11.625528335571289
+ ],
+ [
+ "▁Send",
+ -11.625574111938477
+ ],
+ [
+ "▁seasons",
+ -11.625906944274902
+ ],
+ [
+ "▁aesthetic",
+ -11.625964164733887
+ ],
+ [
+ "▁Block",
+ -11.626086235046387
+ ],
+ [
+ "▁babies",
+ -11.626150131225586
+ ],
+ [
+ "zig",
+ -11.626242637634277
+ ],
+ [
+ "edge",
+ -11.626428604125977
+ ],
+ [
+ "▁alike",
+ -11.626458168029785
+ ],
+ [
+ "▁immune",
+ -11.626609802246094
+ ],
+ [
+ "▁magical",
+ -11.626710891723633
+ ],
+ [
+ "▁Snow",
+ -11.626748085021973
+ ],
+ [
+ "▁spacious",
+ -11.627058982849121
+ ],
+ [
+ "▁Melbourne",
+ -11.62706184387207
+ ],
+ [
+ "order",
+ -11.627081871032715
+ ],
+ [
+ "▁timing",
+ -11.627176284790039
+ ],
+ [
+ "▁inainte",
+ -11.627220153808594
+ ],
+ [
+ "▁width",
+ -11.627327919006348
+ ],
+ [
+ "bild",
+ -11.627386093139648
+ ],
+ [
+ "Tra",
+ -11.627429008483887
+ ],
+ [
+ "▁appliances",
+ -11.627449989318848
+ ],
+ [
+ "▁dirt",
+ -11.627498626708984
+ ],
+ [
+ "▁Rent",
+ -11.627689361572266
+ ],
+ [
+ "responsibilities",
+ -11.627747535705566
+ ],
+ [
+ "▁blogs",
+ -11.62778377532959
+ ],
+ [
+ "nächsten",
+ -11.627799034118652
+ ],
+ [
+ "▁argue",
+ -11.627928733825684
+ ],
+ [
+ "▁Resume",
+ -11.627985954284668
+ ],
+ [
+ "▁Michel",
+ -11.628044128417969
+ ],
+ [
+ "▁terrible",
+ -11.628092765808105
+ ],
+ [
+ "graph",
+ -11.628151893615723
+ ],
+ [
+ "bird",
+ -11.628202438354492
+ ],
+ [
+ "▁Simple",
+ -11.628457069396973
+ ],
+ [
+ "nning",
+ -11.628658294677734
+ ],
+ [
+ "▁coconut",
+ -11.628683090209961
+ ],
+ [
+ "▁comprise",
+ -11.628787994384766
+ ],
+ [
+ "heure",
+ -11.628918647766113
+ ],
+ [
+ "▁nichts",
+ -11.628921508789062
+ ],
+ [
+ "▁manufacture",
+ -11.628966331481934
+ ],
+ [
+ "▁Sar",
+ -11.629011154174805
+ ],
+ [
+ "green",
+ -11.629014015197754
+ ],
+ [
+ "lining",
+ -11.62910270690918
+ ],
+ [
+ "▁tremendous",
+ -11.629128456115723
+ ],
+ [
+ "▁Wine",
+ -11.629164695739746
+ ],
+ [
+ "gir",
+ -11.629290580749512
+ ],
+ [
+ "▁Nothing",
+ -11.629562377929688
+ ],
+ [
+ "▁Miller",
+ -11.62957763671875
+ ],
+ [
+ "▁Schwe",
+ -11.629712104797363
+ ],
+ [
+ "zone",
+ -11.629942893981934
+ ],
+ [
+ "▁cunoscut",
+ -11.629964828491211
+ ],
+ [
+ "rupt",
+ -11.630166053771973
+ ],
+ [
+ "kle",
+ -11.630187034606934
+ ],
+ [
+ "▁Bucuresti",
+ -11.630510330200195
+ ],
+ [
+ "▁Abend",
+ -11.630574226379395
+ ],
+ [
+ "▁aura",
+ -11.630583763122559
+ ],
+ [
+ "▁Dance",
+ -11.63073444366455
+ ],
+ [
+ "▁Wilson",
+ -11.63086986541748
+ ],
+ [
+ "icide",
+ -11.630901336669922
+ ],
+ [
+ "bai",
+ -11.630910873413086
+ ],
+ [
+ "oriented",
+ -11.63103199005127
+ ],
+ [
+ "▁celebrated",
+ -11.631421089172363
+ ],
+ [
+ "schlag",
+ -11.631531715393066
+ ],
+ [
+ "▁10-",
+ -11.631600379943848
+ ],
+ [
+ "Unsere",
+ -11.63167667388916
+ ],
+ [
+ "énergie",
+ -11.632009506225586
+ ],
+ [
+ "▁qualify",
+ -11.63205623626709
+ ],
+ [
+ "▁contenu",
+ -11.632177352905273
+ ],
+ [
+ "▁Lauf",
+ -11.63220500946045
+ ],
+ [
+ "▁einzelne",
+ -11.632360458374023
+ ],
+ [
+ "▁Youth",
+ -11.632415771484375
+ ],
+ [
+ "explains",
+ -11.632601737976074
+ ],
+ [
+ "grat",
+ -11.632782936096191
+ ],
+ [
+ "▁72",
+ -11.632804870605469
+ ],
+ [
+ "labor",
+ -11.632885932922363
+ ],
+ [
+ "2018",
+ -11.632940292358398
+ ],
+ [
+ "▁Dank",
+ -11.633149147033691
+ ],
+ [
+ "▁Hey",
+ -11.633523941040039
+ ],
+ [
+ "▁refuse",
+ -11.633536338806152
+ ],
+ [
+ "▁graduated",
+ -11.633599281311035
+ ],
+ [
+ "▁României",
+ -11.633627891540527
+ ],
+ [
+ "punkt",
+ -11.633807182312012
+ ],
+ [
+ "▁regulation",
+ -11.633834838867188
+ ],
+ [
+ "Bru",
+ -11.633842468261719
+ ],
+ [
+ "▁Side",
+ -11.633891105651855
+ ],
+ [
+ "▁sol",
+ -11.633970260620117
+ ],
+ [
+ "▁extraordinary",
+ -11.634182929992676
+ ],
+ [
+ "▁ging",
+ -11.634247779846191
+ ],
+ [
+ "▁Creative",
+ -11.634299278259277
+ ],
+ [
+ "▁expanding",
+ -11.634349822998047
+ ],
+ [
+ "▁problème",
+ -11.63444995880127
+ ],
+ [
+ "▁Reserve",
+ -11.63459300994873
+ ],
+ [
+ "auteur",
+ -11.634642601013184
+ ],
+ [
+ "sphere",
+ -11.634657859802246
+ ],
+ [
+ "season",
+ -11.634716987609863
+ ],
+ [
+ "frei",
+ -11.634756088256836
+ ],
+ [
+ "▁8,",
+ -11.634765625
+ ],
+ [
+ "▁filing",
+ -11.634810447692871
+ ],
+ [
+ "▁Complete",
+ -11.635017395019531
+ ],
+ [
+ "▁revolution",
+ -11.635035514831543
+ ],
+ [
+ "▁unele",
+ -11.63520622253418
+ ],
+ [
+ "/8",
+ -11.635272979736328
+ ],
+ [
+ "istes",
+ -11.635310173034668
+ ],
+ [
+ "backed",
+ -11.635400772094727
+ ],
+ [
+ "shirt",
+ -11.635554313659668
+ ],
+ [
+ "▁Details",
+ -11.635673522949219
+ ],
+ [
+ "rod",
+ -11.635695457458496
+ ],
+ [
+ "▁pod",
+ -11.63582992553711
+ ],
+ [
+ "▁operators",
+ -11.635921478271484
+ ],
+ [
+ "was",
+ -11.635930061340332
+ ],
+ [
+ "hou",
+ -11.63594913482666
+ ],
+ [
+ "▁Coach",
+ -11.636075019836426
+ ],
+ [
+ "irii",
+ -11.636138916015625
+ ],
+ [
+ "▁ordinary",
+ -11.636186599731445
+ ],
+ [
+ "Institut",
+ -11.63620662689209
+ ],
+ [
+ "▁Flash",
+ -11.63633918762207
+ ],
+ [
+ "0-",
+ -11.636537551879883
+ ],
+ [
+ "▁flavour",
+ -11.6367769241333
+ ],
+ [
+ "specific",
+ -11.636906623840332
+ ],
+ [
+ "▁landing",
+ -11.636930465698242
+ ],
+ [
+ "▁geo",
+ -11.636935234069824
+ ],
+ [
+ "▁legend",
+ -11.636983871459961
+ ],
+ [
+ "vari",
+ -11.63703441619873
+ ],
+ [
+ "rop",
+ -11.637084007263184
+ ],
+ [
+ "▁Excel",
+ -11.6370849609375
+ ],
+ [
+ "▁Flu",
+ -11.637203216552734
+ ],
+ [
+ "▁intent",
+ -11.637582778930664
+ ],
+ [
+ "▁Deep",
+ -11.637594223022461
+ ],
+ [
+ "▁Kor",
+ -11.63763427734375
+ ],
+ [
+ "▁Philadelphia",
+ -11.637914657592773
+ ],
+ [
+ "▁rând",
+ -11.63800048828125
+ ],
+ [
+ "▁USD",
+ -11.638033866882324
+ ],
+ [
+ "laden",
+ -11.63803482055664
+ ],
+ [
+ "▁Hin",
+ -11.638047218322754
+ ],
+ [
+ "hap",
+ -11.638197898864746
+ ],
+ [
+ "▁thorough",
+ -11.638227462768555
+ ],
+ [
+ "▁oferit",
+ -11.63826847076416
+ ],
+ [
+ "kind",
+ -11.63831615447998
+ ],
+ [
+ "▁Cancer",
+ -11.638428688049316
+ ],
+ [
+ "apo",
+ -11.638596534729004
+ ],
+ [
+ "▁valve",
+ -11.638650894165039
+ ],
+ [
+ "▁encouraging",
+ -11.63884449005127
+ ],
+ [
+ "▁sûr",
+ -11.638904571533203
+ ],
+ [
+ "shing",
+ -11.638981819152832
+ ],
+ [
+ "▁49",
+ -11.639132499694824
+ ],
+ [
+ "gov",
+ -11.639142990112305
+ ],
+ [
+ "▁Five",
+ -11.63933277130127
+ ],
+ [
+ "▁stroke",
+ -11.639344215393066
+ ],
+ [
+ "▁apă",
+ -11.639398574829102
+ ],
+ [
+ "▁gambling",
+ -11.639543533325195
+ ],
+ [
+ "▁nord",
+ -11.63963508605957
+ ],
+ [
+ "onal",
+ -11.639691352844238
+ ],
+ [
+ "▁captured",
+ -11.63979721069336
+ ],
+ [
+ "▁lucruri",
+ -11.640068054199219
+ ],
+ [
+ "serait",
+ -11.640192985534668
+ ],
+ [
+ "▁Members",
+ -11.640265464782715
+ ],
+ [
+ "ital",
+ -11.640275955200195
+ ],
+ [
+ "▁mounted",
+ -11.640475273132324
+ ],
+ [
+ "▁opens",
+ -11.640792846679688
+ ],
+ [
+ "▁Marie",
+ -11.640861511230469
+ ],
+ [
+ "Tech",
+ -11.640902519226074
+ ],
+ [
+ "▁wishes",
+ -11.641016006469727
+ ],
+ [
+ "▁regards",
+ -11.641073226928711
+ ],
+ [
+ "going",
+ -11.641156196594238
+ ],
+ [
+ "Opti",
+ -11.641250610351562
+ ],
+ [
+ "▁femei",
+ -11.641331672668457
+ ],
+ [
+ "▁Fish",
+ -11.64142894744873
+ ],
+ [
+ "▁mount",
+ -11.641800880432129
+ ],
+ [
+ "▁Hunt",
+ -11.641887664794922
+ ],
+ [
+ "▁probabil",
+ -11.64205265045166
+ ],
+ [
+ "▁assured",
+ -11.642191886901855
+ ],
+ [
+ "pho",
+ -11.642230033874512
+ ],
+ [
+ "▁manufactured",
+ -11.642313003540039
+ ],
+ [
+ "▁realistic",
+ -11.642437934875488
+ ],
+ [
+ "ații",
+ -11.642580032348633
+ ],
+ [
+ "▁Planning",
+ -11.642598152160645
+ ],
+ [
+ "▁român",
+ -11.642645835876465
+ ],
+ [
+ "ggy",
+ -11.642669677734375
+ ],
+ [
+ "▁produces",
+ -11.642696380615234
+ ],
+ [
+ "▁reminder",
+ -11.64284896850586
+ ],
+ [
+ "TION",
+ -11.642868041992188
+ ],
+ [
+ "▁brake",
+ -11.642909049987793
+ ],
+ [
+ "▁pla",
+ -11.643172264099121
+ ],
+ [
+ "▁Premium",
+ -11.643270492553711
+ ],
+ [
+ "▁carb",
+ -11.643310546875
+ ],
+ [
+ "▁shine",
+ -11.643390655517578
+ ],
+ [
+ "▁carrier",
+ -11.643492698669434
+ ],
+ [
+ "▁poverty",
+ -11.64350414276123
+ ],
+ [
+ "▁effectiveness",
+ -11.6436128616333
+ ],
+ [
+ "administr",
+ -11.643655776977539
+ ],
+ [
+ "▁Chamber",
+ -11.643658638000488
+ ],
+ [
+ "▁suntem",
+ -11.64376163482666
+ ],
+ [
+ "▁noastră",
+ -11.643855094909668
+ ],
+ [
+ "▁sofort",
+ -11.643877983093262
+ ],
+ [
+ "▁moisture",
+ -11.644058227539062
+ ],
+ [
+ "limb",
+ -11.6441011428833
+ ],
+ [
+ "entre",
+ -11.644328117370605
+ ],
+ [
+ "▁SD",
+ -11.644330978393555
+ ],
+ [
+ "▁BC",
+ -11.644539833068848
+ ],
+ [
+ "▁selecting",
+ -11.6445951461792
+ ],
+ [
+ "achieving",
+ -11.644673347473145
+ ],
+ [
+ "info",
+ -11.644735336303711
+ ],
+ [
+ "▁membres",
+ -11.644983291625977
+ ],
+ [
+ "▁shoe",
+ -11.645014762878418
+ ],
+ [
+ "▁locate",
+ -11.645065307617188
+ ],
+ [
+ "▁assignment",
+ -11.645085334777832
+ ],
+ [
+ "lern",
+ -11.645283699035645
+ ],
+ [
+ "▁defeat",
+ -11.645406723022461
+ ],
+ [
+ "▁endless",
+ -11.645458221435547
+ ],
+ [
+ "▁Stunden",
+ -11.645523071289062
+ ],
+ [
+ "то",
+ -11.645561218261719
+ ],
+ [
+ "▁mur",
+ -11.645586013793945
+ ],
+ [
+ "▁wissen",
+ -11.645844459533691
+ ],
+ [
+ "aime",
+ -11.645915031433105
+ ],
+ [
+ "1-2",
+ -11.646056175231934
+ ],
+ [
+ "▁femme",
+ -11.646212577819824
+ ],
+ [
+ "robe",
+ -11.646468162536621
+ ],
+ [
+ "▁embrace",
+ -11.64647102355957
+ ],
+ [
+ "▁baseball",
+ -11.646614074707031
+ ],
+ [
+ "▁hunting",
+ -11.64663314819336
+ ],
+ [
+ "betrieb",
+ -11.646790504455566
+ ],
+ [
+ "▁gardens",
+ -11.647045135498047
+ ],
+ [
+ "▁risc",
+ -11.647096633911133
+ ],
+ [
+ "▁Cri",
+ -11.647263526916504
+ ],
+ [
+ "best",
+ -11.647506713867188
+ ],
+ [
+ "▁Audio",
+ -11.647621154785156
+ ],
+ [
+ "▁intens",
+ -11.647659301757812
+ ],
+ [
+ "▁Round",
+ -11.647744178771973
+ ],
+ [
+ "▁fireplace",
+ -11.6478271484375
+ ],
+ [
+ "▁dozen",
+ -11.647912979125977
+ ],
+ [
+ "▁hospitals",
+ -11.64802360534668
+ ],
+ [
+ "▁profits",
+ -11.648076057434082
+ ],
+ [
+ "▁Mail",
+ -11.64811897277832
+ ],
+ [
+ "obtenir",
+ -11.648191452026367
+ ],
+ [
+ "▁Ross",
+ -11.648241996765137
+ ],
+ [
+ "bun",
+ -11.648573875427246
+ ],
+ [
+ "polar",
+ -11.648688316345215
+ ],
+ [
+ "▁reflection",
+ -11.648873329162598
+ ],
+ [
+ "▁fut",
+ -11.648992538452148
+ ],
+ [
+ "phon",
+ -11.649017333984375
+ ],
+ [
+ "deck",
+ -11.649094581604004
+ ],
+ [
+ "renowned",
+ -11.649188041687012
+ ],
+ [
+ "▁cate",
+ -11.649308204650879
+ ],
+ [
+ "▁decorative",
+ -11.6494722366333
+ ],
+ [
+ "ieri",
+ -11.64957332611084
+ ],
+ [
+ "▁Tap",
+ -11.64958381652832
+ ],
+ [
+ "▁Dallas",
+ -11.649600982666016
+ ],
+ [
+ "rik",
+ -11.649665832519531
+ ],
+ [
+ "▁pied",
+ -11.649727821350098
+ ],
+ [
+ "rés",
+ -11.649821281433105
+ ],
+ [
+ "ppy",
+ -11.650137901306152
+ ],
+ [
+ "▁bitte",
+ -11.650188446044922
+ ],
+ [
+ "▁cave",
+ -11.650257110595703
+ ],
+ [
+ "▁rescue",
+ -11.650559425354004
+ ],
+ [
+ "▁Hilfe",
+ -11.650714874267578
+ ],
+ [
+ "▁Jason",
+ -11.650786399841309
+ ],
+ [
+ "▁Nations",
+ -11.650838851928711
+ ],
+ [
+ "▁profil",
+ -11.650938987731934
+ ],
+ [
+ "▁Atlantic",
+ -11.651105880737305
+ ],
+ [
+ "▁rub",
+ -11.651126861572266
+ ],
+ [
+ "▁collaborative",
+ -11.65113353729248
+ ],
+ [
+ "étude",
+ -11.651150703430176
+ ],
+ [
+ "▁Workshop",
+ -11.651389122009277
+ ],
+ [
+ "nez",
+ -11.651628494262695
+ ],
+ [
+ "▁chacun",
+ -11.651714324951172
+ ],
+ [
+ "▁Too",
+ -11.65211296081543
+ ],
+ [
+ "App",
+ -11.652313232421875
+ ],
+ [
+ "▁conseil",
+ -11.652399063110352
+ ],
+ [
+ "▁signals",
+ -11.652474403381348
+ ],
+ [
+ "▁Dead",
+ -11.652497291564941
+ ],
+ [
+ "▁Austria",
+ -11.652522087097168
+ ],
+ [
+ "▁slots",
+ -11.652579307556152
+ ],
+ [
+ "▁Dies",
+ -11.652623176574707
+ ],
+ [
+ "raj",
+ -11.652629852294922
+ ],
+ [
+ "stick",
+ -11.652833938598633
+ ],
+ [
+ "▁jaw",
+ -11.653030395507812
+ ],
+ [
+ "▁lounge",
+ -11.653059005737305
+ ],
+ [
+ "curi",
+ -11.653359413146973
+ ],
+ [
+ "nem",
+ -11.653456687927246
+ ],
+ [
+ "▁Cluj",
+ -11.653512954711914
+ ],
+ [
+ "▁rapide",
+ -11.653584480285645
+ ],
+ [
+ "▁companion",
+ -11.653716087341309
+ ],
+ [
+ "▁WE",
+ -11.653879165649414
+ ],
+ [
+ "▁bord",
+ -11.65389347076416
+ ],
+ [
+ "ody",
+ -11.654045104980469
+ ],
+ [
+ "gru",
+ -11.654057502746582
+ ],
+ [
+ "▁46",
+ -11.654410362243652
+ ],
+ [
+ "kra",
+ -11.654717445373535
+ ],
+ [
+ "eller",
+ -11.65477180480957
+ ],
+ [
+ "naire",
+ -11.65511703491211
+ ],
+ [
+ "hose",
+ -11.655253410339355
+ ],
+ [
+ "▁Atlanta",
+ -11.655254364013672
+ ],
+ [
+ "▁violent",
+ -11.65530776977539
+ ],
+ [
+ "▁imagination",
+ -11.655352592468262
+ ],
+ [
+ "▁reward",
+ -11.655389785766602
+ ],
+ [
+ "▁Korean",
+ -11.655441284179688
+ ],
+ [
+ "▁branches",
+ -11.655501365661621
+ ],
+ [
+ "▁GPS",
+ -11.655625343322754
+ ],
+ [
+ "glo",
+ -11.655633926391602
+ ],
+ [
+ "▁condo",
+ -11.655705451965332
+ ],
+ [
+ "▁Investment",
+ -11.655765533447266
+ ],
+ [
+ "▁involvement",
+ -11.655813217163086
+ ],
+ [
+ "▁trap",
+ -11.655829429626465
+ ],
+ [
+ "▁schön",
+ -11.655872344970703
+ ],
+ [
+ "▁ofera",
+ -11.655933380126953
+ ],
+ [
+ "▁unterschiedlich",
+ -11.65596866607666
+ ],
+ [
+ "Net",
+ -11.655987739562988
+ ],
+ [
+ "▁predict",
+ -11.656113624572754
+ ],
+ [
+ "identifying",
+ -11.656309127807617
+ ],
+ [
+ "▁noir",
+ -11.6566162109375
+ ],
+ [
+ "kos",
+ -11.656816482543945
+ ],
+ [
+ "poz",
+ -11.656816482543945
+ ],
+ [
+ "▁11,",
+ -11.65698528289795
+ ],
+ [
+ "▁fitted",
+ -11.657384872436523
+ ],
+ [
+ "MU",
+ -11.657469749450684
+ ],
+ [
+ "TT",
+ -11.657645225524902
+ ],
+ [
+ "▁vrea",
+ -11.657846450805664
+ ],
+ [
+ "▁wound",
+ -11.657864570617676
+ ],
+ [
+ "lac",
+ -11.657971382141113
+ ],
+ [
+ "▁purchases",
+ -11.658409118652344
+ ],
+ [
+ "▁Cape",
+ -11.65843677520752
+ ],
+ [
+ "▁Foto",
+ -11.658537864685059
+ ],
+ [
+ "▁acres",
+ -11.65865707397461
+ ],
+ [
+ "▁nec",
+ -11.658677101135254
+ ],
+ [
+ "▁burning",
+ -11.659050941467285
+ ],
+ [
+ "conf",
+ -11.659457206726074
+ ],
+ [
+ "▁browse",
+ -11.659486770629883
+ ],
+ [
+ "ural",
+ -11.659762382507324
+ ],
+ [
+ "▁Ah",
+ -11.659841537475586
+ ],
+ [
+ "▁stellt",
+ -11.65992259979248
+ ],
+ [
+ "▁ratings",
+ -11.660012245178223
+ ],
+ [
+ "▁Bowl",
+ -11.660027503967285
+ ],
+ [
+ "▁grav",
+ -11.660289764404297
+ ],
+ [
+ "titi",
+ -11.66048526763916
+ ],
+ [
+ "▁prêt",
+ -11.66075325012207
+ ],
+ [
+ "▁fallen",
+ -11.660818099975586
+ ],
+ [
+ "▁nombreuses",
+ -11.660940170288086
+ ],
+ [
+ "train",
+ -11.660953521728516
+ ],
+ [
+ "ène",
+ -11.661009788513184
+ ],
+ [
+ "Aceasta",
+ -11.661091804504395
+ ],
+ [
+ "▁drill",
+ -11.661421775817871
+ ],
+ [
+ "▁Exam",
+ -11.661477088928223
+ ],
+ [
+ "▁Furniture",
+ -11.661651611328125
+ ],
+ [
+ "eanu",
+ -11.661919593811035
+ ],
+ [
+ "étant",
+ -11.66230297088623
+ ],
+ [
+ "sville",
+ -11.662391662597656
+ ],
+ [
+ "▁swim",
+ -11.662796020507812
+ ],
+ [
+ "▁routes",
+ -11.662826538085938
+ ],
+ [
+ "INE",
+ -11.662860870361328
+ ],
+ [
+ "▁Por",
+ -11.662976264953613
+ ],
+ [
+ "ither",
+ -11.663168907165527
+ ],
+ [
+ "▁optim",
+ -11.663180351257324
+ ],
+ [
+ "▁lua",
+ -11.66331958770752
+ ],
+ [
+ "▁myth",
+ -11.663491249084473
+ ],
+ [
+ "▁Bett",
+ -11.6635103225708
+ ],
+ [
+ "chim",
+ -11.66355037689209
+ ],
+ [
+ "▁cyber",
+ -11.663553237915039
+ ],
+ [
+ "▁engineer",
+ -11.663825035095215
+ ],
+ [
+ "▁exploration",
+ -11.663918495178223
+ ],
+ [
+ "arranged",
+ -11.663973808288574
+ ],
+ [
+ "▁aged",
+ -11.663993835449219
+ ],
+ [
+ "▁beau",
+ -11.664024353027344
+ ],
+ [
+ "OUT",
+ -11.66402530670166
+ ],
+ [
+ "▁Minnesota",
+ -11.664031982421875
+ ],
+ [
+ "tress",
+ -11.664407730102539
+ ],
+ [
+ "▁Commercial",
+ -11.664509773254395
+ ],
+ [
+ "▁inspiring",
+ -11.66462516784668
+ ],
+ [
+ "▁Mare",
+ -11.664725303649902
+ ],
+ [
+ "apa",
+ -11.665140151977539
+ ],
+ [
+ "▁ignore",
+ -11.6651611328125
+ ],
+ [
+ "▁gros",
+ -11.665186882019043
+ ],
+ [
+ "▁measurement",
+ -11.66531753540039
+ ],
+ [
+ "ager",
+ -11.665395736694336
+ ],
+ [
+ "intele",
+ -11.665966987609863
+ ],
+ [
+ "▁suspension",
+ -11.666180610656738
+ ],
+ [
+ "▁cultures",
+ -11.666211128234863
+ ],
+ [
+ "▁Wow",
+ -11.666231155395508
+ ],
+ [
+ "▁pushing",
+ -11.666363716125488
+ ],
+ [
+ "▁bands",
+ -11.666438102722168
+ ],
+ [
+ "nage",
+ -11.666450500488281
+ ],
+ [
+ "▁Math",
+ -11.666515350341797
+ ],
+ [
+ "comb",
+ -11.66658878326416
+ ],
+ [
+ "▁créer",
+ -11.66658878326416
+ ],
+ [
+ "▁Lewis",
+ -11.666685104370117
+ ],
+ [
+ "▁VI",
+ -11.66678524017334
+ ],
+ [
+ "emploi",
+ -11.666791915893555
+ ],
+ [
+ "▁elections",
+ -11.666890144348145
+ ],
+ [
+ "▁logic",
+ -11.666982650756836
+ ],
+ [
+ "▁unlike",
+ -11.667122840881348
+ ],
+ [
+ "▁Matthew",
+ -11.66743278503418
+ ],
+ [
+ "▁pă",
+ -11.667486190795898
+ ],
+ [
+ "oxy",
+ -11.667620658874512
+ ],
+ [
+ "équipe",
+ -11.667717933654785
+ ],
+ [
+ "▁worden",
+ -11.668088912963867
+ ],
+ [
+ "dev",
+ -11.668258666992188
+ ],
+ [
+ "▁Massachusetts",
+ -11.668691635131836
+ ],
+ [
+ "▁Return",
+ -11.668695449829102
+ ],
+ [
+ "▁Friends",
+ -11.66891098022461
+ ],
+ [
+ "▁movements",
+ -11.66894245147705
+ ],
+ [
+ "chie",
+ -11.668964385986328
+ ],
+ [
+ "rak",
+ -11.669017791748047
+ ],
+ [
+ "▁Fit",
+ -11.66904354095459
+ ],
+ [
+ "▁copil",
+ -11.669113159179688
+ ],
+ [
+ "iunii",
+ -11.669188499450684
+ ],
+ [
+ "▁intensive",
+ -11.669234275817871
+ ],
+ [
+ "▁rug",
+ -11.669452667236328
+ ],
+ [
+ "lichkeit",
+ -11.669686317443848
+ ],
+ [
+ "kov",
+ -11.669724464416504
+ ],
+ [
+ "▁pense",
+ -11.66978645324707
+ ],
+ [
+ "pop",
+ -11.66978931427002
+ ],
+ [
+ "▁closet",
+ -11.669865608215332
+ ],
+ [
+ "▁prevention",
+ -11.669920921325684
+ ],
+ [
+ "▁Deb",
+ -11.670256614685059
+ ],
+ [
+ "▁devant",
+ -11.670430183410645
+ ],
+ [
+ "▁construit",
+ -11.670440673828125
+ ],
+ [
+ "▁breaks",
+ -11.67082405090332
+ ],
+ [
+ "otic",
+ -11.670886993408203
+ ],
+ [
+ "▁dig",
+ -11.67088794708252
+ ],
+ [
+ "▁près",
+ -11.670930862426758
+ ],
+ [
+ "chte",
+ -11.671029090881348
+ ],
+ [
+ "▁Chat",
+ -11.671029090881348
+ ],
+ [
+ "wel",
+ -11.671219825744629
+ ],
+ [
+ "▁edges",
+ -11.671272277832031
+ ],
+ [
+ "▁keen",
+ -11.671419143676758
+ ],
+ [
+ "▁infant",
+ -11.671716690063477
+ ],
+ [
+ "▁Hills",
+ -11.6719388961792
+ ],
+ [
+ "▁grounds",
+ -11.671969413757324
+ ],
+ [
+ "▁hab",
+ -11.672039031982422
+ ],
+ [
+ "▁Mun",
+ -11.67215347290039
+ ],
+ [
+ "▁references",
+ -11.672215461730957
+ ],
+ [
+ "▁hearts",
+ -11.672446250915527
+ ],
+ [
+ "exprim",
+ -11.672487258911133
+ ],
+ [
+ "▁tratament",
+ -11.672553062438965
+ ],
+ [
+ "LD",
+ -11.67258358001709
+ ],
+ [
+ "ssel",
+ -11.67275333404541
+ ],
+ [
+ "cover",
+ -11.672782897949219
+ ],
+ [
+ "bridge",
+ -11.672837257385254
+ ],
+ [
+ "▁Wein",
+ -11.672924995422363
+ ],
+ [
+ "▁voiture",
+ -11.673035621643066
+ ],
+ [
+ "▁Gemeinde",
+ -11.67313289642334
+ ],
+ [
+ "AI",
+ -11.673169136047363
+ ],
+ [
+ "▁renovation",
+ -11.673264503479004
+ ],
+ [
+ "bid",
+ -11.673285484313965
+ ],
+ [
+ "▁Reading",
+ -11.673481941223145
+ ],
+ [
+ "▁Gor",
+ -11.673490524291992
+ ],
+ [
+ "fur",
+ -11.673527717590332
+ ],
+ [
+ "▁Yoga",
+ -11.673544883728027
+ ],
+ [
+ "▁exclusively",
+ -11.673630714416504
+ ],
+ [
+ "▁emissions",
+ -11.67385482788086
+ ],
+ [
+ "ète",
+ -11.673905372619629
+ ],
+ [
+ "▁glasses",
+ -11.674055099487305
+ ],
+ [
+ "▁organizat",
+ -11.674135208129883
+ ],
+ [
+ "▁washing",
+ -11.67415714263916
+ ],
+ [
+ "▁Audi",
+ -11.674173355102539
+ ],
+ [
+ "▁Labor",
+ -11.674331665039062
+ ],
+ [
+ "▁legacy",
+ -11.674381256103516
+ ],
+ [
+ "▁abstract",
+ -11.674519538879395
+ ],
+ [
+ "▁knowledgeable",
+ -11.674601554870605
+ ],
+ [
+ "▁Glo",
+ -11.674795150756836
+ ],
+ [
+ "▁pregnant",
+ -11.67481803894043
+ ],
+ [
+ "liter",
+ -11.674851417541504
+ ],
+ [
+ "▁paintings",
+ -11.67522144317627
+ ],
+ [
+ "▁tête",
+ -11.675244331359863
+ ],
+ [
+ "voy",
+ -11.675626754760742
+ ],
+ [
+ "▁Jacob",
+ -11.675667762756348
+ ],
+ [
+ "▁dressing",
+ -11.675679206848145
+ ],
+ [
+ "▁provisions",
+ -11.675768852233887
+ ],
+ [
+ "bahn",
+ -11.675870895385742
+ ],
+ [
+ "▁depict",
+ -11.675875663757324
+ ],
+ [
+ "AW",
+ -11.676068305969238
+ ],
+ [
+ "▁bleibt",
+ -11.676163673400879
+ ],
+ [
+ "AND",
+ -11.676292419433594
+ ],
+ [
+ "▁fünf",
+ -11.676386833190918
+ ],
+ [
+ "▁hosts",
+ -11.676426887512207
+ ],
+ [
+ "vas",
+ -11.676708221435547
+ ],
+ [
+ "DO",
+ -11.67674732208252
+ ],
+ [
+ "▁max",
+ -11.676753997802734
+ ],
+ [
+ "▁contributed",
+ -11.676774978637695
+ ],
+ [
+ "roz",
+ -11.676796913146973
+ ],
+ [
+ "▁deschis",
+ -11.676800727844238
+ ],
+ [
+ "itaire",
+ -11.676809310913086
+ ],
+ [
+ "tube",
+ -11.676959991455078
+ ],
+ [
+ "▁Beck",
+ -11.676959991455078
+ ],
+ [
+ "▁curious",
+ -11.677130699157715
+ ],
+ [
+ "▁waves",
+ -11.677178382873535
+ ],
+ [
+ "▁regret",
+ -11.677248001098633
+ ],
+ [
+ "FO",
+ -11.677326202392578
+ ],
+ [
+ "droit",
+ -11.67734146118164
+ ],
+ [
+ "rö",
+ -11.677565574645996
+ ],
+ [
+ "▁Panel",
+ -11.677624702453613
+ ],
+ [
+ "▁pile",
+ -11.677660942077637
+ ],
+ [
+ "▁installing",
+ -11.677674293518066
+ ],
+ [
+ "▁Intr",
+ -11.677797317504883
+ ],
+ [
+ "nung",
+ -11.677823066711426
+ ],
+ [
+ "▁Outdoor",
+ -11.677855491638184
+ ],
+ [
+ "▁generator",
+ -11.67786693572998
+ ],
+ [
+ "▁zahlreiche",
+ -11.677868843078613
+ ],
+ [
+ "▁Third",
+ -11.67813491821289
+ ],
+ [
+ "frac",
+ -11.678180694580078
+ ],
+ [
+ "ovi",
+ -11.678236961364746
+ ],
+ [
+ "▁Casa",
+ -11.678374290466309
+ ],
+ [
+ "▁stomach",
+ -11.678393363952637
+ ],
+ [
+ "▁Lincoln",
+ -11.67844009399414
+ ],
+ [
+ "▁Electronic",
+ -11.678584098815918
+ ],
+ [
+ "coding",
+ -11.67895221710205
+ ],
+ [
+ "2017",
+ -11.67900276184082
+ ],
+ [
+ "▁friendship",
+ -11.679238319396973
+ ],
+ [
+ "ried",
+ -11.679250717163086
+ ],
+ [
+ "но",
+ -11.679265022277832
+ ],
+ [
+ "▁tail",
+ -11.679267883300781
+ ],
+ [
+ "▁petits",
+ -11.679308891296387
+ ],
+ [
+ "▁réseau",
+ -11.679696083068848
+ ],
+ [
+ "▁churches",
+ -11.679999351501465
+ ],
+ [
+ "▁marketplace",
+ -11.680062294006348
+ ],
+ [
+ "▁Pool",
+ -11.680318832397461
+ ],
+ [
+ "▁popularity",
+ -11.680455207824707
+ ],
+ [
+ "▁sprijin",
+ -11.680496215820312
+ ],
+ [
+ "▁Od",
+ -11.680527687072754
+ ],
+ [
+ "▁Transfer",
+ -11.680562973022461
+ ],
+ [
+ "▁fake",
+ -11.680791854858398
+ ],
+ [
+ "▁9,",
+ -11.681007385253906
+ ],
+ [
+ "▁weit",
+ -11.681264877319336
+ ],
+ [
+ "▁relaxed",
+ -11.681415557861328
+ ],
+ [
+ "pig",
+ -11.68161678314209
+ ],
+ [
+ "▁Lauren",
+ -11.68166732788086
+ ],
+ [
+ "gesetzt",
+ -11.681669235229492
+ ],
+ [
+ "▁Clar",
+ -11.681694984436035
+ ],
+ [
+ "▁unlikely",
+ -11.681731224060059
+ ],
+ [
+ "color",
+ -11.681832313537598
+ ],
+ [
+ "▁spouse",
+ -11.681843757629395
+ ],
+ [
+ "▁facile",
+ -11.681859970092773
+ ],
+ [
+ "▁Speed",
+ -11.681872367858887
+ ],
+ [
+ "KE",
+ -11.682230949401855
+ ],
+ [
+ "▁PO",
+ -11.68231201171875
+ ],
+ [
+ "▁Channel",
+ -11.682321548461914
+ ],
+ [
+ "argent",
+ -11.682356834411621
+ ],
+ [
+ "▁Making",
+ -11.682430267333984
+ ],
+ [
+ "▁Coll",
+ -11.682585716247559
+ ],
+ [
+ "cci",
+ -11.682721138000488
+ ],
+ [
+ "corresponding",
+ -11.68300724029541
+ ],
+ [
+ "▁heaven",
+ -11.683160781860352
+ ],
+ [
+ "ţă",
+ -11.68319320678711
+ ],
+ [
+ "▁darüber",
+ -11.683236122131348
+ ],
+ [
+ "acted",
+ -11.683420181274414
+ ],
+ [
+ "only",
+ -11.683460235595703
+ ],
+ [
+ "▁slight",
+ -11.683465003967285
+ ],
+ [
+ "lian",
+ -11.68348503112793
+ ],
+ [
+ "flă",
+ -11.683510780334473
+ ],
+ [
+ "▁vulnerable",
+ -11.683530807495117
+ ],
+ [
+ "▁creator",
+ -11.68356704711914
+ ],
+ [
+ "▁protecting",
+ -11.68360424041748
+ ],
+ [
+ "writing",
+ -11.68360710144043
+ ],
+ [
+ "▁Ter",
+ -11.68387222290039
+ ],
+ [
+ "▁barb",
+ -11.683987617492676
+ ],
+ [
+ "▁dată",
+ -11.683995246887207
+ ],
+ [
+ "▁Screen",
+ -11.684052467346191
+ ],
+ [
+ "▁BBC",
+ -11.684082984924316
+ ],
+ [
+ "Col",
+ -11.684206008911133
+ ],
+ [
+ "fung",
+ -11.684453964233398
+ ],
+ [
+ "▁dreptul",
+ -11.684494972229004
+ ],
+ [
+ "derived",
+ -11.684538841247559
+ ],
+ [
+ "▁designated",
+ -11.684553146362305
+ ],
+ [
+ "▁interactions",
+ -11.684617042541504
+ ],
+ [
+ "SG",
+ -11.684621810913086
+ ],
+ [
+ "▁häufig",
+ -11.684625625610352
+ ],
+ [
+ "▁Mega",
+ -11.684638023376465
+ ],
+ [
+ "▁jazz",
+ -11.684660911560059
+ ],
+ [
+ "lbs",
+ -11.684797286987305
+ ],
+ [
+ "▁Manual",
+ -11.68484115600586
+ ],
+ [
+ "pushed",
+ -11.685017585754395
+ ],
+ [
+ "▁analytics",
+ -11.685234069824219
+ ],
+ [
+ "▁lawsuit",
+ -11.68533706665039
+ ],
+ [
+ "▁gray",
+ -11.685364723205566
+ ],
+ [
+ "shirts",
+ -11.685401916503906
+ ],
+ [
+ "▁hill",
+ -11.685508728027344
+ ],
+ [
+ "▁1991",
+ -11.68550968170166
+ ],
+ [
+ "▁obligations",
+ -11.685568809509277
+ ],
+ [
+ "▁Dubai",
+ -11.68580436706543
+ ],
+ [
+ "()",
+ -11.685808181762695
+ ],
+ [
+ "▁acceptable",
+ -11.685810089111328
+ ],
+ [
+ "therapist",
+ -11.685877799987793
+ ],
+ [
+ "inger",
+ -11.6860990524292
+ ],
+ [
+ "▁territory",
+ -11.686208724975586
+ ],
+ [
+ "▁sang",
+ -11.6862211227417
+ ],
+ [
+ "ät",
+ -11.686224937438965
+ ],
+ [
+ "▁Zukunft",
+ -11.686238288879395
+ ],
+ [
+ "TU",
+ -11.68657398223877
+ ],
+ [
+ "▁horizontal",
+ -11.68665599822998
+ ],
+ [
+ "▁entrepreneurs",
+ -11.686710357666016
+ ],
+ [
+ "▁Eltern",
+ -11.687017440795898
+ ],
+ [
+ "▁presentations",
+ -11.687129974365234
+ ],
+ [
+ "▁confirmation",
+ -11.687173843383789
+ ],
+ [
+ "▁technological",
+ -11.687432289123535
+ ],
+ [
+ "▁1989",
+ -11.687530517578125
+ ],
+ [
+ "EF",
+ -11.687640190124512
+ ],
+ [
+ "ponent",
+ -11.687663078308105
+ ],
+ [
+ "NET",
+ -11.687699317932129
+ ],
+ [
+ "750",
+ -11.687772750854492
+ ],
+ [
+ "▁desert",
+ -11.687891960144043
+ ],
+ [
+ "▁contribu",
+ -11.687932968139648
+ ],
+ [
+ "▁Gun",
+ -11.687944412231445
+ ],
+ [
+ "▁Juli",
+ -11.688091278076172
+ ],
+ [
+ "ERS",
+ -11.688261985778809
+ ],
+ [
+ "▁inceput",
+ -11.688261985778809
+ ],
+ [
+ "▁answered",
+ -11.688369750976562
+ ],
+ [
+ "▁basement",
+ -11.688410758972168
+ ],
+ [
+ "film",
+ -11.688434600830078
+ ],
+ [
+ "▁taille",
+ -11.688593864440918
+ ],
+ [
+ "▁survival",
+ -11.688655853271484
+ ],
+ [
+ "ihnen",
+ -11.68869400024414
+ ],
+ [
+ "▁Bird",
+ -11.688840866088867
+ ],
+ [
+ "speed",
+ -11.689336776733398
+ ],
+ [
+ "▁journalist",
+ -11.68941879272461
+ ],
+ [
+ "▁Indonesia",
+ -11.689626693725586
+ ],
+ [
+ "▁15.",
+ -11.689973831176758
+ ],
+ [
+ "▁19.",
+ -11.690025329589844
+ ],
+ [
+ "étaient",
+ -11.690114974975586
+ ],
+ [
+ "▁tennis",
+ -11.69024658203125
+ ],
+ [
+ "▁aproximativ",
+ -11.69039249420166
+ ],
+ [
+ "▁Hans",
+ -11.690650939941406
+ ],
+ [
+ "▁Remove",
+ -11.69067096710205
+ ],
+ [
+ "▁cats",
+ -11.691022872924805
+ ],
+ [
+ "▁calories",
+ -11.691052436828613
+ ],
+ [
+ "▁limitations",
+ -11.69119644165039
+ ],
+ [
+ "▁subscribe",
+ -11.691198348999023
+ ],
+ [
+ "▁Dem",
+ -11.691339492797852
+ ],
+ [
+ "lust",
+ -11.691370010375977
+ ],
+ [
+ "▁adresa",
+ -11.691394805908203
+ ],
+ [
+ "▁sais",
+ -11.69140911102295
+ ],
+ [
+ "...\"",
+ -11.691473960876465
+ ],
+ [
+ "▁Luft",
+ -11.691485404968262
+ ],
+ [
+ "DL",
+ -11.691597938537598
+ ],
+ [
+ "▁estimates",
+ -11.691600799560547
+ ],
+ [
+ "▁protocol",
+ -11.691603660583496
+ ],
+ [
+ "▁Namen",
+ -11.691776275634766
+ ],
+ [
+ "▁grands",
+ -11.691901206970215
+ ],
+ [
+ "▁voter",
+ -11.691970825195312
+ ],
+ [
+ "▁vacuum",
+ -11.692075729370117
+ ],
+ [
+ "▁versch",
+ -11.692103385925293
+ ],
+ [
+ "▁Democratic",
+ -11.692107200622559
+ ],
+ [
+ "▁Books",
+ -11.692170143127441
+ ],
+ [
+ "▁frames",
+ -11.692727088928223
+ ],
+ [
+ "▁Bee",
+ -11.692864418029785
+ ],
+ [
+ "▁helfen",
+ -11.692934036254883
+ ],
+ [
+ "▁dive",
+ -11.692963600158691
+ ],
+ [
+ "▁physician",
+ -11.693037033081055
+ ],
+ [
+ "▁powered",
+ -11.693131446838379
+ ],
+ [
+ "▁zones",
+ -11.693337440490723
+ ],
+ [
+ "▁regime",
+ -11.69345474243164
+ ],
+ [
+ "check",
+ -11.693578720092773
+ ],
+ [
+ "11.",
+ -11.693793296813965
+ ],
+ [
+ "▁plaisir",
+ -11.693793296813965
+ ],
+ [
+ "▁physically",
+ -11.693811416625977
+ ],
+ [
+ "▁Pul",
+ -11.694245338439941
+ ],
+ [
+ "▁jardin",
+ -11.694294929504395
+ ],
+ [
+ "▁Nur",
+ -11.694417953491211
+ ],
+ [
+ "WC",
+ -11.694425582885742
+ ],
+ [
+ "▁Lock",
+ -11.694506645202637
+ ],
+ [
+ "▁économique",
+ -11.694530487060547
+ ],
+ [
+ "user",
+ -11.694536209106445
+ ],
+ [
+ "▁commit",
+ -11.694731712341309
+ ],
+ [
+ "▁oldest",
+ -11.694764137268066
+ ],
+ [
+ "▁fulfill",
+ -11.694780349731445
+ ],
+ [
+ "▁nervous",
+ -11.69482135772705
+ ],
+ [
+ "▁SH",
+ -11.695014953613281
+ ],
+ [
+ "SK",
+ -11.695150375366211
+ ],
+ [
+ "▁plein",
+ -11.695291519165039
+ ],
+ [
+ "show",
+ -11.695354461669922
+ ],
+ [
+ "▁disability",
+ -11.695356369018555
+ ],
+ [
+ "papier",
+ -11.69544506072998
+ ],
+ [
+ "▁Corp",
+ -11.695611000061035
+ ],
+ [
+ "ători",
+ -11.695676803588867
+ ],
+ [
+ "nţă",
+ -11.695813179016113
+ ],
+ [
+ "▁overseas",
+ -11.696009635925293
+ ],
+ [
+ "▁struck",
+ -11.69603157043457
+ ],
+ [
+ "astic",
+ -11.69607162475586
+ ],
+ [
+ "▁advised",
+ -11.696088790893555
+ ],
+ [
+ "BE",
+ -11.696161270141602
+ ],
+ [
+ "▁UV",
+ -11.696218490600586
+ ],
+ [
+ "patient",
+ -11.69626235961914
+ ],
+ [
+ "▁texte",
+ -11.696344375610352
+ ],
+ [
+ "▁timely",
+ -11.696444511413574
+ ],
+ [
+ "used",
+ -11.696471214294434
+ ],
+ [
+ "▁occasionally",
+ -11.696524620056152
+ ],
+ [
+ "▁entries",
+ -11.696550369262695
+ ],
+ [
+ "underlying",
+ -11.6967191696167
+ ],
+ [
+ "01.",
+ -11.696748733520508
+ ],
+ [
+ "▁automated",
+ -11.696791648864746
+ ],
+ [
+ "yes",
+ -11.696828842163086
+ ],
+ [
+ "▁Staff",
+ -11.697057723999023
+ ],
+ [
+ "▁Einzel",
+ -11.697546005249023
+ ],
+ [
+ "quit",
+ -11.697687149047852
+ ],
+ [
+ "▁Cela",
+ -11.697951316833496
+ ],
+ [
+ "▁snap",
+ -11.698298454284668
+ ],
+ [
+ "▁followers",
+ -11.698330879211426
+ ],
+ [
+ "CN",
+ -11.698709487915039
+ ],
+ [
+ "▁Cooper",
+ -11.698892593383789
+ ],
+ [
+ "ô",
+ -11.698921203613281
+ ],
+ [
+ "▁memorable",
+ -11.698965072631836
+ ],
+ [
+ "▁jur",
+ -11.698996543884277
+ ],
+ [
+ "▁ajutorul",
+ -11.69905948638916
+ ],
+ [
+ "▁Enter",
+ -11.6991548538208
+ ],
+ [
+ "Often",
+ -11.699294090270996
+ ],
+ [
+ "▁dintr",
+ -11.699341773986816
+ ],
+ [
+ "-30",
+ -11.699419975280762
+ ],
+ [
+ "ESS",
+ -11.699454307556152
+ ],
+ [
+ "▁weird",
+ -11.699462890625
+ ],
+ [
+ "▁Animal",
+ -11.699706077575684
+ ],
+ [
+ "▁complement",
+ -11.699719429016113
+ ],
+ [
+ "▁Bot",
+ -11.699756622314453
+ ],
+ [
+ "▁darf",
+ -11.699764251708984
+ ],
+ [
+ "yed",
+ -11.699808120727539
+ ],
+ [
+ "▁Mul",
+ -11.699872016906738
+ ],
+ [
+ "lick",
+ -11.700080871582031
+ ],
+ [
+ "▁Cambridge",
+ -11.700216293334961
+ ],
+ [
+ "adore",
+ -11.700407981872559
+ ],
+ [
+ "▁Dutch",
+ -11.700420379638672
+ ],
+ [
+ "▁Castle",
+ -11.700431823730469
+ ],
+ [
+ "igi",
+ -11.700563430786133
+ ],
+ [
+ "▁enemy",
+ -11.70071029663086
+ ],
+ [
+ "accompanied",
+ -11.700725555419922
+ ],
+ [
+ "▁teren",
+ -11.701102256774902
+ ],
+ [
+ "▁ET",
+ -11.701498985290527
+ ],
+ [
+ "ffle",
+ -11.701557159423828
+ ],
+ [
+ "-15",
+ -11.701651573181152
+ ],
+ [
+ "▁Geo",
+ -11.701680183410645
+ ],
+ [
+ "▁attractions",
+ -11.701730728149414
+ ],
+ [
+ "iker",
+ -11.70185661315918
+ ],
+ [
+ "▁bă",
+ -11.701990127563477
+ ],
+ [
+ "▁heal",
+ -11.701995849609375
+ ],
+ [
+ "weisen",
+ -11.702144622802734
+ ],
+ [
+ "▁spectrum",
+ -11.702186584472656
+ ],
+ [
+ "meld",
+ -11.702394485473633
+ ],
+ [
+ "▁eveniment",
+ -11.70247745513916
+ ],
+ [
+ "arra",
+ -11.702478408813477
+ ],
+ [
+ "rete",
+ -11.70250129699707
+ ],
+ [
+ "▁Had",
+ -11.70250415802002
+ ],
+ [
+ "looking",
+ -11.702692031860352
+ ],
+ [
+ "isierung",
+ -11.702805519104004
+ ],
+ [
+ "▁moyen",
+ -11.703129768371582
+ ],
+ [
+ "▁gesamte",
+ -11.703202247619629
+ ],
+ [
+ "▁destroy",
+ -11.703407287597656
+ ],
+ [
+ "125",
+ -11.703518867492676
+ ],
+ [
+ "▁suivant",
+ -11.703913688659668
+ ],
+ [
+ "▁declared",
+ -11.703925132751465
+ ],
+ [
+ "▁Urban",
+ -11.704131126403809
+ ],
+ [
+ "▁16.",
+ -11.704168319702148
+ ],
+ [
+ "▁Beg",
+ -11.704168319702148
+ ],
+ [
+ "▁canal",
+ -11.704225540161133
+ ],
+ [
+ "▁Pres",
+ -11.70431137084961
+ ],
+ [
+ "▁geeignet",
+ -11.704339981079102
+ ],
+ [
+ "▁strat",
+ -11.704365730285645
+ ],
+ [
+ "UB",
+ -11.704395294189453
+ ],
+ [
+ "▁Alexander",
+ -11.704424858093262
+ ],
+ [
+ "cycle",
+ -11.704666137695312
+ ],
+ [
+ "▁Var",
+ -11.704802513122559
+ ],
+ [
+ "▁domin",
+ -11.704805374145508
+ ],
+ [
+ "▁lasting",
+ -11.704939842224121
+ ],
+ [
+ "terio",
+ -11.705262184143066
+ ],
+ [
+ "▁Battle",
+ -11.705339431762695
+ ],
+ [
+ "▁publications",
+ -11.705647468566895
+ ],
+ [
+ "▁implica",
+ -11.705886840820312
+ ],
+ [
+ "▁NA",
+ -11.705963134765625
+ ],
+ [
+ "▁stocks",
+ -11.706036567687988
+ ],
+ [
+ "Plat",
+ -11.70611572265625
+ ],
+ [
+ "▁excitement",
+ -11.706149101257324
+ ],
+ [
+ "▁Muslim",
+ -11.706524848937988
+ ],
+ [
+ "▁Mari",
+ -11.706530570983887
+ ],
+ [
+ "▁Ul",
+ -11.706647872924805
+ ],
+ [
+ "nächst",
+ -11.706757545471191
+ ],
+ [
+ "▁trait",
+ -11.706833839416504
+ ],
+ [
+ "▁(3)",
+ -11.706852912902832
+ ],
+ [
+ "▁Attorney",
+ -11.706894874572754
+ ],
+ [
+ "▁Malaysia",
+ -11.70689582824707
+ ],
+ [
+ "▁slab",
+ -11.706960678100586
+ ],
+ [
+ "▁dam",
+ -11.707113265991211
+ ],
+ [
+ "▁Bir",
+ -11.707226753234863
+ ],
+ [
+ "▁sing",
+ -11.70738410949707
+ ],
+ [
+ "▁Culture",
+ -11.7073974609375
+ ],
+ [
+ "UD",
+ -11.707417488098145
+ ],
+ [
+ "▁Mes",
+ -11.707443237304688
+ ],
+ [
+ "ități",
+ -11.707615852355957
+ ],
+ [
+ "▁possess",
+ -11.708173751831055
+ ],
+ [
+ "enabling",
+ -11.70820426940918
+ ],
+ [
+ "▁settled",
+ -11.708335876464844
+ ],
+ [
+ "▁sagen",
+ -11.708492279052734
+ ],
+ [
+ "▁erfolgt",
+ -11.708564758300781
+ ],
+ [
+ "dog",
+ -11.708600997924805
+ ],
+ [
+ "ndu",
+ -11.708732604980469
+ ],
+ [
+ "ității",
+ -11.708745002746582
+ ],
+ [
+ "▁Islam",
+ -11.708930015563965
+ ],
+ [
+ "▁catalog",
+ -11.708931922912598
+ ],
+ [
+ "▁simt",
+ -11.709102630615234
+ ],
+ [
+ "tische",
+ -11.709150314331055
+ ],
+ [
+ "▁Mach",
+ -11.709334373474121
+ ],
+ [
+ "▁EP",
+ -11.709359169006348
+ ],
+ [
+ "▁Certified",
+ -11.709386825561523
+ ],
+ [
+ "▁Resources",
+ -11.70945930480957
+ ],
+ [
+ "▁Past",
+ -11.709607124328613
+ ],
+ [
+ "▁Termin",
+ -11.709755897521973
+ ],
+ [
+ "▁lightweight",
+ -11.709755897521973
+ ],
+ [
+ "▁championship",
+ -11.70994758605957
+ ],
+ [
+ "gebiet",
+ -11.710122108459473
+ ],
+ [
+ "▁jurisdiction",
+ -11.710135459899902
+ ],
+ [
+ "▁euros",
+ -11.710169792175293
+ ],
+ [
+ "▁Familien",
+ -11.710554122924805
+ ],
+ [
+ "▁GT",
+ -11.710677146911621
+ ],
+ [
+ "▁dvs",
+ -11.71081256866455
+ ],
+ [
+ "▁nouveaux",
+ -11.710838317871094
+ ],
+ [
+ "▁chill",
+ -11.710916519165039
+ ],
+ [
+ "▁ridicat",
+ -11.710920333862305
+ ],
+ [
+ "his",
+ -11.711079597473145
+ ],
+ [
+ "▁Indi",
+ -11.711159706115723
+ ],
+ [
+ "▁arrested",
+ -11.71116828918457
+ ],
+ [
+ "ităţii",
+ -11.711170196533203
+ ],
+ [
+ "onul",
+ -11.711274147033691
+ ],
+ [
+ "appar",
+ -11.711296081542969
+ ],
+ [
+ "▁Bachelor",
+ -11.711297988891602
+ ],
+ [
+ "▁erfolgreich",
+ -11.711426734924316
+ ],
+ [
+ "▁versatile",
+ -11.71163558959961
+ ],
+ [
+ "▁nécessaire",
+ -11.711761474609375
+ ],
+ [
+ "▁facial",
+ -11.712160110473633
+ ],
+ [
+ "▁Bull",
+ -11.712226867675781
+ ],
+ [
+ "Comm",
+ -11.712237358093262
+ ],
+ [
+ "atte",
+ -11.712307929992676
+ ],
+ [
+ "hom",
+ -11.7123384475708
+ ],
+ [
+ "start",
+ -11.712576866149902
+ ],
+ [
+ "▁roughly",
+ -11.712936401367188
+ ],
+ [
+ "▁bay",
+ -11.712984085083008
+ ],
+ [
+ "▁american",
+ -11.712986946105957
+ ],
+ [
+ "▁Wisconsin",
+ -11.713135719299316
+ ],
+ [
+ "▁Clinton",
+ -11.713142395019531
+ ],
+ [
+ "appareil",
+ -11.713153839111328
+ ],
+ [
+ "▁liberal",
+ -11.713455200195312
+ ],
+ [
+ "▁dau",
+ -11.713519096374512
+ ],
+ [
+ "ech",
+ -11.713521957397461
+ ],
+ [
+ "2014",
+ -11.713624000549316
+ ],
+ [
+ "▁lip",
+ -11.713645935058594
+ ],
+ [
+ "▁maintenant",
+ -11.713762283325195
+ ],
+ [
+ "▁Sil",
+ -11.713805198669434
+ ],
+ [
+ "rben",
+ -11.713891983032227
+ ],
+ [
+ "▁contents",
+ -11.713980674743652
+ ],
+ [
+ "▁magnetic",
+ -11.714111328125
+ ],
+ [
+ "▁terre",
+ -11.714151382446289
+ ],
+ [
+ "▁Rights",
+ -11.714475631713867
+ ],
+ [
+ "lose",
+ -11.714570045471191
+ ],
+ [
+ "▁crown",
+ -11.71468448638916
+ ],
+ [
+ "▁oils",
+ -11.7147216796875
+ ],
+ [
+ "▁entertaining",
+ -11.714841842651367
+ ],
+ [
+ "▁Option",
+ -11.714848518371582
+ ],
+ [
+ "▁Previous",
+ -11.714916229248047
+ ],
+ [
+ "▁vrai",
+ -11.714930534362793
+ ],
+ [
+ "▁Auswahl",
+ -11.715056419372559
+ ],
+ [
+ "▁horses",
+ -11.715106010437012
+ ],
+ [
+ "▁Author",
+ -11.71533489227295
+ ],
+ [
+ "▁Writing",
+ -11.715461730957031
+ ],
+ [
+ "▁travelling",
+ -11.715522766113281
+ ],
+ [
+ "▁350",
+ -11.715567588806152
+ ],
+ [
+ "daten",
+ -11.71560287475586
+ ],
+ [
+ "zan",
+ -11.715765953063965
+ ],
+ [
+ "▁sweat",
+ -11.715924263000488
+ ],
+ [
+ "▁Junior",
+ -11.715970993041992
+ ],
+ [
+ "markt",
+ -11.71609878540039
+ ],
+ [
+ "after",
+ -11.716105461120605
+ ],
+ [
+ "▁admitted",
+ -11.716262817382812
+ ],
+ [
+ "▁1950",
+ -11.716347694396973
+ ],
+ [
+ "▁Sche",
+ -11.71648120880127
+ ],
+ [
+ "▁dorit",
+ -11.716818809509277
+ ],
+ [
+ "▁transferred",
+ -11.716958045959473
+ ],
+ [
+ "utilise",
+ -11.717194557189941
+ ],
+ [
+ "sitz",
+ -11.717301368713379
+ ],
+ [
+ "gio",
+ -11.717320442199707
+ ],
+ [
+ "▁bisher",
+ -11.717473983764648
+ ],
+ [
+ "RD",
+ -11.717491149902344
+ ],
+ [
+ "▁Wales",
+ -11.717747688293457
+ ],
+ [
+ "▁smoking",
+ -11.717904090881348
+ ],
+ [
+ "dire",
+ -11.717939376831055
+ ],
+ [
+ "▁seating",
+ -11.717979431152344
+ ],
+ [
+ "▁constat",
+ -11.718056678771973
+ ],
+ [
+ "▁Hub",
+ -11.718324661254883
+ ],
+ [
+ "▁sieht",
+ -11.718345642089844
+ ],
+ [
+ "▁prospect",
+ -11.718378067016602
+ ],
+ [
+ "▁RO",
+ -11.718413352966309
+ ],
+ [
+ "▁Wars",
+ -11.718423843383789
+ ],
+ [
+ "eek",
+ -11.718496322631836
+ ],
+ [
+ "▁Bring",
+ -11.718646049499512
+ ],
+ [
+ "▁bleiben",
+ -11.718696594238281
+ ],
+ [
+ "arri",
+ -11.718826293945312
+ ],
+ [
+ "inal",
+ -11.718904495239258
+ ],
+ [
+ "▁Maryland",
+ -11.718932151794434
+ ],
+ [
+ "▁Process",
+ -11.719145774841309
+ ],
+ [
+ "They",
+ -11.719154357910156
+ ],
+ [
+ "▁Oxford",
+ -11.719176292419434
+ ],
+ [
+ "▁neat",
+ -11.719330787658691
+ ],
+ [
+ "▁cinema",
+ -11.719597816467285
+ ],
+ [
+ "▁Ist",
+ -11.719620704650879
+ ],
+ [
+ "▁vegan",
+ -11.719682693481445
+ ],
+ [
+ "wall",
+ -11.719708442687988
+ ],
+ [
+ "▁motive",
+ -11.72010612487793
+ ],
+ [
+ "▁mature",
+ -11.720544815063477
+ ],
+ [
+ "▁Dragon",
+ -11.720653533935547
+ ],
+ [
+ "▁google",
+ -11.720677375793457
+ ],
+ [
+ "blick",
+ -11.72110652923584
+ ],
+ [
+ "▁Cod",
+ -11.721220970153809
+ ],
+ [
+ "▁suffi",
+ -11.721319198608398
+ ],
+ [
+ "▁terrorist",
+ -11.721478462219238
+ ],
+ [
+ "Posted",
+ -11.721484184265137
+ ],
+ [
+ "▁Schi",
+ -11.72157096862793
+ ],
+ [
+ "▁Marc",
+ -11.721597671508789
+ ],
+ [
+ "▁operates",
+ -11.721661567687988
+ ],
+ [
+ "gress",
+ -11.721805572509766
+ ],
+ [
+ "has",
+ -11.721899032592773
+ ],
+ [
+ "sole",
+ -11.722108840942383
+ ],
+ [
+ "▁Buck",
+ -11.722122192382812
+ ],
+ [
+ "impl",
+ -11.722160339355469
+ ],
+ [
+ "▁Ron",
+ -11.722172737121582
+ ],
+ [
+ "▁handled",
+ -11.722346305847168
+ ],
+ [
+ "▁Apr",
+ -11.722347259521484
+ ],
+ [
+ "▁Storage",
+ -11.722467422485352
+ ],
+ [
+ "▁temp",
+ -11.722512245178223
+ ],
+ [
+ "▁differently",
+ -11.722614288330078
+ ],
+ [
+ "▁wherever",
+ -11.722670555114746
+ ],
+ [
+ "matched",
+ -11.722695350646973
+ ],
+ [
+ "rios",
+ -11.72276496887207
+ ],
+ [
+ "▁surprising",
+ -11.722846031188965
+ ],
+ [
+ "teilen",
+ -11.722867965698242
+ ],
+ [
+ "▁difficulties",
+ -11.72294807434082
+ ],
+ [
+ "tab",
+ -11.723064422607422
+ ],
+ [
+ "▁Leader",
+ -11.723128318786621
+ ],
+ [
+ "implementing",
+ -11.723372459411621
+ ],
+ [
+ "▁workforce",
+ -11.723384857177734
+ ],
+ [
+ "▁bereit",
+ -11.723503112792969
+ ],
+ [
+ "vig",
+ -11.72352123260498
+ ],
+ [
+ "▁LOVE",
+ -11.723580360412598
+ ],
+ [
+ "▁instances",
+ -11.723954200744629
+ ],
+ [
+ "▁frumos",
+ -11.723960876464844
+ ],
+ [
+ "▁Java",
+ -11.723974227905273
+ ],
+ [
+ "▁arrest",
+ -11.723977088928223
+ ],
+ [
+ "▁apparent",
+ -11.724152565002441
+ ],
+ [
+ "▁hence",
+ -11.724200248718262
+ ],
+ [
+ "▁entwickelt",
+ -11.72437572479248
+ ],
+ [
+ "▁Fra",
+ -11.724471092224121
+ ],
+ [
+ "▁prend",
+ -11.724486351013184
+ ],
+ [
+ "ließ",
+ -11.724522590637207
+ ],
+ [
+ "▁drawer",
+ -11.724671363830566
+ ],
+ [
+ "ARD",
+ -11.724926948547363
+ ],
+ [
+ "▁caring",
+ -11.72499942779541
+ ],
+ [
+ "▁wollte",
+ -11.725024223327637
+ ],
+ [
+ "▁vielleicht",
+ -11.72511100769043
+ ],
+ [
+ "▁iconic",
+ -11.725324630737305
+ ],
+ [
+ "äch",
+ -11.72552490234375
+ ],
+ [
+ "abel",
+ -11.725639343261719
+ ],
+ [
+ "▁génér",
+ -11.72570514678955
+ ],
+ [
+ "ault",
+ -11.725727081298828
+ ],
+ [
+ "▁alternatives",
+ -11.725909233093262
+ ],
+ [
+ "think",
+ -11.726025581359863
+ ],
+ [
+ "ро",
+ -11.726055145263672
+ ],
+ [
+ "whereas",
+ -11.726058006286621
+ ],
+ [
+ "erei",
+ -11.726366996765137
+ ],
+ [
+ "▁Eagle",
+ -11.726766586303711
+ ],
+ [
+ "situé",
+ -11.72704792022705
+ ],
+ [
+ "▁laboratory",
+ -11.727157592773438
+ ],
+ [
+ "▁Nutzung",
+ -11.727256774902344
+ ],
+ [
+ "▁Bathroom",
+ -11.72728157043457
+ ],
+ [
+ "▁loaded",
+ -11.727293968200684
+ ],
+ [
+ "niste",
+ -11.727408409118652
+ ],
+ [
+ "som",
+ -11.727429389953613
+ ],
+ [
+ "▁aucun",
+ -11.727666854858398
+ ],
+ [
+ "gebracht",
+ -11.727676391601562
+ ],
+ [
+ "▁tomb",
+ -11.727771759033203
+ ],
+ [
+ "▁Ty",
+ -11.727785110473633
+ ],
+ [
+ "▁afaceri",
+ -11.727971076965332
+ ],
+ [
+ "tex",
+ -11.72803783416748
+ ],
+ [
+ "ality",
+ -11.728147506713867
+ ],
+ [
+ "▁identification",
+ -11.728150367736816
+ ],
+ [
+ "▁cultiv",
+ -11.728255271911621
+ ],
+ [
+ "Not",
+ -11.728326797485352
+ ],
+ [
+ "▁acestor",
+ -11.72846508026123
+ ],
+ [
+ "▁PhD",
+ -11.728466033935547
+ ],
+ [
+ "nell",
+ -11.728470802307129
+ ],
+ [
+ "▁dial",
+ -11.728594779968262
+ ],
+ [
+ "chro",
+ -11.728673934936523
+ ],
+ [
+ "▁specifications",
+ -11.728682518005371
+ ],
+ [
+ "anii",
+ -11.72877025604248
+ ],
+ [
+ "▁cloth",
+ -11.728836059570312
+ ],
+ [
+ "▁highway",
+ -11.728914260864258
+ ],
+ [
+ "▁Vitamin",
+ -11.729118347167969
+ ],
+ [
+ "▁indication",
+ -11.729349136352539
+ ],
+ [
+ "80%",
+ -11.72959041595459
+ ],
+ [
+ "▁Lion",
+ -11.729681015014648
+ ],
+ [
+ "▁10,",
+ -11.729693412780762
+ ],
+ [
+ "▁Werk",
+ -11.72974967956543
+ ],
+ [
+ "▁combin",
+ -11.729803085327148
+ ],
+ [
+ "▁releases",
+ -11.7298583984375
+ ],
+ [
+ "LL",
+ -11.730006217956543
+ ],
+ [
+ "ktor",
+ -11.730186462402344
+ ],
+ [
+ "ufgrund",
+ -11.73018741607666
+ ],
+ [
+ "calc",
+ -11.73034381866455
+ ],
+ [
+ "▁accomplished",
+ -11.730606079101562
+ ],
+ [
+ "▁los",
+ -11.730619430541992
+ ],
+ [
+ "▁distant",
+ -11.730688095092773
+ ],
+ [
+ "▁secteur",
+ -11.73068904876709
+ ],
+ [
+ "logue",
+ -11.730781555175781
+ ],
+ [
+ "▁betting",
+ -11.730792999267578
+ ],
+ [
+ "elf",
+ -11.731180191040039
+ ],
+ [
+ "puteti",
+ -11.73123550415039
+ ],
+ [
+ "▁Moment",
+ -11.731236457824707
+ ],
+ [
+ "▁scoring",
+ -11.731548309326172
+ ],
+ [
+ "▁freuen",
+ -11.731572151184082
+ ],
+ [
+ "▁fastest",
+ -11.731873512268066
+ ],
+ [
+ "▁directors",
+ -11.732080459594727
+ ],
+ [
+ "▁fame",
+ -11.732234954833984
+ ],
+ [
+ "▁complaint",
+ -11.732239723205566
+ ],
+ [
+ "▁Ep",
+ -11.732314109802246
+ ],
+ [
+ "▁delicate",
+ -11.732329368591309
+ ],
+ [
+ "annonce",
+ -11.73240852355957
+ ],
+ [
+ "ext",
+ -11.732454299926758
+ ],
+ [
+ "▁quit",
+ -11.732473373413086
+ ],
+ [
+ "▁Cop",
+ -11.73253345489502
+ ],
+ [
+ "prop",
+ -11.732565879821777
+ ],
+ [
+ "365",
+ -11.732742309570312
+ ],
+ [
+ "▁Say",
+ -11.732879638671875
+ ],
+ [
+ "▁internationale",
+ -11.733064651489258
+ ],
+ [
+ "cott",
+ -11.733213424682617
+ ],
+ [
+ "▁Whatever",
+ -11.733261108398438
+ ],
+ [
+ "▁admir",
+ -11.733261108398438
+ ],
+ [
+ "▁bucur",
+ -11.733549118041992
+ ],
+ [
+ "▁entity",
+ -11.733779907226562
+ ],
+ [
+ "▁dancing",
+ -11.733837127685547
+ ],
+ [
+ "▁printre",
+ -11.733892440795898
+ ],
+ [
+ "▁meditation",
+ -11.734396934509277
+ ],
+ [
+ "▁avis",
+ -11.734416961669922
+ ],
+ [
+ "▁1988",
+ -11.73447036743164
+ ],
+ [
+ "10.",
+ -11.734506607055664
+ ],
+ [
+ "▁worker",
+ -11.734638214111328
+ ],
+ [
+ "▁$100",
+ -11.734784126281738
+ ],
+ [
+ "▁contrôle",
+ -11.7349853515625
+ ],
+ [
+ "▁insist",
+ -11.734997749328613
+ ],
+ [
+ "ements",
+ -11.73505973815918
+ ],
+ [
+ "izate",
+ -11.735163688659668
+ ],
+ [
+ "▁tied",
+ -11.735332489013672
+ ],
+ [
+ "▁correspond",
+ -11.735396385192871
+ ],
+ [
+ "▁apartments",
+ -11.735547065734863
+ ],
+ [
+ "▁2009.",
+ -11.735599517822266
+ ],
+ [
+ "▁tiles",
+ -11.735624313354492
+ ],
+ [
+ "▁boots",
+ -11.735639572143555
+ ],
+ [
+ "▁laundry",
+ -11.735673904418945
+ ],
+ [
+ "▁Coffee",
+ -11.735674858093262
+ ],
+ [
+ "▁CV",
+ -11.735727310180664
+ ],
+ [
+ "▁composed",
+ -11.736035346984863
+ ],
+ [
+ "atom",
+ -11.73622989654541
+ ],
+ [
+ "▁shore",
+ -11.736270904541016
+ ],
+ [
+ "▁marijuana",
+ -11.736312866210938
+ ],
+ [
+ "plic",
+ -11.73648452758789
+ ],
+ [
+ "▁Zahl",
+ -11.736649513244629
+ ],
+ [
+ "depth",
+ -11.73682689666748
+ ],
+ [
+ "▁Egypt",
+ -11.736854553222656
+ ],
+ [
+ "▁NFL",
+ -11.736906051635742
+ ],
+ [
+ "▁12,",
+ -11.736922264099121
+ ],
+ [
+ "▁pollution",
+ -11.736964225769043
+ ],
+ [
+ "▁Vergleich",
+ -11.73704719543457
+ ],
+ [
+ "û",
+ -11.737109184265137
+ ],
+ [
+ "▁nurse",
+ -11.737153053283691
+ ],
+ [
+ "▁Susan",
+ -11.737173080444336
+ ],
+ [
+ "▁verify",
+ -11.737393379211426
+ ],
+ [
+ "▁kon",
+ -11.737504959106445
+ ],
+ [
+ "▁ulei",
+ -11.7376127243042
+ ],
+ [
+ "▁Sept",
+ -11.737699508666992
+ ],
+ [
+ "▁Location",
+ -11.737908363342285
+ ],
+ [
+ "▁frozen",
+ -11.737991333007812
+ ],
+ [
+ "good",
+ -11.73802661895752
+ ],
+ [
+ "▁cine",
+ -11.738066673278809
+ ],
+ [
+ "forming",
+ -11.738181114196777
+ ],
+ [
+ "▁Near",
+ -11.738391876220703
+ ],
+ [
+ "▁Tab",
+ -11.738545417785645
+ ],
+ [
+ "▁Alexandr",
+ -11.738600730895996
+ ],
+ [
+ "ст",
+ -11.73863697052002
+ ],
+ [
+ "CK",
+ -11.738656044006348
+ ],
+ [
+ "▁loads",
+ -11.738948822021484
+ ],
+ [
+ "▁disorders",
+ -11.738957405090332
+ ],
+ [
+ "hip",
+ -11.739596366882324
+ ],
+ [
+ "▁blessing",
+ -11.73987102508545
+ ],
+ [
+ "▁vechi",
+ -11.73997688293457
+ ],
+ [
+ "▁Bookmark",
+ -11.740296363830566
+ ],
+ [
+ "SON",
+ -11.74036979675293
+ ],
+ [
+ "books",
+ -11.740428924560547
+ ],
+ [
+ "▁tropical",
+ -11.740438461303711
+ ],
+ [
+ "▁Garten",
+ -11.740447044372559
+ ],
+ [
+ "ôt",
+ -11.740760803222656
+ ],
+ [
+ "tures",
+ -11.740827560424805
+ ],
+ [
+ "▁obligation",
+ -11.741010665893555
+ ],
+ [
+ "▁admin",
+ -11.741011619567871
+ ],
+ [
+ "▁sélection",
+ -11.741106986999512
+ ],
+ [
+ "disp",
+ -11.741172790527344
+ ],
+ [
+ "▁Anyone",
+ -11.741225242614746
+ ],
+ [
+ "keeper",
+ -11.74138355255127
+ ],
+ [
+ "▁konnten",
+ -11.741521835327148
+ ],
+ [
+ "▁existe",
+ -11.741615295410156
+ ],
+ [
+ "▁Rund",
+ -11.741798400878906
+ ],
+ [
+ "▁retailers",
+ -11.74184799194336
+ ],
+ [
+ "folg",
+ -11.741948127746582
+ ],
+ [
+ "▁urmare",
+ -11.742019653320312
+ ],
+ [
+ "▁Liebe",
+ -11.742321014404297
+ ],
+ [
+ "▁actors",
+ -11.742422103881836
+ ],
+ [
+ "▁Druck",
+ -11.742618560791016
+ ],
+ [
+ "lien",
+ -11.742752075195312
+ ],
+ [
+ "sian",
+ -11.742847442626953
+ ],
+ [
+ "▁partid",
+ -11.74304485321045
+ ],
+ [
+ "▁loin",
+ -11.743114471435547
+ ],
+ [
+ "AZ",
+ -11.743119239807129
+ ],
+ [
+ "oasă",
+ -11.743501663208008
+ ],
+ [
+ "▁inclusiv",
+ -11.743656158447266
+ ],
+ [
+ "TD",
+ -11.743680953979492
+ ],
+ [
+ "▁anului",
+ -11.743766784667969
+ ],
+ [
+ "poc",
+ -11.743844985961914
+ ],
+ [
+ "▁musique",
+ -11.743972778320312
+ ],
+ [
+ "▁Hart",
+ -11.743997573852539
+ ],
+ [
+ "Sh",
+ -11.744283676147461
+ ],
+ [
+ "html",
+ -11.744290351867676
+ ],
+ [
+ "▁serial",
+ -11.744318008422852
+ ],
+ [
+ "țele",
+ -11.744369506835938
+ ],
+ [
+ "inning",
+ -11.744544982910156
+ ],
+ [
+ "▁Bureau",
+ -11.744555473327637
+ ],
+ [
+ "▁rush",
+ -11.744626998901367
+ ],
+ [
+ "▁deosebit",
+ -11.744637489318848
+ ],
+ [
+ "▁Wort",
+ -11.744648933410645
+ ],
+ [
+ "▁Thailand",
+ -11.744688987731934
+ ],
+ [
+ "▁Language",
+ -11.745193481445312
+ ],
+ [
+ "▁Governor",
+ -11.745213508605957
+ ],
+ [
+ "▁Later",
+ -11.74525260925293
+ ],
+ [
+ "rilor",
+ -11.745282173156738
+ ],
+ [
+ "▁activités",
+ -11.745372772216797
+ ],
+ [
+ "schaffen",
+ -11.745598793029785
+ ],
+ [
+ "▁harvest",
+ -11.74567985534668
+ ],
+ [
+ "▁municipal",
+ -11.745783805847168
+ ],
+ [
+ "einander",
+ -11.74600601196289
+ ],
+ [
+ "▁fingers",
+ -11.746383666992188
+ ],
+ [
+ "▁sculpture",
+ -11.74638843536377
+ ],
+ [
+ "▁Bien",
+ -11.746390342712402
+ ],
+ [
+ "▁departments",
+ -11.746562957763672
+ ],
+ [
+ "▁période",
+ -11.746746063232422
+ ],
+ [
+ "▁jeune",
+ -11.746960639953613
+ ],
+ [
+ "▁governments",
+ -11.74710750579834
+ ],
+ [
+ "uter",
+ -11.747179985046387
+ ],
+ [
+ "Aceste",
+ -11.747220039367676
+ ],
+ [
+ "▁Deal",
+ -11.747243881225586
+ ],
+ [
+ "▁Equipment",
+ -11.74726390838623
+ ],
+ [
+ "nous",
+ -11.747300148010254
+ ],
+ [
+ "▁gate",
+ -11.747315406799316
+ ],
+ [
+ "▁meta",
+ -11.747447967529297
+ ],
+ [
+ "▁stiu",
+ -11.747474670410156
+ ],
+ [
+ "fold",
+ -11.747486114501953
+ ],
+ [
+ "▁seule",
+ -11.747523307800293
+ ],
+ [
+ "▁varied",
+ -11.747541427612305
+ ],
+ [
+ "hit",
+ -11.747635841369629
+ ],
+ [
+ "▁DIY",
+ -11.74768352508545
+ ],
+ [
+ "▁lemn",
+ -11.747685432434082
+ ],
+ [
+ "OB",
+ -11.747865676879883
+ ],
+ [
+ "▁colorful",
+ -11.748095512390137
+ ],
+ [
+ "▁câ",
+ -11.74826431274414
+ ],
+ [
+ "▁semester",
+ -11.74830150604248
+ ],
+ [
+ "▁dealer",
+ -11.748575210571289
+ ],
+ [
+ "nett",
+ -11.748788833618164
+ ],
+ [
+ "▁shortly",
+ -11.748932838439941
+ ],
+ [
+ "▁Driver",
+ -11.748983383178711
+ ],
+ [
+ "culture",
+ -11.749052047729492
+ ],
+ [
+ "▁permitted",
+ -11.749072074890137
+ ],
+ [
+ "▁sorts",
+ -11.749432563781738
+ ],
+ [
+ "▁crop",
+ -11.74999713897705
+ ],
+ [
+ "▁valoare",
+ -11.75046157836914
+ ],
+ [
+ "▁analog",
+ -11.750576972961426
+ ],
+ [
+ "▁excuse",
+ -11.750588417053223
+ ],
+ [
+ "▁modèle",
+ -11.750657081604004
+ ],
+ [
+ "When",
+ -11.75068473815918
+ ],
+ [
+ "▁march",
+ -11.750744819641113
+ ],
+ [
+ "haz",
+ -11.750978469848633
+ ],
+ [
+ "▁minimize",
+ -11.750992774963379
+ ],
+ [
+ "traction",
+ -11.751028060913086
+ ],
+ [
+ "▁caracter",
+ -11.752382278442383
+ ],
+ [
+ "▁modules",
+ -11.7523832321167
+ ],
+ [
+ "clu",
+ -11.75244426727295
+ ],
+ [
+ "ţional",
+ -11.752482414245605
+ ],
+ [
+ "▁breach",
+ -11.752562522888184
+ ],
+ [
+ "▁priced",
+ -11.752614974975586
+ ],
+ [
+ "▁attorneys",
+ -11.752644538879395
+ ],
+ [
+ "▁implant",
+ -11.752645492553711
+ ],
+ [
+ "▁ANY",
+ -11.752655029296875
+ ],
+ [
+ "dition",
+ -11.752707481384277
+ ],
+ [
+ "▁trials",
+ -11.752838134765625
+ ],
+ [
+ "▁Nas",
+ -11.75293254852295
+ ],
+ [
+ "Pre",
+ -11.752970695495605
+ ],
+ [
+ "lorsque",
+ -11.752979278564453
+ ],
+ [
+ "plin",
+ -11.753050804138184
+ ],
+ [
+ "Er",
+ -11.753056526184082
+ ],
+ [
+ "▁Dom",
+ -11.753067970275879
+ ],
+ [
+ "▁tire",
+ -11.753190040588379
+ ],
+ [
+ "sili",
+ -11.753233909606934
+ ],
+ [
+ "▁coins",
+ -11.753350257873535
+ ],
+ [
+ "▁rend",
+ -11.753470420837402
+ ],
+ [
+ "▁reliability",
+ -11.753503799438477
+ ],
+ [
+ "▁Analysis",
+ -11.753508567810059
+ ],
+ [
+ "▁trails",
+ -11.753692626953125
+ ],
+ [
+ "trägt",
+ -11.753762245178223
+ ],
+ [
+ "▁Kansas",
+ -11.753908157348633
+ ],
+ [
+ "▁responsive",
+ -11.75390911102295
+ ],
+ [
+ "▁disappear",
+ -11.753988265991211
+ ],
+ [
+ "▁stakeholders",
+ -11.754022598266602
+ ],
+ [
+ "▁aplica",
+ -11.754164695739746
+ ],
+ [
+ "▁imi",
+ -11.754180908203125
+ ],
+ [
+ "▁Laura",
+ -11.754369735717773
+ ],
+ [
+ "▁Terms",
+ -11.75440788269043
+ ],
+ [
+ "450",
+ -11.754460334777832
+ ],
+ [
+ "▁voltage",
+ -11.754483222961426
+ ],
+ [
+ "▁Gel",
+ -11.754544258117676
+ ],
+ [
+ "▁qualities",
+ -11.754549026489258
+ ],
+ [
+ "▁qualifi",
+ -11.754603385925293
+ ],
+ [
+ "▁Mé",
+ -11.754735946655273
+ ],
+ [
+ "bereit",
+ -11.754829406738281
+ ],
+ [
+ "gleich",
+ -11.754875183105469
+ ],
+ [
+ "▁voting",
+ -11.754961013793945
+ ],
+ [
+ "▁trademark",
+ -11.755128860473633
+ ],
+ [
+ "▁2.5",
+ -11.75515079498291
+ ],
+ [
+ "ND",
+ -11.755438804626465
+ ],
+ [
+ "▁Kelly",
+ -11.755470275878906
+ ],
+ [
+ "▁weiteren",
+ -11.755559921264648
+ ],
+ [
+ "▁filters",
+ -11.75562572479248
+ ],
+ [
+ "▁coût",
+ -11.75562858581543
+ ],
+ [
+ "jur",
+ -11.755765914916992
+ ],
+ [
+ "acre",
+ -11.755804061889648
+ ],
+ [
+ "▁retired",
+ -11.756022453308105
+ ],
+ [
+ "▁Engine",
+ -11.756205558776855
+ ],
+ [
+ "▁président",
+ -11.756264686584473
+ ],
+ [
+ "ajul",
+ -11.756307601928711
+ ],
+ [
+ "▁GA",
+ -11.756425857543945
+ ],
+ [
+ "rät",
+ -11.75666332244873
+ ],
+ [
+ "▁instructor",
+ -11.756669998168945
+ ],
+ [
+ "▁Allen",
+ -11.75668716430664
+ ],
+ [
+ "▁Delhi",
+ -11.756771087646484
+ ],
+ [
+ "▁cure",
+ -11.756844520568848
+ ],
+ [
+ "seite",
+ -11.756898880004883
+ ],
+ [
+ "coming",
+ -11.756914138793945
+ ],
+ [
+ "▁mixing",
+ -11.756963729858398
+ ],
+ [
+ "▁Kno",
+ -11.757041931152344
+ ],
+ [
+ "▁Sure",
+ -11.757079124450684
+ ],
+ [
+ "▁hired",
+ -11.757102012634277
+ ],
+ [
+ "▁participated",
+ -11.757196426391602
+ ],
+ [
+ "Count",
+ -11.757320404052734
+ ],
+ [
+ "treffen",
+ -11.757355690002441
+ ],
+ [
+ "▁54",
+ -11.75735855102539
+ ],
+ [
+ "▁rings",
+ -11.75735855102539
+ ],
+ [
+ "▁Thor",
+ -11.757359504699707
+ ],
+ [
+ "éro",
+ -11.75744915008545
+ ],
+ [
+ "▁buttons",
+ -11.757488250732422
+ ],
+ [
+ "▁47",
+ -11.757539749145508
+ ],
+ [
+ "▁Tel",
+ -11.757694244384766
+ ],
+ [
+ "▁suport",
+ -11.757776260375977
+ ],
+ [
+ "▁rhythm",
+ -11.75782585144043
+ ],
+ [
+ "▁Theater",
+ -11.758113861083984
+ ],
+ [
+ "▁informatii",
+ -11.758121490478516
+ ],
+ [
+ "hält",
+ -11.758201599121094
+ ],
+ [
+ "▁ouvert",
+ -11.758238792419434
+ ],
+ [
+ "fewer",
+ -11.75828742980957
+ ],
+ [
+ "▁alumni",
+ -11.758466720581055
+ ],
+ [
+ "▁valley",
+ -11.758508682250977
+ ],
+ [
+ "tial",
+ -11.75860595703125
+ ],
+ [
+ "***",
+ -11.758782386779785
+ ],
+ [
+ "kri",
+ -11.75905704498291
+ ],
+ [
+ "▁accidents",
+ -11.759113311767578
+ ],
+ [
+ "▁barrel",
+ -11.759170532226562
+ ],
+ [
+ "mobil",
+ -11.759310722351074
+ ],
+ [
+ "etti",
+ -11.759437561035156
+ ],
+ [
+ "▁immigration",
+ -11.759515762329102
+ ],
+ [
+ "▁poveste",
+ -11.759528160095215
+ ],
+ [
+ "hren",
+ -11.759669303894043
+ ],
+ [
+ "hydr",
+ -11.759719848632812
+ ],
+ [
+ "▁tweet",
+ -11.759744644165039
+ ],
+ [
+ "▁zip",
+ -11.759872436523438
+ ],
+ [
+ "▁Bonus",
+ -11.760189056396484
+ ],
+ [
+ "ordnung",
+ -11.760287284851074
+ ],
+ [
+ "liber",
+ -11.76046085357666
+ ],
+ [
+ "▁Navy",
+ -11.760591506958008
+ ],
+ [
+ "▁agreements",
+ -11.760612487792969
+ ],
+ [
+ "▁detection",
+ -11.7607421875
+ ],
+ [
+ "DF",
+ -11.760762214660645
+ ],
+ [
+ "hur",
+ -11.760774612426758
+ ],
+ [
+ "0.00",
+ -11.760798454284668
+ ],
+ [
+ "▁07",
+ -11.760866165161133
+ ],
+ [
+ "etta",
+ -11.760884284973145
+ ],
+ [
+ "▁13,",
+ -11.760887145996094
+ ],
+ [
+ "rolled",
+ -11.760970115661621
+ ],
+ [
+ "▁injection",
+ -11.761002540588379
+ ],
+ [
+ "mig",
+ -11.761017799377441
+ ],
+ [
+ "wach",
+ -11.761107444763184
+ ],
+ [
+ "▁choisir",
+ -11.761515617370605
+ ],
+ [
+ "▁professionnels",
+ -11.76159954071045
+ ],
+ [
+ "▁Tower",
+ -11.76169490814209
+ ],
+ [
+ "▁neighbor",
+ -11.76170539855957
+ ],
+ [
+ "deutschen",
+ -11.76187801361084
+ ],
+ [
+ "▁luxurious",
+ -11.76201057434082
+ ],
+ [
+ "▁walks",
+ -11.762033462524414
+ ],
+ [
+ "reti",
+ -11.762046813964844
+ ],
+ [
+ "▁Pad",
+ -11.762085914611816
+ ],
+ [
+ "wise",
+ -11.762297630310059
+ ],
+ [
+ "▁exhaust",
+ -11.762307167053223
+ ],
+ [
+ "▁demonstration",
+ -11.762582778930664
+ ],
+ [
+ "▁agricultural",
+ -11.762667655944824
+ ],
+ [
+ "Upon",
+ -11.762885093688965
+ ],
+ [
+ "▁Blu",
+ -11.76292610168457
+ ],
+ [
+ "atorul",
+ -11.762967109680176
+ ],
+ [
+ "amour",
+ -11.762984275817871
+ ],
+ [
+ "issant",
+ -11.763004302978516
+ ],
+ [
+ "▁delighted",
+ -11.763031959533691
+ ],
+ [
+ "rita",
+ -11.763113021850586
+ ],
+ [
+ "requiring",
+ -11.763195037841797
+ ],
+ [
+ "ivity",
+ -11.763216972351074
+ ],
+ [
+ "▁Unser",
+ -11.763306617736816
+ ],
+ [
+ "FP",
+ -11.763379096984863
+ ],
+ [
+ "fait",
+ -11.763533592224121
+ ],
+ [
+ "dite",
+ -11.763562202453613
+ ],
+ [
+ "kul",
+ -11.763716697692871
+ ],
+ [
+ "arth",
+ -11.76376724243164
+ ],
+ [
+ "▁Ker",
+ -11.763815879821777
+ ],
+ [
+ "torilor",
+ -11.763816833496094
+ ],
+ [
+ "stage",
+ -11.763866424560547
+ ],
+ [
+ "▁HTML",
+ -11.76398754119873
+ ],
+ [
+ "▁Wheel",
+ -11.764005661010742
+ ],
+ [
+ "▁quelque",
+ -11.76414680480957
+ ],
+ [
+ "▁Ou",
+ -11.764196395874023
+ ],
+ [
+ "▁considerable",
+ -11.764277458190918
+ ],
+ [
+ "▁Sco",
+ -11.76458740234375
+ ],
+ [
+ "▁donations",
+ -11.76481819152832
+ ],
+ [
+ "dessen",
+ -11.765002250671387
+ ],
+ [
+ "▁pourquoi",
+ -11.765039443969727
+ ],
+ [
+ "▁Bow",
+ -11.765189170837402
+ ],
+ [
+ "▁Dupa",
+ -11.76522445678711
+ ],
+ [
+ "ska",
+ -11.765707015991211
+ ],
+ [
+ "hot",
+ -11.765732765197754
+ ],
+ [
+ "▁drove",
+ -11.765849113464355
+ ],
+ [
+ "▁oppos",
+ -11.766018867492676
+ ],
+ [
+ "▁hiking",
+ -11.766035079956055
+ ],
+ [
+ "▁Boot",
+ -11.766081809997559
+ ],
+ [
+ "One",
+ -11.766087532043457
+ ],
+ [
+ "▁guvern",
+ -11.766094207763672
+ ],
+ [
+ "▁15,",
+ -11.766400337219238
+ ],
+ [
+ "scheid",
+ -11.766437530517578
+ ],
+ [
+ "▁Miet",
+ -11.766458511352539
+ ],
+ [
+ "▁Technical",
+ -11.766767501831055
+ ],
+ [
+ "▁Dal",
+ -11.7669038772583
+ ],
+ [
+ "▁Metro",
+ -11.766966819763184
+ ],
+ [
+ "▁Baker",
+ -11.767215728759766
+ ],
+ [
+ "▁trece",
+ -11.767252922058105
+ ],
+ [
+ "tained",
+ -11.767302513122559
+ ],
+ [
+ "block",
+ -11.76738452911377
+ ],
+ [
+ "▁wander",
+ -11.767401695251465
+ ],
+ [
+ "▁penalty",
+ -11.76742172241211
+ ],
+ [
+ "▁shipped",
+ -11.767509460449219
+ ],
+ [
+ "▁30%",
+ -11.767518043518066
+ ],
+ [
+ "group",
+ -11.767541885375977
+ ],
+ [
+ "▁brothers",
+ -11.767701148986816
+ ],
+ [
+ "▁comanda",
+ -11.767777442932129
+ ],
+ [
+ "▁retreat",
+ -11.767789840698242
+ ],
+ [
+ "▁Movie",
+ -11.767802238464355
+ ],
+ [
+ "PU",
+ -11.76787281036377
+ ],
+ [
+ "▁Jun",
+ -11.767885208129883
+ ],
+ [
+ "▁$6",
+ -11.767969131469727
+ ],
+ [
+ "▁Fal",
+ -11.768054962158203
+ ],
+ [
+ "▁Palestinian",
+ -11.768075942993164
+ ],
+ [
+ "▁soccer",
+ -11.768217086791992
+ ],
+ [
+ "▁Autor",
+ -11.768254280090332
+ ],
+ [
+ "▁chamber",
+ -11.768266677856445
+ ],
+ [
+ "nement",
+ -11.768463134765625
+ ],
+ [
+ "▁offense",
+ -11.768610954284668
+ ],
+ [
+ "▁gig",
+ -11.768631935119629
+ ],
+ [
+ "▁abandon",
+ -11.768691062927246
+ ],
+ [
+ "▁Kraft",
+ -11.768783569335938
+ ],
+ [
+ "▁Medicare",
+ -11.768784523010254
+ ],
+ [
+ "▁soap",
+ -11.768835067749023
+ ],
+ [
+ "▁Fur",
+ -11.768990516662598
+ ],
+ [
+ "▁conditioning",
+ -11.769103050231934
+ ],
+ [
+ "rained",
+ -11.769132614135742
+ ],
+ [
+ "▁puts",
+ -11.769134521484375
+ ],
+ [
+ "▁cod",
+ -11.76930046081543
+ ],
+ [
+ "lassen",
+ -11.76941967010498
+ ],
+ [
+ "FL",
+ -11.769600868225098
+ ],
+ [
+ "▁komplett",
+ -11.769664764404297
+ ],
+ [
+ "▁entscheiden",
+ -11.769665718078613
+ ],
+ [
+ "▁Hour",
+ -11.769691467285156
+ ],
+ [
+ "?!",
+ -11.770040512084961
+ ],
+ [
+ "Stream",
+ -11.770145416259766
+ ],
+ [
+ "▁Grad",
+ -11.770209312438965
+ ],
+ [
+ "▁gently",
+ -11.770231246948242
+ ],
+ [
+ "▁poetry",
+ -11.770429611206055
+ ],
+ [
+ "▁secured",
+ -11.770438194274902
+ ],
+ [
+ "oph",
+ -11.770466804504395
+ ],
+ [
+ "hop",
+ -11.770561218261719
+ ],
+ [
+ "handel",
+ -11.770634651184082
+ ],
+ [
+ "▁besoins",
+ -11.770658493041992
+ ],
+ [
+ "got",
+ -11.770824432373047
+ ],
+ [
+ "▁Chrome",
+ -11.77088737487793
+ ],
+ [
+ "ILL",
+ -11.770930290222168
+ ],
+ [
+ "▁Schritt",
+ -11.771014213562012
+ ],
+ [
+ "▁spell",
+ -11.771063804626465
+ ],
+ [
+ "▁grinding",
+ -11.771334648132324
+ ],
+ [
+ "▁ramp",
+ -11.77144718170166
+ ],
+ [
+ "▁mama",
+ -11.7716064453125
+ ],
+ [
+ "▁bottles",
+ -11.77180290222168
+ ],
+ [
+ "▁canvas",
+ -11.771906852722168
+ ],
+ [
+ "▁ecosystem",
+ -11.77194595336914
+ ],
+ [
+ "aţii",
+ -11.771967887878418
+ ],
+ [
+ "cellular",
+ -11.772085189819336
+ ],
+ [
+ "▁Spin",
+ -11.772164344787598
+ ],
+ [
+ "▁Discover",
+ -11.772217750549316
+ ],
+ [
+ "-17",
+ -11.772322654724121
+ ],
+ [
+ "▁feeding",
+ -11.77246379852295
+ ],
+ [
+ "▁stops",
+ -11.7725191116333
+ ],
+ [
+ "▁haute",
+ -11.772552490234375
+ ],
+ [
+ "▁Entscheidung",
+ -11.7725830078125
+ ],
+ [
+ "▁semble",
+ -11.772590637207031
+ ],
+ [
+ "▁acele",
+ -11.772857666015625
+ ],
+ [
+ "▁Walk",
+ -11.773154258728027
+ ],
+ [
+ "▁joke",
+ -11.773180961608887
+ ],
+ [
+ "▁Fed",
+ -11.773294448852539
+ ],
+ [
+ "climat",
+ -11.773306846618652
+ ],
+ [
+ "▁Lot",
+ -11.773460388183594
+ ],
+ [
+ "runner",
+ -11.773551940917969
+ ],
+ [
+ "▁flip",
+ -11.773786544799805
+ ],
+ [
+ "▁werde",
+ -11.773818016052246
+ ],
+ [
+ "▁Deck",
+ -11.77417278289795
+ ],
+ [
+ "bala",
+ -11.774296760559082
+ ],
+ [
+ "▁sacrifice",
+ -11.774375915527344
+ ],
+ [
+ "cid",
+ -11.774388313293457
+ ],
+ [
+ "him",
+ -11.774569511413574
+ ],
+ [
+ "zahlen",
+ -11.774587631225586
+ ],
+ [
+ "▁heater",
+ -11.774596214294434
+ ],
+ [
+ "formed",
+ -11.774619102478027
+ ],
+ [
+ "plus",
+ -11.774711608886719
+ ],
+ [
+ "▁util",
+ -11.774742126464844
+ ],
+ [
+ "rama",
+ -11.775019645690918
+ ],
+ [
+ "(4)",
+ -11.7750244140625
+ ],
+ [
+ "▁knife",
+ -11.775111198425293
+ ],
+ [
+ "▁traditions",
+ -11.77520751953125
+ ],
+ [
+ "▁dip",
+ -11.775357246398926
+ ],
+ [
+ "kill",
+ -11.775405883789062
+ ],
+ [
+ "▁Rich",
+ -11.775418281555176
+ ],
+ [
+ "▁DI",
+ -11.775555610656738
+ ],
+ [
+ "▁containers",
+ -11.775677680969238
+ ],
+ [
+ "▁locuri",
+ -11.775728225708008
+ ],
+ [
+ "▁continent",
+ -11.775797843933105
+ ],
+ [
+ "teilung",
+ -11.776005744934082
+ ],
+ [
+ "▁vreme",
+ -11.776028633117676
+ ],
+ [
+ "organisation",
+ -11.776126861572266
+ ],
+ [
+ "serie",
+ -11.776135444641113
+ ],
+ [
+ "▁Diamond",
+ -11.776204109191895
+ ],
+ [
+ "magazin",
+ -11.77627944946289
+ ],
+ [
+ "▁poster",
+ -11.776455879211426
+ ],
+ [
+ "▁passenger",
+ -11.7765474319458
+ ],
+ [
+ "▁soldiers",
+ -11.776552200317383
+ ],
+ [
+ "▁urgent",
+ -11.776616096496582
+ ],
+ [
+ "▁Lip",
+ -11.77680778503418
+ ],
+ [
+ "▁aşa",
+ -11.776972770690918
+ ],
+ [
+ "▁BO",
+ -11.777024269104004
+ ],
+ [
+ "▁somebody",
+ -11.777076721191406
+ ],
+ [
+ "▁silence",
+ -11.777132034301758
+ ],
+ [
+ "cop",
+ -11.777359962463379
+ ],
+ [
+ "▁Burn",
+ -11.77749252319336
+ ],
+ [
+ "▁stopping",
+ -11.777544021606445
+ ],
+ [
+ "▁essence",
+ -11.777568817138672
+ ],
+ [
+ "▁hitting",
+ -11.777762413024902
+ ],
+ [
+ "▁producers",
+ -11.777801513671875
+ ],
+ [
+ "▁fibre",
+ -11.777894020080566
+ ],
+ [
+ "▁seasonal",
+ -11.777960777282715
+ ],
+ [
+ "▁tara",
+ -11.778096199035645
+ ],
+ [
+ "▁Jose",
+ -11.778099060058594
+ ],
+ [
+ "▁Better",
+ -11.77825927734375
+ ],
+ [
+ "▁steep",
+ -11.778295516967773
+ ],
+ [
+ "Alors",
+ -11.778353691101074
+ ],
+ [
+ "▁collecting",
+ -11.778507232666016
+ ],
+ [
+ "vre",
+ -11.778635025024414
+ ],
+ [
+ "▁disabled",
+ -11.77863883972168
+ ],
+ [
+ "▁voters",
+ -11.778679847717285
+ ],
+ [
+ "consuming",
+ -11.779092788696289
+ ],
+ [
+ "deemed",
+ -11.779115676879883
+ ],
+ [
+ "éra",
+ -11.779227256774902
+ ],
+ [
+ "opération",
+ -11.779273986816406
+ ],
+ [
+ "▁roller",
+ -11.779305458068848
+ ],
+ [
+ "Rather",
+ -11.779321670532227
+ ],
+ [
+ "▁leider",
+ -11.779370307922363
+ ],
+ [
+ "▁IV",
+ -11.779434204101562
+ ],
+ [
+ "▁erreichen",
+ -11.779473304748535
+ ],
+ [
+ "▁charging",
+ -11.779657363891602
+ ],
+ [
+ "tions",
+ -11.77973747253418
+ ],
+ [
+ "tiques",
+ -11.779861450195312
+ ],
+ [
+ "▁formats",
+ -11.779876708984375
+ ],
+ [
+ "▁painful",
+ -11.78000545501709
+ ],
+ [
+ "▁eager",
+ -11.780061721801758
+ ],
+ [
+ "generation",
+ -11.780137062072754
+ ],
+ [
+ "anna",
+ -11.780235290527344
+ ],
+ [
+ "▁races",
+ -11.780323028564453
+ ],
+ [
+ "force",
+ -11.780357360839844
+ ],
+ [
+ "▁ferm",
+ -11.780522346496582
+ ],
+ [
+ "▁breathing",
+ -11.780618667602539
+ ],
+ [
+ "▁offen",
+ -11.780648231506348
+ ],
+ [
+ "▁minds",
+ -11.780805587768555
+ ],
+ [
+ "▁musste",
+ -11.780832290649414
+ ],
+ [
+ "▁Vision",
+ -11.780888557434082
+ ],
+ [
+ "▁Installation",
+ -11.780988693237305
+ ],
+ [
+ "▁hesitate",
+ -11.781002044677734
+ ],
+ [
+ "▁somit",
+ -11.781023979187012
+ ],
+ [
+ "hôtel",
+ -11.781044006347656
+ ],
+ [
+ "cab",
+ -11.781235694885254
+ ],
+ [
+ "-16",
+ -11.781312942504883
+ ],
+ [
+ "▁Visual",
+ -11.781418800354004
+ ],
+ [
+ "intérêt",
+ -11.781524658203125
+ ],
+ [
+ "▁apel",
+ -11.781831741333008
+ ],
+ [
+ "therapy",
+ -11.782089233398438
+ ],
+ [
+ "volt",
+ -11.78225040435791
+ ],
+ [
+ "▁Rou",
+ -11.782439231872559
+ ],
+ [
+ "▁efficace",
+ -11.782464027404785
+ ],
+ [
+ "▁architectural",
+ -11.782605171203613
+ ],
+ [
+ "▁privilege",
+ -11.782670974731445
+ ],
+ [
+ "▁treating",
+ -11.782711029052734
+ ],
+ [
+ "▁Tam",
+ -11.782722473144531
+ ],
+ [
+ "tsch",
+ -11.782744407653809
+ ],
+ [
+ "building",
+ -11.782750129699707
+ ],
+ [
+ "▁associations",
+ -11.782929420471191
+ ],
+ [
+ "▁Consumer",
+ -11.783424377441406
+ ],
+ [
+ "▁Lim",
+ -11.783496856689453
+ ],
+ [
+ "newest",
+ -11.7835054397583
+ ],
+ [
+ "▁față",
+ -11.783675193786621
+ ],
+ [
+ "▁ships",
+ -11.783732414245605
+ ],
+ [
+ "lev",
+ -11.78373908996582
+ ],
+ [
+ "raft",
+ -11.783817291259766
+ ],
+ [
+ "▁variations",
+ -11.783845901489258
+ ],
+ [
+ "▁noua",
+ -11.78386402130127
+ ],
+ [
+ "▁Cab",
+ -11.784063339233398
+ ],
+ [
+ "1.2",
+ -11.78409481048584
+ ],
+ [
+ "▁ocazi",
+ -11.784347534179688
+ ],
+ [
+ "▁recommendation",
+ -11.784449577331543
+ ],
+ [
+ "titled",
+ -11.78445053100586
+ ],
+ [
+ "▁invoice",
+ -11.78459644317627
+ ],
+ [
+ "▁noastra",
+ -11.784647941589355
+ ],
+ [
+ "kur",
+ -11.784700393676758
+ ],
+ [
+ "issent",
+ -11.784758567810059
+ ],
+ [
+ "base",
+ -11.784778594970703
+ ],
+ [
+ "hä",
+ -11.7848482131958
+ ],
+ [
+ "888",
+ -11.784914016723633
+ ],
+ [
+ "▁declar",
+ -11.784941673278809
+ ],
+ [
+ "▁Football",
+ -11.7850341796875
+ ],
+ [
+ "▁Indeed",
+ -11.785293579101562
+ ],
+ [
+ "▁weapon",
+ -11.785333633422852
+ ],
+ [
+ "▁destroyed",
+ -11.785457611083984
+ ],
+ [
+ "▁enormous",
+ -11.785594940185547
+ ],
+ [
+ "▁blanket",
+ -11.7857084274292
+ ],
+ [
+ "▁aktiv",
+ -11.785759925842285
+ ],
+ [
+ "raw",
+ -11.785791397094727
+ ],
+ [
+ "▁computing",
+ -11.785823822021484
+ ],
+ [
+ "6)",
+ -11.785955429077148
+ ],
+ [
+ "▁Dam",
+ -11.786152839660645
+ ],
+ [
+ "▁confort",
+ -11.786174774169922
+ ],
+ [
+ "▁Gla",
+ -11.786198616027832
+ ],
+ [
+ "hardly",
+ -11.786242485046387
+ ],
+ [
+ "▁annually",
+ -11.786269187927246
+ ],
+ [
+ "▁destinations",
+ -11.786401748657227
+ ],
+ [
+ "▁guilty",
+ -11.786404609680176
+ ],
+ [
+ "▁scholarship",
+ -11.786439895629883
+ ],
+ [
+ "▁harmful",
+ -11.786453247070312
+ ],
+ [
+ "▁2-3",
+ -11.786616325378418
+ ],
+ [
+ "▁Race",
+ -11.786638259887695
+ ],
+ [
+ "▁hypo",
+ -11.78671646118164
+ ],
+ [
+ "▁shorter",
+ -11.786733627319336
+ ],
+ [
+ "quest",
+ -11.78675651550293
+ ],
+ [
+ "uze",
+ -11.786812782287598
+ ],
+ [
+ "izi",
+ -11.787005424499512
+ ],
+ [
+ "OO",
+ -11.787095069885254
+ ],
+ [
+ "▁Schutz",
+ -11.787097930908203
+ ],
+ [
+ "▁Teilnehmer",
+ -11.787185668945312
+ ],
+ [
+ "▁profiles",
+ -11.787199020385742
+ ],
+ [
+ "▁sustainability",
+ -11.78747272491455
+ ],
+ [
+ "▁emb",
+ -11.787489891052246
+ ],
+ [
+ "▁Augen",
+ -11.787516593933105
+ ],
+ [
+ "▁outdoors",
+ -11.787542343139648
+ ],
+ [
+ "▁Individual",
+ -11.787548065185547
+ ],
+ [
+ "▁pou",
+ -11.78757095336914
+ ],
+ [
+ "▁Together",
+ -11.787575721740723
+ ],
+ [
+ "HT",
+ -11.787674903869629
+ ],
+ [
+ "suited",
+ -11.787755012512207
+ ],
+ [
+ "▁tro",
+ -11.787782669067383
+ ],
+ [
+ "▁Strom",
+ -11.787805557250977
+ ],
+ [
+ "▁achievement",
+ -11.78799819946289
+ ],
+ [
+ "▁Range",
+ -11.78815746307373
+ ],
+ [
+ "tory",
+ -11.78817081451416
+ ],
+ [
+ "▁distribute",
+ -11.788250923156738
+ ],
+ [
+ "▁letzte",
+ -11.788276672363281
+ ],
+ [
+ "incorporated",
+ -11.788287162780762
+ ],
+ [
+ "▁Kir",
+ -11.788325309753418
+ ],
+ [
+ "ruf",
+ -11.78839111328125
+ ],
+ [
+ "▁disappointed",
+ -11.788543701171875
+ ],
+ [
+ "▁referral",
+ -11.788602828979492
+ ],
+ [
+ "flam",
+ -11.788687705993652
+ ],
+ [
+ "▁excessive",
+ -11.7886962890625
+ ],
+ [
+ "▁rapidement",
+ -11.788743019104004
+ ],
+ [
+ "▁Rio",
+ -11.78875732421875
+ ],
+ [
+ "aţia",
+ -11.788951873779297
+ ],
+ [
+ "▁meuble",
+ -11.78912353515625
+ ],
+ [
+ "▁2008.",
+ -11.789135932922363
+ ],
+ [
+ "▁Gall",
+ -11.78915023803711
+ ],
+ [
+ "▁française",
+ -11.789369583129883
+ ],
+ [
+ "▁ladies",
+ -11.789695739746094
+ ],
+ [
+ "ailed",
+ -11.789746284484863
+ ],
+ [
+ "El",
+ -11.789834976196289
+ ],
+ [
+ "▁wines",
+ -11.789868354797363
+ ],
+ [
+ "▁beispielsweise",
+ -11.789876937866211
+ ],
+ [
+ "▁gamme",
+ -11.790193557739258
+ ],
+ [
+ "▁guided",
+ -11.79028034210205
+ ],
+ [
+ "▁plin",
+ -11.790339469909668
+ ],
+ [
+ "Î",
+ -11.790390968322754
+ ],
+ [
+ "▁True",
+ -11.790498733520508
+ ],
+ [
+ "▁Temple",
+ -11.790507316589355
+ ],
+ [
+ "▁Pic",
+ -11.790520668029785
+ ],
+ [
+ "permalink",
+ -11.790547370910645
+ ],
+ [
+ "▁vedea",
+ -11.790656089782715
+ ],
+ [
+ "▁rank",
+ -11.790922164916992
+ ],
+ [
+ "▁Grill",
+ -11.791025161743164
+ ],
+ [
+ "clin",
+ -11.791070938110352
+ ],
+ [
+ "▁Hab",
+ -11.791089057922363
+ ],
+ [
+ "▁odds",
+ -11.791125297546387
+ ],
+ [
+ "▁anytime",
+ -11.791146278381348
+ ],
+ [
+ "▁Thanksgiving",
+ -11.791265487670898
+ ],
+ [
+ "guard",
+ -11.791300773620605
+ ],
+ [
+ "▁essays",
+ -11.791389465332031
+ ],
+ [
+ "▁PE",
+ -11.79139518737793
+ ],
+ [
+ "▁Rechts",
+ -11.791494369506836
+ ],
+ [
+ "mals",
+ -11.791751861572266
+ ],
+ [
+ "achi",
+ -11.791762351989746
+ ],
+ [
+ "▁Anthony",
+ -11.791765213012695
+ ],
+ [
+ "▁réponse",
+ -11.792036056518555
+ ],
+ [
+ "standing",
+ -11.79227352142334
+ ],
+ [
+ "▁Mol",
+ -11.792427062988281
+ ],
+ [
+ "▁Canon",
+ -11.792474746704102
+ ],
+ [
+ "▁silk",
+ -11.792515754699707
+ ],
+ [
+ "▁pourrait",
+ -11.79278564453125
+ ],
+ [
+ "▁raport",
+ -11.79280948638916
+ ],
+ [
+ "▁Woche",
+ -11.792889595031738
+ ],
+ [
+ "fallen",
+ -11.79293155670166
+ ],
+ [
+ "sting",
+ -11.79310131072998
+ ],
+ [
+ "▁circulation",
+ -11.793102264404297
+ ],
+ [
+ "▁skirt",
+ -11.7931547164917
+ ],
+ [
+ "▁Title",
+ -11.793187141418457
+ ],
+ [
+ "▁17.",
+ -11.79331111907959
+ ],
+ [
+ "▁Touch",
+ -11.793486595153809
+ ],
+ [
+ "▁utilizat",
+ -11.79352855682373
+ ],
+ [
+ "▁Organisation",
+ -11.793569564819336
+ ],
+ [
+ "▁mereu",
+ -11.793848991394043
+ ],
+ [
+ "▁oxygen",
+ -11.793953895568848
+ ],
+ [
+ "lique",
+ -11.793985366821289
+ ],
+ [
+ "▁consume",
+ -11.794100761413574
+ ],
+ [
+ "▁Barb",
+ -11.794102668762207
+ ],
+ [
+ "1.1",
+ -11.794105529785156
+ ],
+ [
+ "▁nicely",
+ -11.79419231414795
+ ],
+ [
+ "▁psychological",
+ -11.794227600097656
+ ],
+ [
+ "▁refrigerator",
+ -11.794478416442871
+ ],
+ [
+ "▁fantasy",
+ -11.79481029510498
+ ],
+ [
+ "▁dispute",
+ -11.79494571685791
+ ],
+ [
+ "▁IBM",
+ -11.794954299926758
+ ],
+ [
+ "▁Nation",
+ -11.794971466064453
+ ],
+ [
+ "▁mobil",
+ -11.795063972473145
+ ],
+ [
+ "▁density",
+ -11.795201301574707
+ ],
+ [
+ "ske",
+ -11.795230865478516
+ ],
+ [
+ "▁intimate",
+ -11.795313835144043
+ ],
+ [
+ "▁tailored",
+ -11.795319557189941
+ ],
+ [
+ "▁outline",
+ -11.795472145080566
+ ],
+ [
+ "TN",
+ -11.79554557800293
+ ],
+ [
+ "mur",
+ -11.795634269714355
+ ],
+ [
+ "GC",
+ -11.795662879943848
+ ],
+ [
+ "they",
+ -11.795992851257324
+ ],
+ [
+ "pag",
+ -11.796161651611328
+ ],
+ [
+ "▁Kultur",
+ -11.796246528625488
+ ],
+ [
+ "grün",
+ -11.796281814575195
+ ],
+ [
+ "voted",
+ -11.796529769897461
+ ],
+ [
+ "▁donné",
+ -11.796546936035156
+ ],
+ [
+ "▁Să",
+ -11.796629905700684
+ ],
+ [
+ "enberg",
+ -11.796648979187012
+ ],
+ [
+ "▁wi",
+ -11.79686450958252
+ ],
+ [
+ "▁Francis",
+ -11.797057151794434
+ ],
+ [
+ "▁Rick",
+ -11.797157287597656
+ ],
+ [
+ "accord",
+ -11.797403335571289
+ ],
+ [
+ "▁Zusammen",
+ -11.797415733337402
+ ],
+ [
+ "▁nonprofit",
+ -11.797456741333008
+ ],
+ [
+ "▁listings",
+ -11.797615051269531
+ ],
+ [
+ "6,",
+ -11.797908782958984
+ ],
+ [
+ "▁maximize",
+ -11.798253059387207
+ ],
+ [
+ "bud",
+ -11.798345565795898
+ ],
+ [
+ "▁promotional",
+ -11.798486709594727
+ ],
+ [
+ "cina",
+ -11.798646926879883
+ ],
+ [
+ "▁potatoes",
+ -11.79869556427002
+ ],
+ [
+ "▁mot",
+ -11.798871040344238
+ ],
+ [
+ "carries",
+ -11.799384117126465
+ ],
+ [
+ "▁stabilit",
+ -11.799458503723145
+ ],
+ [
+ "▁Door",
+ -11.799574851989746
+ ],
+ [
+ "▁downloaded",
+ -11.799574851989746
+ ],
+ [
+ "▁experimental",
+ -11.799724578857422
+ ],
+ [
+ "HD",
+ -11.7997407913208
+ ],
+ [
+ "▁parfois",
+ -11.79980182647705
+ ],
+ [
+ "▁zeigen",
+ -11.800092697143555
+ ],
+ [
+ "▁proposé",
+ -11.80030632019043
+ ],
+ [
+ "▁Verein",
+ -11.800636291503906
+ ],
+ [
+ "▁amestec",
+ -11.800676345825195
+ ],
+ [
+ "▁entreprise",
+ -11.800718307495117
+ ],
+ [
+ "▁PSD",
+ -11.800841331481934
+ ],
+ [
+ "▁bake",
+ -11.800897598266602
+ ],
+ [
+ "▁Rh",
+ -11.800904273986816
+ ],
+ [
+ "▁Mehr",
+ -11.800922393798828
+ ],
+ [
+ "▁purple",
+ -11.801074028015137
+ ],
+ [
+ "▁recipient",
+ -11.80109691619873
+ ],
+ [
+ "rare",
+ -11.801166534423828
+ ],
+ [
+ "egi",
+ -11.80117130279541
+ ],
+ [
+ "ancien",
+ -11.801176071166992
+ ],
+ [
+ "▁risque",
+ -11.80118465423584
+ ],
+ [
+ "▁mystery",
+ -11.80157470703125
+ ],
+ [
+ "mac",
+ -11.801697731018066
+ ],
+ [
+ "ibility",
+ -11.80182933807373
+ ],
+ [
+ "▁Moore",
+ -11.801881790161133
+ ],
+ [
+ "▁flavors",
+ -11.801911354064941
+ ],
+ [
+ "▁trauma",
+ -11.801966667175293
+ ],
+ [
+ "▁automotive",
+ -11.802112579345703
+ ],
+ [
+ "▁Anyway",
+ -11.802197456359863
+ ],
+ [
+ "▁simulation",
+ -11.802253723144531
+ ],
+ [
+ "▁crafts",
+ -11.802525520324707
+ ],
+ [
+ "▁measurements",
+ -11.80257511138916
+ ],
+ [
+ "▁cour",
+ -11.80257797241211
+ ],
+ [
+ "▁tard",
+ -11.802600860595703
+ ],
+ [
+ "nnie",
+ -11.802881240844727
+ ],
+ [
+ "▁Production",
+ -11.803388595581055
+ ],
+ [
+ "▁Cleaning",
+ -11.803567886352539
+ ],
+ [
+ "5,",
+ -11.803644180297852
+ ],
+ [
+ "▁Islamic",
+ -11.803766250610352
+ ],
+ [
+ "▁Gate",
+ -11.80378532409668
+ ],
+ [
+ "bay",
+ -11.803814888000488
+ ],
+ [
+ "HR",
+ -11.803990364074707
+ ],
+ [
+ "▁Offer",
+ -11.80399227142334
+ ],
+ [
+ "▁acceptance",
+ -11.804107666015625
+ ],
+ [
+ "▁Erfahrung",
+ -11.80412769317627
+ ],
+ [
+ "▁environ",
+ -11.804193496704102
+ ],
+ [
+ "▁fancy",
+ -11.804218292236328
+ ],
+ [
+ "▁bullet",
+ -11.80437183380127
+ ],
+ [
+ "organ",
+ -11.804466247558594
+ ],
+ [
+ "▁Peace",
+ -11.804520606994629
+ ],
+ [
+ "▁detalii",
+ -11.80461597442627
+ ],
+ [
+ "▁promised",
+ -11.804715156555176
+ ],
+ [
+ "▁wellness",
+ -11.804746627807617
+ ],
+ [
+ "▁satisfy",
+ -11.80481243133545
+ ],
+ [
+ "▁grants",
+ -11.805212020874023
+ ],
+ [
+ "accueil",
+ -11.80522346496582
+ ],
+ [
+ "▁oben",
+ -11.805412292480469
+ ],
+ [
+ "▁prospects",
+ -11.80543327331543
+ ],
+ [
+ "▁Events",
+ -11.805513381958008
+ ],
+ [
+ "2013",
+ -11.805569648742676
+ ],
+ [
+ "gesehen",
+ -11.805685997009277
+ ],
+ [
+ "▁£1",
+ -11.805727005004883
+ ],
+ [
+ "▁handelt",
+ -11.805798530578613
+ ],
+ [
+ "▁Spieler",
+ -11.805876731872559
+ ],
+ [
+ "▁Virtual",
+ -11.806145668029785
+ ],
+ [
+ "▁bubble",
+ -11.806239128112793
+ ],
+ [
+ "▁Trend",
+ -11.806254386901855
+ ],
+ [
+ "▁sistemul",
+ -11.806315422058105
+ ],
+ [
+ "▁Morgan",
+ -11.806320190429688
+ ],
+ [
+ "▁pole",
+ -11.806503295898438
+ ],
+ [
+ "▁spielen",
+ -11.806533813476562
+ ],
+ [
+ "tür",
+ -11.806571006774902
+ ],
+ [
+ "SCO",
+ -11.806572914123535
+ ],
+ [
+ "▁informative",
+ -11.806678771972656
+ ],
+ [
+ "▁affirm",
+ -11.806755065917969
+ ],
+ [
+ "▁Aqua",
+ -11.806818008422852
+ ],
+ [
+ "▁AR",
+ -11.806888580322266
+ ],
+ [
+ "richten",
+ -11.807071685791016
+ ],
+ [
+ "▁rewards",
+ -11.807122230529785
+ ],
+ [
+ "lub",
+ -11.807235717773438
+ ],
+ [
+ "shot",
+ -11.807236671447754
+ ],
+ [
+ "LM",
+ -11.807540893554688
+ ],
+ [
+ "Up",
+ -11.807586669921875
+ ],
+ [
+ "▁absolut",
+ -11.807737350463867
+ ],
+ [
+ "▁Mart",
+ -11.807806968688965
+ ],
+ [
+ "erweise",
+ -11.807812690734863
+ ],
+ [
+ "BP",
+ -11.807977676391602
+ ],
+ [
+ "▁difficile",
+ -11.808152198791504
+ ],
+ [
+ "▁Document",
+ -11.808159828186035
+ ],
+ [
+ "▁Sweet",
+ -11.8082914352417
+ ],
+ [
+ "▁indicator",
+ -11.808338165283203
+ ],
+ [
+ "▁Boden",
+ -11.808389663696289
+ ],
+ [
+ "mates",
+ -11.808477401733398
+ ],
+ [
+ "▁supporters",
+ -11.808504104614258
+ ],
+ [
+ "▁begun",
+ -11.808600425720215
+ ],
+ [
+ "▁blogging",
+ -11.808611869812012
+ ],
+ [
+ "▁CL",
+ -11.808663368225098
+ ],
+ [
+ "gres",
+ -11.808692932128906
+ ],
+ [
+ "▁preferences",
+ -11.808738708496094
+ ],
+ [
+ "▁screw",
+ -11.808756828308105
+ ],
+ [
+ "▁tutor",
+ -11.808858871459961
+ ],
+ [
+ "▁Additional",
+ -11.80891227722168
+ ],
+ [
+ "▁Bitte",
+ -11.808976173400879
+ ],
+ [
+ "utilizing",
+ -11.808998107910156
+ ],
+ [
+ "▁expérience",
+ -11.809073448181152
+ ],
+ [
+ "▁dur",
+ -11.809146881103516
+ ],
+ [
+ "▁precisely",
+ -11.809178352355957
+ ],
+ [
+ "▁janvier",
+ -11.809394836425781
+ ],
+ [
+ "AGE",
+ -11.80987548828125
+ ],
+ [
+ "moto",
+ -11.810007095336914
+ ],
+ [
+ "▁counsel",
+ -11.810195922851562
+ ],
+ [
+ "▁110",
+ -11.810226440429688
+ ],
+ [
+ "nick",
+ -11.810245513916016
+ ],
+ [
+ "licit",
+ -11.810540199279785
+ ],
+ [
+ "technik",
+ -11.810659408569336
+ ],
+ [
+ "▁collaborate",
+ -11.810736656188965
+ ],
+ [
+ "▁neighbors",
+ -11.810794830322266
+ ],
+ [
+ "tered",
+ -11.810922622680664
+ ],
+ [
+ "▁excel",
+ -11.811025619506836
+ ],
+ [
+ "▁Route",
+ -11.811059951782227
+ ],
+ [
+ "steuer",
+ -11.81109619140625
+ ],
+ [
+ "▁pioneer",
+ -11.811607360839844
+ ],
+ [
+ "nuit",
+ -11.81169319152832
+ ],
+ [
+ "▁skip",
+ -11.811963081359863
+ ],
+ [
+ "▁destruction",
+ -11.811997413635254
+ ],
+ [
+ "▁thesis",
+ -11.812249183654785
+ ],
+ [
+ "▁libre",
+ -11.812317848205566
+ ],
+ [
+ "▁petition",
+ -11.81234073638916
+ ],
+ [
+ "▁steady",
+ -11.812456130981445
+ ],
+ [
+ "▁medications",
+ -11.812458992004395
+ ],
+ [
+ "▁audiences",
+ -11.812623023986816
+ ],
+ [
+ "▁coaches",
+ -11.812689781188965
+ ],
+ [
+ "aller",
+ -11.812704086303711
+ ],
+ [
+ "3,000",
+ -11.812705993652344
+ ],
+ [
+ "▁anger",
+ -11.812785148620605
+ ],
+ [
+ "▁striking",
+ -11.812844276428223
+ ],
+ [
+ "▁shades",
+ -11.81291675567627
+ ],
+ [
+ "▁Sitz",
+ -11.812994956970215
+ ],
+ [
+ "▁gluten",
+ -11.813162803649902
+ ],
+ [
+ "▁egal",
+ -11.813222885131836
+ ],
+ [
+ "ania",
+ -11.813223838806152
+ ],
+ [
+ "▁defend",
+ -11.813241004943848
+ ],
+ [
+ "gut",
+ -11.81382942199707
+ ],
+ [
+ "▁reserves",
+ -11.813895225524902
+ ],
+ [
+ "▁advocate",
+ -11.814053535461426
+ ],
+ [
+ "▁Cit",
+ -11.814082145690918
+ ],
+ [
+ "▁technicians",
+ -11.814105033874512
+ ],
+ [
+ "▁cater",
+ -11.814138412475586
+ ],
+ [
+ "leitung",
+ -11.814190864562988
+ ],
+ [
+ "▁towns",
+ -11.814335823059082
+ ],
+ [
+ "▁Costa",
+ -11.814364433288574
+ ],
+ [
+ "▁confront",
+ -11.814567565917969
+ ],
+ [
+ "mount",
+ -11.814652442932129
+ ],
+ [
+ "▁nationale",
+ -11.814706802368164
+ ],
+ [
+ "▁adverse",
+ -11.814932823181152
+ ],
+ [
+ "▁couleur",
+ -11.815112113952637
+ ],
+ [
+ "▁delight",
+ -11.815169334411621
+ ],
+ [
+ "▁promises",
+ -11.815224647521973
+ ],
+ [
+ "▁silent",
+ -11.81550121307373
+ ],
+ [
+ "richtet",
+ -11.815556526184082
+ ],
+ [
+ "▁Companies",
+ -11.815614700317383
+ ],
+ [
+ "▁Charlotte",
+ -11.815620422363281
+ ],
+ [
+ "▁labels",
+ -11.815652847290039
+ ],
+ [
+ "▁Süd",
+ -11.815656661987305
+ ],
+ [
+ "▁Honor",
+ -11.81567096710205
+ ],
+ [
+ "▁complaints",
+ -11.815710067749023
+ ],
+ [
+ "▁siècle",
+ -11.815752029418945
+ ],
+ [
+ "▁suits",
+ -11.815792083740234
+ ],
+ [
+ "▁Bath",
+ -11.815827369689941
+ ],
+ [
+ "mise",
+ -11.815926551818848
+ ],
+ [
+ "▁acela",
+ -11.8159818649292
+ ],
+ [
+ "▁candidat",
+ -11.816011428833008
+ ],
+ [
+ "Flo",
+ -11.816207885742188
+ ],
+ [
+ "▁conservative",
+ -11.816215515136719
+ ],
+ [
+ "DD",
+ -11.816314697265625
+ ],
+ [
+ "▁changement",
+ -11.816414833068848
+ ],
+ [
+ "▁login",
+ -11.816492080688477
+ ],
+ [
+ "▁Fashion",
+ -11.816585540771484
+ ],
+ [
+ "reichen",
+ -11.816672325134277
+ ],
+ [
+ "through",
+ -11.816751480102539
+ ],
+ [
+ "aki",
+ -11.817240715026855
+ ],
+ [
+ "gna",
+ -11.817547798156738
+ ],
+ [
+ "▁verse",
+ -11.817551612854004
+ ],
+ [
+ "▁threats",
+ -11.817622184753418
+ ],
+ [
+ "▁Song",
+ -11.817770004272461
+ ],
+ [
+ "▁funded",
+ -11.81792163848877
+ ],
+ [
+ "langen",
+ -11.818023681640625
+ ],
+ [
+ "▁distribu",
+ -11.818195343017578
+ ],
+ [
+ "édition",
+ -11.818316459655762
+ ],
+ [
+ "▁royal",
+ -11.818562507629395
+ ],
+ [
+ "▁bevor",
+ -11.818829536437988
+ ],
+ [
+ "▁02",
+ -11.818854331970215
+ ],
+ [
+ "straße",
+ -11.818938255310059
+ ],
+ [
+ "edit",
+ -11.81904125213623
+ ],
+ [
+ "▁energetic",
+ -11.81922721862793
+ ],
+ [
+ "▁Carr",
+ -11.819757461547852
+ ],
+ [
+ "viol",
+ -11.819937705993652
+ ],
+ [
+ "▁niche",
+ -11.820054054260254
+ ],
+ [
+ "avais",
+ -11.820099830627441
+ ],
+ [
+ "▁backyard",
+ -11.82010269165039
+ ],
+ [
+ "▁Saudi",
+ -11.820158958435059
+ ],
+ [
+ "▁Zwei",
+ -11.820207595825195
+ ],
+ [
+ "▁Legal",
+ -11.82027530670166
+ ],
+ [
+ "accessed",
+ -11.820277214050293
+ ],
+ [
+ "▁choisi",
+ -11.820340156555176
+ ],
+ [
+ "▁GDP",
+ -11.820343971252441
+ ],
+ [
+ "oferă",
+ -11.820352554321289
+ ],
+ [
+ "hlen",
+ -11.820490837097168
+ ],
+ [
+ "▁Wor",
+ -11.820520401000977
+ ],
+ [
+ "▁cheer",
+ -11.820586204528809
+ ],
+ [
+ "▁barely",
+ -11.820625305175781
+ ],
+ [
+ "cost",
+ -11.820646286010742
+ ],
+ [
+ "▁Really",
+ -11.820661544799805
+ ],
+ [
+ "kol",
+ -11.820721626281738
+ ],
+ [
+ "▁binding",
+ -11.821045875549316
+ ],
+ [
+ "euer",
+ -11.821136474609375
+ ],
+ [
+ "▁optimization",
+ -11.821158409118652
+ ],
+ [
+ "▁Designer",
+ -11.8211669921875
+ ],
+ [
+ "▁measuring",
+ -11.82117748260498
+ ],
+ [
+ "ncy",
+ -11.821516036987305
+ ],
+ [
+ "weise",
+ -11.821520805358887
+ ],
+ [
+ "DER",
+ -11.821850776672363
+ ],
+ [
+ "▁$7",
+ -11.821949005126953
+ ],
+ [
+ "▁Anfang",
+ -11.821954727172852
+ ],
+ [
+ "material",
+ -11.821967124938965
+ ],
+ [
+ "▁antique",
+ -11.822281837463379
+ ],
+ [
+ "▁Certificate",
+ -11.822294235229492
+ ],
+ [
+ "▁modest",
+ -11.822370529174805
+ ],
+ [
+ "ției",
+ -11.822427749633789
+ ],
+ [
+ "▁praise",
+ -11.82245922088623
+ ],
+ [
+ "▁Springs",
+ -11.822660446166992
+ ],
+ [
+ "▁organiza",
+ -11.823041915893555
+ ],
+ [
+ "jurul",
+ -11.823047637939453
+ ],
+ [
+ "▁plumbing",
+ -11.82341194152832
+ ],
+ [
+ "▁foster",
+ -11.823490142822266
+ ],
+ [
+ "▁Wy",
+ -11.823491096496582
+ ],
+ [
+ "▁Sab",
+ -11.823503494262695
+ ],
+ [
+ "▁overwhelming",
+ -11.823677062988281
+ ],
+ [
+ "▁matin",
+ -11.823812484741211
+ ],
+ [
+ "▁responded",
+ -11.82408332824707
+ ],
+ [
+ "▁confused",
+ -11.824150085449219
+ ],
+ [
+ "▁blessed",
+ -11.824280738830566
+ ],
+ [
+ "▁160",
+ -11.824295997619629
+ ],
+ [
+ "▁ingredient",
+ -11.824360847473145
+ ],
+ [
+ "▁confer",
+ -11.82448673248291
+ ],
+ [
+ "▁Gesundheit",
+ -11.824530601501465
+ ],
+ [
+ "▁bucket",
+ -11.824555397033691
+ ],
+ [
+ "kraft",
+ -11.824565887451172
+ ],
+ [
+ "lange",
+ -11.824630737304688
+ ],
+ [
+ "▁Kopf",
+ -11.824678421020508
+ ],
+ [
+ "▁Prize",
+ -11.824678421020508
+ ],
+ [
+ "▁authorized",
+ -11.824779510498047
+ ],
+ [
+ "▁tick",
+ -11.824803352355957
+ ],
+ [
+ "▁steal",
+ -11.824910163879395
+ ],
+ [
+ "Depending",
+ -11.824918746948242
+ ],
+ [
+ "Depuis",
+ -11.824952125549316
+ ],
+ [
+ "▁functie",
+ -11.82499885559082
+ ],
+ [
+ "▁developments",
+ -11.825053215026855
+ ],
+ [
+ "▁Christians",
+ -11.825311660766602
+ ],
+ [
+ "▁calculated",
+ -11.8256254196167
+ ],
+ [
+ "▁Leave",
+ -11.825672149658203
+ ],
+ [
+ "▁Jam",
+ -11.82573413848877
+ ],
+ [
+ "▁habitat",
+ -11.825760841369629
+ ],
+ [
+ "▁Sorry",
+ -11.825801849365234
+ ],
+ [
+ "▁oficial",
+ -11.825944900512695
+ ],
+ [
+ "▁allein",
+ -11.826079368591309
+ ],
+ [
+ "▁concentrate",
+ -11.82608413696289
+ ],
+ [
+ "dica",
+ -11.826302528381348
+ ],
+ [
+ "▁Convention",
+ -11.826476097106934
+ ],
+ [
+ "illes",
+ -11.826550483703613
+ ],
+ [
+ "▁fum",
+ -11.82664680480957
+ ],
+ [
+ "▁Tal",
+ -11.826651573181152
+ ],
+ [
+ "Europe",
+ -11.826899528503418
+ ],
+ [
+ "▁attachment",
+ -11.826949119567871
+ ],
+ [
+ "▁sensibil",
+ -11.826995849609375
+ ],
+ [
+ "▁clue",
+ -11.82715892791748
+ ],
+ [
+ "▁specialty",
+ -11.827203750610352
+ ],
+ [
+ "▁Cou",
+ -11.827229499816895
+ ],
+ [
+ "▁liste",
+ -11.827278137207031
+ ],
+ [
+ "▁Penn",
+ -11.827465057373047
+ ],
+ [
+ "TRA",
+ -11.827559471130371
+ ],
+ [
+ "▁Themen",
+ -11.827561378479004
+ ],
+ [
+ "▁motivated",
+ -11.827906608581543
+ ],
+ [
+ "▁camere",
+ -11.828017234802246
+ ],
+ [
+ "▁14,",
+ -11.828393936157227
+ ],
+ [
+ "▁attendance",
+ -11.828557968139648
+ ],
+ [
+ "atorii",
+ -11.828581809997559
+ ],
+ [
+ "chemistry",
+ -11.82873821258545
+ ],
+ [
+ "▁roofing",
+ -11.828959465026855
+ ],
+ [
+ "▁Links",
+ -11.829048156738281
+ ],
+ [
+ "▁trou",
+ -11.829103469848633
+ ],
+ [
+ "▁trucks",
+ -11.829136848449707
+ ],
+ [
+ "hilfe",
+ -11.829557418823242
+ ],
+ [
+ "▁(6",
+ -11.829599380493164
+ ],
+ [
+ "vapor",
+ -11.82964038848877
+ ],
+ [
+ "mad",
+ -11.829668045043945
+ ],
+ [
+ "▁Albert",
+ -11.829877853393555
+ ],
+ [
+ "▁FIG",
+ -11.830073356628418
+ ],
+ [
+ "▁Rand",
+ -11.830187797546387
+ ],
+ [
+ "▁Constitution",
+ -11.830219268798828
+ ],
+ [
+ "ambi",
+ -11.830294609069824
+ ],
+ [
+ "▁Syria",
+ -11.830307006835938
+ ],
+ [
+ "▁Fond",
+ -11.830477714538574
+ ],
+ [
+ "▁gouvernement",
+ -11.830594062805176
+ ],
+ [
+ "▁Active",
+ -11.830705642700195
+ ],
+ [
+ "▁prints",
+ -11.830801963806152
+ ],
+ [
+ "▁weigh",
+ -11.8308687210083
+ ],
+ [
+ "▁Craft",
+ -11.831069946289062
+ ],
+ [
+ "▁projets",
+ -11.831247329711914
+ ],
+ [
+ "▁paste",
+ -11.831377029418945
+ ],
+ [
+ "anci",
+ -11.83139705657959
+ ],
+ [
+ "kie",
+ -11.831411361694336
+ ],
+ [
+ "▁gains",
+ -11.83165168762207
+ ],
+ [
+ "▁Record",
+ -11.831942558288574
+ ],
+ [
+ "▁beliefs",
+ -11.831954956054688
+ ],
+ [
+ "countless",
+ -11.831957817077637
+ ],
+ [
+ "▁tomatoes",
+ -11.831997871398926
+ ],
+ [
+ "arie",
+ -11.832082748413086
+ ],
+ [
+ "▁140",
+ -11.83211612701416
+ ],
+ [
+ "▁ethical",
+ -11.832229614257812
+ ],
+ [
+ "objectif",
+ -11.832279205322266
+ ],
+ [
+ "▁acestuia",
+ -11.832283973693848
+ ],
+ [
+ "▁Bluetooth",
+ -11.832398414611816
+ ],
+ [
+ "▁agriculture",
+ -11.832746505737305
+ ],
+ [
+ "uré",
+ -11.833027839660645
+ ],
+ [
+ "▁cale",
+ -11.833072662353516
+ ],
+ [
+ "▁articol",
+ -11.833073616027832
+ ],
+ [
+ "▁gum",
+ -11.833319664001465
+ ],
+ [
+ "▁vendor",
+ -11.833490371704102
+ ],
+ [
+ "ifié",
+ -11.833527565002441
+ ],
+ [
+ "▁peer",
+ -11.833662033081055
+ ],
+ [
+ "pod",
+ -11.834036827087402
+ ],
+ [
+ "▁utilized",
+ -11.834113121032715
+ ],
+ [
+ "▁Mü",
+ -11.834207534790039
+ ],
+ [
+ "owohl",
+ -11.834208488464355
+ ],
+ [
+ "hilst",
+ -11.834233283996582
+ ],
+ [
+ "frame",
+ -11.834260940551758
+ ],
+ [
+ "▁fridge",
+ -11.834822654724121
+ ],
+ [
+ "▁query",
+ -11.835108757019043
+ ],
+ [
+ "▁Survey",
+ -11.835227012634277
+ ],
+ [
+ "▁Hell",
+ -11.835247993469238
+ ],
+ [
+ "▁notification",
+ -11.83530044555664
+ ],
+ [
+ "TR",
+ -11.83538818359375
+ ],
+ [
+ "▁ultima",
+ -11.835505485534668
+ ],
+ [
+ "▁radiation",
+ -11.835631370544434
+ ],
+ [
+ "▁musicians",
+ -11.835821151733398
+ ],
+ [
+ "CAN",
+ -11.83595085144043
+ ],
+ [
+ "▁grocery",
+ -11.83607292175293
+ ],
+ [
+ "▁Sicherheit",
+ -11.83611011505127
+ ],
+ [
+ "▁Highway",
+ -11.836276054382324
+ ],
+ [
+ "▁Break",
+ -11.836285591125488
+ ],
+ [
+ "TED",
+ -11.836345672607422
+ ],
+ [
+ "ön",
+ -11.836352348327637
+ ],
+ [
+ "▁biological",
+ -11.836352348327637
+ ],
+ [
+ "qual",
+ -11.836397171020508
+ ],
+ [
+ "250",
+ -11.83641242980957
+ ],
+ [
+ "▁modify",
+ -11.836651802062988
+ ],
+ [
+ "▁Hit",
+ -11.836698532104492
+ ],
+ [
+ "▁Iar",
+ -11.836838722229004
+ ],
+ [
+ "aged",
+ -11.836884498596191
+ ],
+ [
+ "...)",
+ -11.83688735961914
+ ],
+ [
+ "▁contrat",
+ -11.836928367614746
+ ],
+ [
+ "▁centres",
+ -11.836956977844238
+ ],
+ [
+ "griff",
+ -11.836987495422363
+ ],
+ [
+ "Our",
+ -11.837233543395996
+ ],
+ [
+ "▁determination",
+ -11.837300300598145
+ ],
+ [
+ "▁variables",
+ -11.83742904663086
+ ],
+ [
+ "▁nuts",
+ -11.837472915649414
+ ],
+ [
+ "échange",
+ -11.837577819824219
+ ],
+ [
+ "extérieur",
+ -11.837631225585938
+ ],
+ [
+ "▁suflet",
+ -11.83764362335205
+ ],
+ [
+ "▁Scha",
+ -11.837752342224121
+ ],
+ [
+ "stück",
+ -11.837774276733398
+ ],
+ [
+ "▁Tau",
+ -11.837821960449219
+ ],
+ [
+ "▁participa",
+ -11.838008880615234
+ ],
+ [
+ "▁mad",
+ -11.838034629821777
+ ],
+ [
+ "▁relie",
+ -11.838051795959473
+ ],
+ [
+ "▁Fine",
+ -11.83808422088623
+ ],
+ [
+ "▁grape",
+ -11.838118553161621
+ ],
+ [
+ "▁wage",
+ -11.838141441345215
+ ],
+ [
+ "▁startup",
+ -11.838193893432617
+ ],
+ [
+ "▁blank",
+ -11.838194847106934
+ ],
+ [
+ "▁physique",
+ -11.838199615478516
+ ],
+ [
+ "▁punch",
+ -11.838233947753906
+ ],
+ [
+ "▁contacts",
+ -11.838321685791016
+ ],
+ [
+ "▁dezvolt",
+ -11.83835220336914
+ ],
+ [
+ "cross",
+ -11.838639259338379
+ ],
+ [
+ "▁TR",
+ -11.838652610778809
+ ],
+ [
+ "▁gener",
+ -11.838754653930664
+ ],
+ [
+ "▁indem",
+ -11.838823318481445
+ ],
+ [
+ "▁Stan",
+ -11.838839530944824
+ ],
+ [
+ "▁azi",
+ -11.838930130004883
+ ],
+ [
+ "▁Sel",
+ -11.838958740234375
+ ],
+ [
+ "▁Tot",
+ -11.83924674987793
+ ],
+ [
+ "vra",
+ -11.839341163635254
+ ],
+ [
+ "▁recruit",
+ -11.839482307434082
+ ],
+ [
+ "▁Yeah",
+ -11.839494705200195
+ ],
+ [
+ "/10",
+ -11.839507102966309
+ ],
+ [
+ "▁nail",
+ -11.83956241607666
+ ],
+ [
+ "▁Ky",
+ -11.839611053466797
+ ],
+ [
+ "▁beloved",
+ -11.839760780334473
+ ],
+ [
+ "operative",
+ -11.839823722839355
+ ],
+ [
+ "▁Tickets",
+ -11.83983325958252
+ ],
+ [
+ "▁tear",
+ -11.840229988098145
+ ],
+ [
+ "▁amp",
+ -11.840352058410645
+ ],
+ [
+ "▁04",
+ -11.840361595153809
+ ],
+ [
+ "▁illustrate",
+ -11.840361595153809
+ ],
+ [
+ "▁mac",
+ -11.840400695800781
+ ],
+ [
+ "▁receiver",
+ -11.840482711791992
+ ],
+ [
+ "atrice",
+ -11.840508460998535
+ ],
+ [
+ "▁souhait",
+ -11.840572357177734
+ ],
+ [
+ "▁Gewinn",
+ -11.840619087219238
+ ],
+ [
+ "▁Vit",
+ -11.840808868408203
+ ],
+ [
+ "roch",
+ -11.841202735900879
+ ],
+ [
+ "▁arata",
+ -11.841262817382812
+ ],
+ [
+ "▁Indiana",
+ -11.841364860534668
+ ],
+ [
+ "child",
+ -11.841516494750977
+ ],
+ [
+ "▁invested",
+ -11.84157657623291
+ ],
+ [
+ "▁Excellent",
+ -11.841625213623047
+ ],
+ [
+ "gori",
+ -11.841769218444824
+ ],
+ [
+ "▁thermal",
+ -11.841813087463379
+ ],
+ [
+ "Str",
+ -11.841973304748535
+ ],
+ [
+ "▁liver",
+ -11.84201717376709
+ ],
+ [
+ "miss",
+ -11.842035293579102
+ ],
+ [
+ "▁utiliser",
+ -11.842120170593262
+ ],
+ [
+ "▁prest",
+ -11.842445373535156
+ ],
+ [
+ "2016",
+ -11.842506408691406
+ ],
+ [
+ "isée",
+ -11.842508316040039
+ ],
+ [
+ "▁Index",
+ -11.842559814453125
+ ],
+ [
+ "▁arch",
+ -11.842639923095703
+ ],
+ [
+ "▁Toyota",
+ -11.842748641967773
+ ],
+ [
+ "▁YOUR",
+ -11.842782020568848
+ ],
+ [
+ "▁Mexican",
+ -11.842891693115234
+ ],
+ [
+ "▁gegenüber",
+ -11.842940330505371
+ ],
+ [
+ "▁cannabis",
+ -11.843033790588379
+ ],
+ [
+ "bis",
+ -11.843077659606934
+ ],
+ [
+ "vage",
+ -11.843083381652832
+ ],
+ [
+ "hall",
+ -11.843091011047363
+ ],
+ [
+ "fax",
+ -11.843137741088867
+ ],
+ [
+ "▁spoken",
+ -11.843232154846191
+ ],
+ [
+ "▁Zimmer",
+ -11.843544960021973
+ ],
+ [
+ "kauf",
+ -11.8436279296875
+ ],
+ [
+ "▁couleurs",
+ -11.843705177307129
+ ],
+ [
+ "▁NJ",
+ -11.844026565551758
+ ],
+ [
+ "▁Heritage",
+ -11.844318389892578
+ ],
+ [
+ "▁Pflege",
+ -11.844321250915527
+ ],
+ [
+ "luc",
+ -11.844361305236816
+ ],
+ [
+ "▁56",
+ -11.844489097595215
+ ],
+ [
+ "VP",
+ -11.844542503356934
+ ],
+ [
+ "▁cuvinte",
+ -11.844594955444336
+ ],
+ [
+ "▁Alliance",
+ -11.844614028930664
+ ],
+ [
+ "▁coco",
+ -11.844615936279297
+ ],
+ [
+ "▁leverage",
+ -11.844762802124023
+ ],
+ [
+ "auch",
+ -11.844844818115234
+ ],
+ [
+ "▁Cart",
+ -11.84506607055664
+ ],
+ [
+ "taux",
+ -11.84532642364502
+ ],
+ [
+ "east",
+ -11.84560775756836
+ ],
+ [
+ "▁decorating",
+ -11.84565258026123
+ ],
+ [
+ "tip",
+ -11.84565544128418
+ ],
+ [
+ "▁Communications",
+ -11.845780372619629
+ ],
+ [
+ "ACE",
+ -11.84580135345459
+ ],
+ [
+ "▁Consul",
+ -11.845993041992188
+ ],
+ [
+ "▁Swiss",
+ -11.846197128295898
+ ],
+ [
+ "inci",
+ -11.846230506896973
+ ],
+ [
+ "▁Fact",
+ -11.846312522888184
+ ],
+ [
+ "▁ajung",
+ -11.846321105957031
+ ],
+ [
+ "▁airline",
+ -11.846325874328613
+ ],
+ [
+ "▁kidney",
+ -11.846379280090332
+ ],
+ [
+ "▁Records",
+ -11.84642505645752
+ ],
+ [
+ "▁Olympic",
+ -11.846747398376465
+ ],
+ [
+ "▁dried",
+ -11.84719467163086
+ ],
+ [
+ "oivent",
+ -11.847333908081055
+ ],
+ [
+ "▁Adobe",
+ -11.847467422485352
+ ],
+ [
+ "▁powers",
+ -11.847748756408691
+ ],
+ [
+ "lande",
+ -11.847834587097168
+ ],
+ [
+ "▁relieve",
+ -11.847858428955078
+ ],
+ [
+ "ţine",
+ -11.847898483276367
+ ],
+ [
+ "▁gradually",
+ -11.847945213317871
+ ],
+ [
+ "mud",
+ -11.84811019897461
+ ],
+ [
+ "▁30,",
+ -11.848116874694824
+ ],
+ [
+ "▁plante",
+ -11.848133087158203
+ ],
+ [
+ "▁Hug",
+ -11.848225593566895
+ ],
+ [
+ "▁Focus",
+ -11.84853458404541
+ ],
+ [
+ "▁distinctive",
+ -11.848594665527344
+ ],
+ [
+ "▁Bab",
+ -11.848662376403809
+ ],
+ [
+ "tata",
+ -11.848679542541504
+ ],
+ [
+ "▁Nun",
+ -11.848797798156738
+ ],
+ [
+ "▁Eve",
+ -11.848811149597168
+ ],
+ [
+ "▁déc",
+ -11.848881721496582
+ ],
+ [
+ "▁Beitrag",
+ -11.84900951385498
+ ],
+ [
+ "▁devenit",
+ -11.849042892456055
+ ],
+ [
+ "driven",
+ -11.849250793457031
+ ],
+ [
+ "▁offerings",
+ -11.84933853149414
+ ],
+ [
+ "▁exc",
+ -11.84941577911377
+ ],
+ [
+ "encies",
+ -11.849576950073242
+ ],
+ [
+ "▁Neuro",
+ -11.849588394165039
+ ],
+ [
+ "scher",
+ -11.849604606628418
+ ],
+ [
+ "map",
+ -11.849703788757324
+ ],
+ [
+ "pending",
+ -11.849783897399902
+ ],
+ [
+ "▁courage",
+ -11.849799156188965
+ ],
+ [
+ "axe",
+ -11.849894523620605
+ ],
+ [
+ "▁Gesellschaft",
+ -11.849900245666504
+ ],
+ [
+ "▁ears",
+ -11.85000991821289
+ ],
+ [
+ "▁aider",
+ -11.850403785705566
+ ],
+ [
+ "▁Cast",
+ -11.85042667388916
+ ],
+ [
+ "fast",
+ -11.850442886352539
+ ],
+ [
+ "▁departe",
+ -11.850502014160156
+ ],
+ [
+ "▁oak",
+ -11.850507736206055
+ ],
+ [
+ "▁batch",
+ -11.850730895996094
+ ],
+ [
+ "▁Corporate",
+ -11.850762367248535
+ ],
+ [
+ "▁Ost",
+ -11.850895881652832
+ ],
+ [
+ "-14",
+ -11.850897789001465
+ ],
+ [
+ "▁Pie",
+ -11.85115909576416
+ ],
+ [
+ "▁ranking",
+ -11.851273536682129
+ ],
+ [
+ "clusion",
+ -11.851316452026367
+ ],
+ [
+ "▁costume",
+ -11.851347923278809
+ ],
+ [
+ "▁Knight",
+ -11.851449966430664
+ ],
+ [
+ "▁privat",
+ -11.851577758789062
+ ],
+ [
+ "▁Engineer",
+ -11.851593971252441
+ ],
+ [
+ "▁gens",
+ -11.8517427444458
+ ],
+ [
+ "physics",
+ -11.85176944732666
+ ],
+ [
+ "generating",
+ -11.851773262023926
+ ],
+ [
+ "directement",
+ -11.851786613464355
+ ],
+ [
+ "▁confidential",
+ -11.851810455322266
+ ],
+ [
+ "▁poet",
+ -11.851937294006348
+ ],
+ [
+ "▁monster",
+ -11.851944923400879
+ ],
+ [
+ "▁suppose",
+ -11.851984977722168
+ ],
+ [
+ "său",
+ -11.851996421813965
+ ],
+ [
+ "▁balls",
+ -11.852103233337402
+ ],
+ [
+ "▁substitute",
+ -11.852137565612793
+ ],
+ [
+ "▁simultaneously",
+ -11.852238655090332
+ ],
+ [
+ "▁specify",
+ -11.852272033691406
+ ],
+ [
+ "wald",
+ -11.852287292480469
+ ],
+ [
+ "▁collapse",
+ -11.852352142333984
+ ],
+ [
+ "dessus",
+ -11.852458953857422
+ ],
+ [
+ "▁vitr",
+ -11.852516174316406
+ ],
+ [
+ "▁recruitment",
+ -11.852607727050781
+ ],
+ [
+ "denken",
+ -11.852632522583008
+ ],
+ [
+ "▁candy",
+ -11.852691650390625
+ ],
+ [
+ "▁tourists",
+ -11.852721214294434
+ ],
+ [
+ "dimensional",
+ -11.852782249450684
+ ],
+ [
+ "conce",
+ -11.852814674377441
+ ],
+ [
+ "wechsel",
+ -11.852822303771973
+ ],
+ [
+ "▁passende",
+ -11.852971076965332
+ ],
+ [
+ "industrie",
+ -11.85299301147461
+ ],
+ [
+ "agne",
+ -11.853127479553223
+ ],
+ [
+ "▁warehouse",
+ -11.853233337402344
+ ],
+ [
+ "▁Jugend",
+ -11.853277206420898
+ ],
+ [
+ "▁Weise",
+ -11.853357315063477
+ ],
+ [
+ "▁Zone",
+ -11.853528022766113
+ ],
+ [
+ "▁licence",
+ -11.853550910949707
+ ],
+ [
+ "▁broker",
+ -11.853630065917969
+ ],
+ [
+ "▁Rolle",
+ -11.85365104675293
+ ],
+ [
+ "pton",
+ -11.853789329528809
+ ],
+ [
+ "▁preference",
+ -11.853846549987793
+ ],
+ [
+ "▁homeowners",
+ -11.853861808776855
+ ],
+ [
+ "▁Lum",
+ -11.85387134552002
+ ],
+ [
+ "▁Chairman",
+ -11.853879928588867
+ ],
+ [
+ "▁Pages",
+ -11.853998184204102
+ ],
+ [
+ "▁beam",
+ -11.854005813598633
+ ],
+ [
+ "▁coordinate",
+ -11.854158401489258
+ ],
+ [
+ "▁Tool",
+ -11.854212760925293
+ ],
+ [
+ "▁complexity",
+ -11.854272842407227
+ ],
+ [
+ "▁checks",
+ -11.854339599609375
+ ],
+ [
+ "▁Bedroom",
+ -11.854405403137207
+ ],
+ [
+ "minded",
+ -11.854538917541504
+ ],
+ [
+ "▁copiii",
+ -11.854694366455078
+ ],
+ [
+ "▁celebrating",
+ -11.85470199584961
+ ],
+ [
+ "zimmer",
+ -11.854759216308594
+ ],
+ [
+ "▁Imagine",
+ -11.854759216308594
+ ],
+ [
+ "▁decoration",
+ -11.854830741882324
+ ],
+ [
+ "team",
+ -11.855354309082031
+ ],
+ [
+ "▁împreună",
+ -11.855369567871094
+ ],
+ [
+ "▁publicly",
+ -11.855391502380371
+ ],
+ [
+ "▁centuries",
+ -11.855514526367188
+ ],
+ [
+ "▁Islands",
+ -11.855644226074219
+ ],
+ [
+ "▁ethnic",
+ -11.855663299560547
+ ],
+ [
+ "still",
+ -11.85576057434082
+ ],
+ [
+ "stieg",
+ -11.855823516845703
+ ],
+ [
+ "emia",
+ -11.855904579162598
+ ],
+ [
+ "tags",
+ -11.856026649475098
+ ],
+ [
+ "▁marche",
+ -11.856062889099121
+ ],
+ [
+ "▁migration",
+ -11.856096267700195
+ ],
+ [
+ "▁banner",
+ -11.85616683959961
+ ],
+ [
+ "▁macro",
+ -11.856378555297852
+ ],
+ [
+ "▁Edit",
+ -11.856379508972168
+ ],
+ [
+ "tran",
+ -11.85656452178955
+ ],
+ [
+ "ça",
+ -11.856597900390625
+ ],
+ [
+ "▁recycling",
+ -11.856670379638672
+ ],
+ [
+ "▁1,000",
+ -11.856673240661621
+ ],
+ [
+ "▁Quelle",
+ -11.856891632080078
+ ],
+ [
+ "▁Vel",
+ -11.85700511932373
+ ],
+ [
+ "▁Rit",
+ -11.857025146484375
+ ],
+ [
+ "▁Spaß",
+ -11.857046127319336
+ ],
+ [
+ "▁Corn",
+ -11.857074737548828
+ ],
+ [
+ "tracted",
+ -11.857177734375
+ ],
+ [
+ "cited",
+ -11.857185363769531
+ ],
+ [
+ "▁tablets",
+ -11.857202529907227
+ ],
+ [
+ "▁Display",
+ -11.857337951660156
+ ],
+ [
+ "▁persoana",
+ -11.857392311096191
+ ],
+ [
+ "Term",
+ -11.857410430908203
+ ],
+ [
+ "▁Vancouver",
+ -11.857537269592285
+ ],
+ [
+ "▁Gäste",
+ -11.857550621032715
+ ],
+ [
+ "determining",
+ -11.857608795166016
+ ],
+ [
+ "▁populations",
+ -11.85778522491455
+ ],
+ [
+ "aison",
+ -11.857873916625977
+ ],
+ [
+ "▁surgical",
+ -11.858072280883789
+ ],
+ [
+ "tale",
+ -11.858160018920898
+ ],
+ [
+ "ivi",
+ -11.858283042907715
+ ],
+ [
+ "▁Zur",
+ -11.858388900756836
+ ],
+ [
+ "esprit",
+ -11.858574867248535
+ ],
+ [
+ "▁Edge",
+ -11.858665466308594
+ ],
+ [
+ "dach",
+ -11.858760833740234
+ ],
+ [
+ "phi",
+ -11.858773231506348
+ ],
+ [
+ "▁suc",
+ -11.858841896057129
+ ],
+ [
+ "▁scrie",
+ -11.858848571777344
+ ],
+ [
+ "▁Ausbildung",
+ -11.858885765075684
+ ],
+ [
+ "▁51",
+ -11.85892391204834
+ ],
+ [
+ "ologi",
+ -11.858938217163086
+ ],
+ [
+ "▁correction",
+ -11.859049797058105
+ ],
+ [
+ "▁Wald",
+ -11.859078407287598
+ ],
+ [
+ "▁additionally",
+ -11.859131813049316
+ ],
+ [
+ "▁proche",
+ -11.859353065490723
+ ],
+ [
+ "▁classical",
+ -11.859477996826172
+ ],
+ [
+ "▁bringen",
+ -11.859490394592285
+ ],
+ [
+ "▁(10",
+ -11.859611511230469
+ ],
+ [
+ "▁Mile",
+ -11.859809875488281
+ ],
+ [
+ "lace",
+ -11.859885215759277
+ ],
+ [
+ "▁premi",
+ -11.85988712310791
+ ],
+ [
+ "▁constitute",
+ -11.860029220581055
+ ],
+ [
+ "▁bitter",
+ -11.860078811645508
+ ],
+ [
+ "▁Inform",
+ -11.860295295715332
+ ],
+ [
+ "▁corporations",
+ -11.860334396362305
+ ],
+ [
+ "▁Lisa",
+ -11.860494613647461
+ ],
+ [
+ "▁obligat",
+ -11.860685348510742
+ ],
+ [
+ "Throughout",
+ -11.860738754272461
+ ],
+ [
+ "▁Rs",
+ -11.860769271850586
+ ],
+ [
+ "▁Hair",
+ -11.860916137695312
+ ],
+ [
+ "▁supplements",
+ -11.86099624633789
+ ],
+ [
+ "▁motorcycle",
+ -11.861054420471191
+ ],
+ [
+ "escent",
+ -11.861132621765137
+ ],
+ [
+ "▁investi",
+ -11.861222267150879
+ ],
+ [
+ "▁continuously",
+ -11.861265182495117
+ ],
+ [
+ "▁Essen",
+ -11.861334800720215
+ ],
+ [
+ "▁precision",
+ -11.8613862991333
+ ],
+ [
+ "▁deficit",
+ -11.861461639404297
+ ],
+ [
+ "▁wallet",
+ -11.861481666564941
+ ],
+ [
+ "▁Bürger",
+ -11.861531257629395
+ ],
+ [
+ "chir",
+ -11.861574172973633
+ ],
+ [
+ "9)",
+ -11.86161994934082
+ ],
+ [
+ "▁Programme",
+ -11.861716270446777
+ ],
+ [
+ "▁simplement",
+ -11.86193561553955
+ ],
+ [
+ "MD",
+ -11.862093925476074
+ ],
+ [
+ "▁rouge",
+ -11.862096786499023
+ ],
+ [
+ "usion",
+ -11.862133979797363
+ ],
+ [
+ "▁stove",
+ -11.862208366394043
+ ],
+ [
+ "▁prospective",
+ -11.862224578857422
+ ],
+ [
+ "▁corp",
+ -11.86234188079834
+ ],
+ [
+ "▁impacts",
+ -11.862401008605957
+ ],
+ [
+ "▁bride",
+ -11.86266803741455
+ ],
+ [
+ "0.0",
+ -11.862788200378418
+ ],
+ [
+ "hid",
+ -11.862833976745605
+ ],
+ [
+ "▁warrant",
+ -11.862930297851562
+ ],
+ [
+ "▁Ice",
+ -11.8631010055542
+ ],
+ [
+ "▁sensible",
+ -11.863151550292969
+ ],
+ [
+ "▁vreo",
+ -11.863166809082031
+ ],
+ [
+ "spekt",
+ -11.863249778747559
+ ],
+ [
+ "▁appreciation",
+ -11.8633394241333
+ ],
+ [
+ "▁automation",
+ -11.863377571105957
+ ],
+ [
+ "Luc",
+ -11.86341381072998
+ ],
+ [
+ "teaches",
+ -11.863471031188965
+ ],
+ [
+ "▁fold",
+ -11.863506317138672
+ ],
+ [
+ "deutsche",
+ -11.863523483276367
+ ],
+ [
+ "▁assisted",
+ -11.86380386352539
+ ],
+ [
+ "▁straightforward",
+ -11.863932609558105
+ ],
+ [
+ "▁mechanic",
+ -11.864068031311035
+ ],
+ [
+ "observ",
+ -11.864169120788574
+ ],
+ [
+ "▁Schau",
+ -11.864195823669434
+ ],
+ [
+ "▁Recently",
+ -11.864301681518555
+ ],
+ [
+ "kers",
+ -11.86435604095459
+ ],
+ [
+ "▁Soft",
+ -11.864455223083496
+ ],
+ [
+ "muni",
+ -11.864537239074707
+ ],
+ [
+ "▁lie",
+ -11.864617347717285
+ ],
+ [
+ "▁Fat",
+ -11.864728927612305
+ ],
+ [
+ "cream",
+ -11.86476993560791
+ ],
+ [
+ "▁snack",
+ -11.864909172058105
+ ],
+ [
+ "▁juin",
+ -11.865068435668945
+ ],
+ [
+ "▁competent",
+ -11.865134239196777
+ ],
+ [
+ "▁Drug",
+ -11.865141868591309
+ ],
+ [
+ "▁Row",
+ -11.865302085876465
+ ],
+ [
+ "▁needle",
+ -11.865852355957031
+ ],
+ [
+ "▁convey",
+ -11.865900039672852
+ ],
+ [
+ "▁voie",
+ -11.86600399017334
+ ],
+ [
+ "▁Hon",
+ -11.866190910339355
+ ],
+ [
+ "▁ebook",
+ -11.866194725036621
+ ],
+ [
+ "▁veteran",
+ -11.866209030151367
+ ],
+ [
+ "▁statistical",
+ -11.866217613220215
+ ],
+ [
+ "190",
+ -11.866312980651855
+ ],
+ [
+ "▁munca",
+ -11.866402626037598
+ ],
+ [
+ "▁venues",
+ -11.866438865661621
+ ],
+ [
+ "▁Viel",
+ -11.866604804992676
+ ],
+ [
+ "▁décor",
+ -11.866799354553223
+ ],
+ [
+ "▁répond",
+ -11.8670015335083
+ ],
+ [
+ "▁produsele",
+ -11.86700439453125
+ ],
+ [
+ "ruc",
+ -11.867009162902832
+ ],
+ [
+ "▁drops",
+ -11.867011070251465
+ ],
+ [
+ "▁autant",
+ -11.867311477661133
+ ],
+ [
+ "▁Fahrzeug",
+ -11.867313385009766
+ ],
+ [
+ "▁hills",
+ -11.86735725402832
+ ],
+ [
+ "ference",
+ -11.867414474487305
+ ],
+ [
+ "▁Glück",
+ -11.86742115020752
+ ],
+ [
+ "▁Pac",
+ -11.867480278015137
+ ],
+ [
+ "▁permettr",
+ -11.867568969726562
+ ],
+ [
+ "▁mouvement",
+ -11.867713928222656
+ ],
+ [
+ "établissement",
+ -11.867859840393066
+ ],
+ [
+ "▁Parc",
+ -11.867874145507812
+ ],
+ [
+ "▁solving",
+ -11.867900848388672
+ ],
+ [
+ "▁jail",
+ -11.867972373962402
+ ],
+ [
+ "▁junk",
+ -11.867980003356934
+ ],
+ [
+ "▁jeux",
+ -11.868091583251953
+ ],
+ [
+ "▁rôle",
+ -11.868107795715332
+ ],
+ [
+ "▁cache",
+ -11.868124961853027
+ ],
+ [
+ "▁Answer",
+ -11.86832046508789
+ ],
+ [
+ "wir",
+ -11.868706703186035
+ ],
+ [
+ "option",
+ -11.868732452392578
+ ],
+ [
+ "▁Tiger",
+ -11.868739128112793
+ ],
+ [
+ "▁Ble",
+ -11.868793487548828
+ ],
+ [
+ "Mitglied",
+ -11.868797302246094
+ ],
+ [
+ "▁partial",
+ -11.868819236755371
+ ],
+ [
+ "▁Mercedes",
+ -11.86888313293457
+ ],
+ [
+ "tire",
+ -11.869001388549805
+ ],
+ [
+ "MENT",
+ -11.869091987609863
+ ],
+ [
+ "▁transit",
+ -11.869230270385742
+ ],
+ [
+ "▁cineva",
+ -11.869285583496094
+ ],
+ [
+ "▁Andrea",
+ -11.869294166564941
+ ],
+ [
+ "▁boundaries",
+ -11.869497299194336
+ ],
+ [
+ "script",
+ -11.870061874389648
+ ],
+ [
+ "▁Medi",
+ -11.870123863220215
+ ],
+ [
+ "schreiben",
+ -11.870203018188477
+ ],
+ [
+ "▁lobby",
+ -11.87035846710205
+ ],
+ [
+ "▁defendant",
+ -11.870406150817871
+ ],
+ [
+ "▁sq",
+ -11.870467185974121
+ ],
+ [
+ "▁forgotten",
+ -11.870569229125977
+ ],
+ [
+ "stimmung",
+ -11.870651245117188
+ ],
+ [
+ "hus",
+ -11.870665550231934
+ ],
+ [
+ "RY",
+ -11.870728492736816
+ ],
+ [
+ "▁Anderson",
+ -11.870748519897461
+ ],
+ [
+ "▁Dental",
+ -11.870828628540039
+ ],
+ [
+ "ject",
+ -11.87110710144043
+ ],
+ [
+ "▁Nutzer",
+ -11.871377944946289
+ ],
+ [
+ "▁Portland",
+ -11.871540069580078
+ ],
+ [
+ "scription",
+ -11.871636390686035
+ ],
+ [
+ "▁angel",
+ -11.871695518493652
+ ],
+ [
+ "▁monument",
+ -11.871748924255371
+ ],
+ [
+ "▁număr",
+ -11.871784210205078
+ ],
+ [
+ "▁Lane",
+ -11.871800422668457
+ ],
+ [
+ "▁Bai",
+ -11.871894836425781
+ ],
+ [
+ "But",
+ -11.871909141540527
+ ],
+ [
+ "▁calculate",
+ -11.872315406799316
+ ],
+ [
+ "▁provoca",
+ -11.87247371673584
+ ],
+ [
+ "▁votes",
+ -11.872493743896484
+ ],
+ [
+ "RNA",
+ -11.872503280639648
+ ],
+ [
+ "though",
+ -11.87259292602539
+ ],
+ [
+ "spor",
+ -11.872631072998047
+ ],
+ [
+ "▁connaissance",
+ -11.872695922851562
+ ],
+ [
+ "▁Anwendung",
+ -11.872932434082031
+ ],
+ [
+ "▁Kate",
+ -11.873123168945312
+ ],
+ [
+ "lob",
+ -11.87315845489502
+ ],
+ [
+ "▁Conf",
+ -11.873180389404297
+ ],
+ [
+ "bung",
+ -11.873212814331055
+ ],
+ [
+ "ander",
+ -11.873282432556152
+ ],
+ [
+ "▁functioning",
+ -11.873297691345215
+ ],
+ [
+ "▁sponsored",
+ -11.873324394226074
+ ],
+ [
+ "rav",
+ -11.873734474182129
+ ],
+ [
+ "▁resistant",
+ -11.873797416687012
+ ],
+ [
+ "tră",
+ -11.873916625976562
+ ],
+ [
+ "▁costly",
+ -11.873923301696777
+ ],
+ [
+ "▁Mars",
+ -11.873991012573242
+ ],
+ [
+ "▁tir",
+ -11.874075889587402
+ ],
+ [
+ "▁writes",
+ -11.874134063720703
+ ],
+ [
+ "▁Greg",
+ -11.874267578125
+ ],
+ [
+ "▁Question",
+ -11.874714851379395
+ ],
+ [
+ "▁corporation",
+ -11.87485408782959
+ ],
+ [
+ "▁lire",
+ -11.874991416931152
+ ],
+ [
+ "locked",
+ -11.875048637390137
+ ],
+ [
+ "8,",
+ -11.875092506408691
+ ],
+ [
+ "▁sagt",
+ -11.875301361083984
+ ],
+ [
+ "gaining",
+ -11.87536907196045
+ ],
+ [
+ "▁Pierre",
+ -11.875688552856445
+ ],
+ [
+ "verb",
+ -11.875725746154785
+ ],
+ [
+ "▁Barcelona",
+ -11.87578296661377
+ ],
+ [
+ "werte",
+ -11.876474380493164
+ ],
+ [
+ "▁disponible",
+ -11.87651538848877
+ ],
+ [
+ "▁urge",
+ -11.876521110534668
+ ],
+ [
+ "▁expecting",
+ -11.876572608947754
+ ],
+ [
+ "▁Girl",
+ -11.87662124633789
+ ],
+ [
+ "▁unlimited",
+ -11.876761436462402
+ ],
+ [
+ "watt",
+ -11.876788139343262
+ ],
+ [
+ "▁Möglichkeiten",
+ -11.876813888549805
+ ],
+ [
+ "▁schöne",
+ -11.876847267150879
+ ],
+ [
+ "rium",
+ -11.877076148986816
+ ],
+ [
+ "That",
+ -11.877272605895996
+ ],
+ [
+ "▁socio",
+ -11.877296447753906
+ ],
+ [
+ "▁Democrats",
+ -11.877351760864258
+ ],
+ [
+ "guten",
+ -11.877422332763672
+ ],
+ [
+ "▁Lou",
+ -11.877425193786621
+ ],
+ [
+ "ităţi",
+ -11.877559661865234
+ ],
+ [
+ "▁possibilité",
+ -11.877717018127441
+ ],
+ [
+ "▁adjustable",
+ -11.877938270568848
+ ],
+ [
+ "▁Salt",
+ -11.877967834472656
+ ],
+ [
+ "Thr",
+ -11.878021240234375
+ ],
+ [
+ "▁biseric",
+ -11.878056526184082
+ ],
+ [
+ "ieux",
+ -11.87808895111084
+ ],
+ [
+ "▁procur",
+ -11.8782377243042
+ ],
+ [
+ "▁credits",
+ -11.878250122070312
+ ],
+ [
+ "▁Netflix",
+ -11.878585815429688
+ ],
+ [
+ "doi",
+ -11.878605842590332
+ ],
+ [
+ "▁Jews",
+ -11.878663063049316
+ ],
+ [
+ "▁Ukraine",
+ -11.87873363494873
+ ],
+ [
+ "▁adevărat",
+ -11.878785133361816
+ ],
+ [
+ "▁Apply",
+ -11.878813743591309
+ ],
+ [
+ "▁coupons",
+ -11.878859519958496
+ ],
+ [
+ "▁Detroit",
+ -11.878881454467773
+ ],
+ [
+ "▁rue",
+ -11.878889083862305
+ ],
+ [
+ "anumite",
+ -11.878926277160645
+ ],
+ [
+ "ished",
+ -11.878973960876465
+ ],
+ [
+ "▁withdrawal",
+ -11.87915325164795
+ ],
+ [
+ "▁replacing",
+ -11.87917709350586
+ ],
+ [
+ "catching",
+ -11.879385948181152
+ ],
+ [
+ "▁climbing",
+ -11.879612922668457
+ ],
+ [
+ "▁Basic",
+ -11.879770278930664
+ ],
+ [
+ "▁inclus",
+ -11.879783630371094
+ ],
+ [
+ "scope",
+ -11.879887580871582
+ ],
+ [
+ "▁facem",
+ -11.879892349243164
+ ],
+ [
+ "▁plec",
+ -11.879904747009277
+ ],
+ [
+ "mäßig",
+ -11.879980087280273
+ ],
+ [
+ "▁tasty",
+ -11.880064010620117
+ ],
+ [
+ "▁tunnel",
+ -11.880074501037598
+ ],
+ [
+ "figured",
+ -11.88032341003418
+ ],
+ [
+ "gged",
+ -11.880390167236328
+ ],
+ [
+ "▁conditii",
+ -11.880599975585938
+ ],
+ [
+ "▁homework",
+ -11.880631446838379
+ ],
+ [
+ "volle",
+ -11.88063907623291
+ ],
+ [
+ "▁Gott",
+ -11.880807876586914
+ ],
+ [
+ "▁95",
+ -11.880969047546387
+ ],
+ [
+ "▁elect",
+ -11.881020545959473
+ ],
+ [
+ "▁blast",
+ -11.881043434143066
+ ],
+ [
+ "▁easiest",
+ -11.881248474121094
+ ],
+ [
+ "USE",
+ -11.881462097167969
+ ],
+ [
+ "concentr",
+ -11.881475448608398
+ ],
+ [
+ "orial",
+ -11.881596565246582
+ ],
+ [
+ "▁scroll",
+ -11.881638526916504
+ ],
+ [
+ "stead",
+ -11.881691932678223
+ ],
+ [
+ "▁hormone",
+ -11.881710052490234
+ ],
+ [
+ "▁starter",
+ -11.88179874420166
+ ],
+ [
+ "▁cald",
+ -11.881878852844238
+ ],
+ [
+ "▁wax",
+ -11.881895065307617
+ ],
+ [
+ "▁ridic",
+ -11.881900787353516
+ ],
+ [
+ "ously",
+ -11.881982803344727
+ ],
+ [
+ "maschine",
+ -11.882101058959961
+ ],
+ [
+ "licher",
+ -11.882399559020996
+ ],
+ [
+ "▁16,",
+ -11.882452964782715
+ ],
+ [
+ "▁hassle",
+ -11.882469177246094
+ ],
+ [
+ "semnat",
+ -11.882535934448242
+ ],
+ [
+ "▁pub",
+ -11.88260555267334
+ ],
+ [
+ "240",
+ -11.882800102233887
+ ],
+ [
+ "▁kits",
+ -11.882871627807617
+ ],
+ [
+ "▁Generation",
+ -11.88293743133545
+ ],
+ [
+ "▁merchant",
+ -11.883052825927734
+ ],
+ [
+ "▁Erd",
+ -11.883068084716797
+ ],
+ [
+ "▁café",
+ -11.883077621459961
+ ],
+ [
+ "hoff",
+ -11.88314151763916
+ ],
+ [
+ "▁WITH",
+ -11.883376121520996
+ ],
+ [
+ "▁gesch",
+ -11.883515357971191
+ ],
+ [
+ "▁Editor",
+ -11.883557319641113
+ ],
+ [
+ "▁treats",
+ -11.883609771728516
+ ],
+ [
+ "▁harsh",
+ -11.883711814880371
+ ],
+ [
+ "rome",
+ -11.883729934692383
+ ],
+ [
+ "▁Foreign",
+ -11.883928298950195
+ ],
+ [
+ "▁denied",
+ -11.883968353271484
+ ],
+ [
+ "▁Valentine",
+ -11.884014129638672
+ ],
+ [
+ "▁healthier",
+ -11.88408088684082
+ ],
+ [
+ "▁readily",
+ -11.884138107299805
+ ],
+ [
+ "nac",
+ -11.884190559387207
+ ],
+ [
+ "▁intake",
+ -11.884191513061523
+ ],
+ [
+ "▁puncte",
+ -11.884230613708496
+ ],
+ [
+ "erne",
+ -11.884431838989258
+ ],
+ [
+ "file",
+ -11.884668350219727
+ ],
+ [
+ "▁continually",
+ -11.884688377380371
+ ],
+ [
+ "door",
+ -11.884699821472168
+ ],
+ [
+ "▁imediat",
+ -11.884822845458984
+ ],
+ [
+ "▁accused",
+ -11.884833335876465
+ ],
+ [
+ "chy",
+ -11.884854316711426
+ ],
+ [
+ "▁wrapped",
+ -11.884861946105957
+ ],
+ [
+ "IES",
+ -11.884878158569336
+ ],
+ [
+ "▁terrace",
+ -11.884883880615234
+ ],
+ [
+ "mouth",
+ -11.884897232055664
+ ],
+ [
+ "▁defensive",
+ -11.884991645812988
+ ],
+ [
+ "▁Luci",
+ -11.88508129119873
+ ],
+ [
+ "▁significance",
+ -11.885107040405273
+ ],
+ [
+ "▁2007,",
+ -11.885213851928711
+ ],
+ [
+ "▁inclusion",
+ -11.885221481323242
+ ],
+ [
+ "▁rotation",
+ -11.885248184204102
+ ],
+ [
+ "hos",
+ -11.885283470153809
+ ],
+ [
+ "▁crea",
+ -11.885357856750488
+ ],
+ [
+ "üß",
+ -11.885903358459473
+ ],
+ [
+ "▁Install",
+ -11.885988235473633
+ ],
+ [
+ "▁dump",
+ -11.885998725891113
+ ],
+ [
+ "▁informations",
+ -11.886114120483398
+ ],
+ [
+ "▁Thi",
+ -11.886117935180664
+ ],
+ [
+ "▁85",
+ -11.886252403259277
+ ],
+ [
+ "dox",
+ -11.886283874511719
+ ],
+ [
+ "track",
+ -11.886436462402344
+ ],
+ [
+ "▁couples",
+ -11.886571884155273
+ ],
+ [
+ "▁Assembly",
+ -11.886594772338867
+ ],
+ [
+ "wagen",
+ -11.88672161102295
+ ],
+ [
+ "▁Hil",
+ -11.886723518371582
+ ],
+ [
+ "ières",
+ -11.886833190917969
+ ],
+ [
+ "▁Gabriel",
+ -11.886903762817383
+ ],
+ [
+ "▁patience",
+ -11.887053489685059
+ ],
+ [
+ "▁colored",
+ -11.887147903442383
+ ],
+ [
+ "▁separately",
+ -11.88715934753418
+ ],
+ [
+ "▁deployment",
+ -11.887166023254395
+ ],
+ [
+ "scape",
+ -11.887306213378906
+ ],
+ [
+ "▁Acum",
+ -11.8875150680542
+ ],
+ [
+ "▁länger",
+ -11.887518882751465
+ ],
+ [
+ "▁screens",
+ -11.887598991394043
+ ],
+ [
+ "▁prezenta",
+ -11.887630462646484
+ ],
+ [
+ "▁obicei",
+ -11.887638092041016
+ ],
+ [
+ "▁crisp",
+ -11.887758255004883
+ ],
+ [
+ "▁mechanisms",
+ -11.887771606445312
+ ],
+ [
+ "▁thirty",
+ -11.887786865234375
+ ],
+ [
+ "▁individually",
+ -11.887989044189453
+ ],
+ [
+ "▁internationally",
+ -11.887991905212402
+ ],
+ [
+ "lling",
+ -11.888050079345703
+ ],
+ [
+ "▁bureau",
+ -11.88843059539795
+ ],
+ [
+ "▁erfahren",
+ -11.88844108581543
+ ],
+ [
+ "TY",
+ -11.888553619384766
+ ],
+ [
+ "PF",
+ -11.888607025146484
+ ],
+ [
+ "wid",
+ -11.888752937316895
+ ],
+ [
+ "sell",
+ -11.888835906982422
+ ],
+ [
+ "▁Luke",
+ -11.888879776000977
+ ],
+ [
+ "▁Must",
+ -11.888916969299316
+ ],
+ [
+ "▁identical",
+ -11.888927459716797
+ ],
+ [
+ "▁Netherlands",
+ -11.888980865478516
+ ],
+ [
+ "▁investor",
+ -11.88905143737793
+ ],
+ [
+ "▁squad",
+ -11.889073371887207
+ ],
+ [
+ "▁21,",
+ -11.889143943786621
+ ],
+ [
+ "iko",
+ -11.889230728149414
+ ],
+ [
+ "▁departure",
+ -11.88937759399414
+ ],
+ [
+ "ega",
+ -11.889384269714355
+ ],
+ [
+ "uzi",
+ -11.889408111572266
+ ],
+ [
+ "▁lasa",
+ -11.889458656311035
+ ],
+ [
+ "bian",
+ -11.889525413513184
+ ],
+ [
+ "▁Madrid",
+ -11.889623641967773
+ ],
+ [
+ "▁Iowa",
+ -11.889806747436523
+ ],
+ [
+ "▁Yellow",
+ -11.890026092529297
+ ],
+ [
+ "conom",
+ -11.89004898071289
+ ],
+ [
+ "▁hint",
+ -11.890098571777344
+ ],
+ [
+ "NOW",
+ -11.890111923217773
+ ],
+ [
+ "dress",
+ -11.890204429626465
+ ],
+ [
+ "▁Stück",
+ -11.890267372131348
+ ],
+ [
+ "echt",
+ -11.890424728393555
+ ],
+ [
+ "rial",
+ -11.89045238494873
+ ],
+ [
+ "▁Initiative",
+ -11.890474319458008
+ ],
+ [
+ "▁magnificent",
+ -11.890474319458008
+ ],
+ [
+ "▁pipeline",
+ -11.890543937683105
+ ],
+ [
+ "▁08",
+ -11.890806198120117
+ ],
+ [
+ "▁écrit",
+ -11.890889167785645
+ ],
+ [
+ "KA",
+ -11.891085624694824
+ ],
+ [
+ "arile",
+ -11.891151428222656
+ ],
+ [
+ "▁unfortunately",
+ -11.891352653503418
+ ],
+ [
+ "dose",
+ -11.891355514526367
+ ],
+ [
+ "▁counts",
+ -11.891427993774414
+ ],
+ [
+ "deciding",
+ -11.891549110412598
+ ],
+ [
+ "WA",
+ -11.89167308807373
+ ],
+ [
+ "▁doresc",
+ -11.891685485839844
+ ],
+ [
+ "NY",
+ -11.892008781433105
+ ],
+ [
+ "olin",
+ -11.892112731933594
+ ],
+ [
+ "▁Urlaub",
+ -11.892133712768555
+ ],
+ [
+ "▁alătur",
+ -11.892317771911621
+ ],
+ [
+ "▁Vic",
+ -11.892515182495117
+ ],
+ [
+ "▁fier",
+ -11.89269733428955
+ ],
+ [
+ "EU",
+ -11.892772674560547
+ ],
+ [
+ "▁triple",
+ -11.892871856689453
+ ],
+ [
+ "▁compliment",
+ -11.89310359954834
+ ],
+ [
+ "▁vegetable",
+ -11.89334487915039
+ ],
+ [
+ "member",
+ -11.893743515014648
+ ],
+ [
+ "atiei",
+ -11.893793106079102
+ ],
+ [
+ "▁toxic",
+ -11.893835067749023
+ ],
+ [
+ "▁converted",
+ -11.893888473510742
+ ],
+ [
+ "▁Pink",
+ -11.893999099731445
+ ],
+ [
+ "▁fragment",
+ -11.894020080566406
+ ],
+ [
+ "presenting",
+ -11.894027709960938
+ ],
+ [
+ "▁garantie",
+ -11.894031524658203
+ ],
+ [
+ "▁31,",
+ -11.894052505493164
+ ],
+ [
+ "▁puisqu",
+ -11.894105911254883
+ ],
+ [
+ "aching",
+ -11.894107818603516
+ ],
+ [
+ "▁Shan",
+ -11.894119262695312
+ ],
+ [
+ "▁Affairs",
+ -11.894368171691895
+ ],
+ [
+ "üsse",
+ -11.894405364990234
+ ],
+ [
+ "▁CBD",
+ -11.894428253173828
+ ],
+ [
+ "▁quatre",
+ -11.894588470458984
+ ],
+ [
+ "▁horror",
+ -11.894651412963867
+ ],
+ [
+ "▁culoare",
+ -11.894661903381348
+ ],
+ [
+ "▁welcoming",
+ -11.894673347473145
+ ],
+ [
+ "▁headache",
+ -11.894808769226074
+ ],
+ [
+ "▁septembre",
+ -11.894820213317871
+ ],
+ [
+ "▁Tür",
+ -11.894862174987793
+ ],
+ [
+ "lateral",
+ -11.89507007598877
+ ],
+ [
+ "▁termin",
+ -11.895228385925293
+ ],
+ [
+ "▁Aid",
+ -11.895291328430176
+ ],
+ [
+ "second",
+ -11.895308494567871
+ ],
+ [
+ "▁Philip",
+ -11.895310401916504
+ ],
+ [
+ "berries",
+ -11.895347595214844
+ ],
+ [
+ "▁Slot",
+ -11.895431518554688
+ ],
+ [
+ "ка",
+ -11.895442962646484
+ ],
+ [
+ "▁consecutive",
+ -11.895590782165527
+ ],
+ [
+ "value",
+ -11.895705223083496
+ ],
+ [
+ "▁islands",
+ -11.8958101272583
+ ],
+ [
+ "▁posibilitatea",
+ -11.895928382873535
+ ],
+ [
+ "0.5",
+ -11.896341323852539
+ ],
+ [
+ "▁Dumpster",
+ -11.896471977233887
+ ],
+ [
+ "▁Gran",
+ -11.89647388458252
+ ],
+ [
+ "▁restricted",
+ -11.8967924118042
+ ],
+ [
+ "▁discussing",
+ -11.896921157836914
+ ],
+ [
+ "cock",
+ -11.896966934204102
+ ],
+ [
+ "Serie",
+ -11.896989822387695
+ ],
+ [
+ "▁crushing",
+ -11.896998405456543
+ ],
+ [
+ "RB",
+ -11.897034645080566
+ ],
+ [
+ "▁Gy",
+ -11.897068977355957
+ ],
+ [
+ "normal",
+ -11.897098541259766
+ ],
+ [
+ "DT",
+ -11.897180557250977
+ ],
+ [
+ "▁concurs",
+ -11.897181510925293
+ ],
+ [
+ "▁Beratung",
+ -11.897231101989746
+ ],
+ [
+ "▁handful",
+ -11.897235870361328
+ ],
+ [
+ "▁loading",
+ -11.897237777709961
+ ],
+ [
+ "▁WI",
+ -11.897269248962402
+ ],
+ [
+ "▁Fitness",
+ -11.897283554077148
+ ],
+ [
+ "▁RAM",
+ -11.897302627563477
+ ],
+ [
+ "▁Twi",
+ -11.89730453491211
+ ],
+ [
+ "adurch",
+ -11.897345542907715
+ ],
+ [
+ "▁obiectiv",
+ -11.897366523742676
+ ],
+ [
+ "BM",
+ -11.897635459899902
+ ],
+ [
+ "▁amendment",
+ -11.8976469039917
+ ],
+ [
+ "whi",
+ -11.897652626037598
+ ],
+ [
+ "▁Besonder",
+ -11.897871017456055
+ ],
+ [
+ "ALL",
+ -11.898003578186035
+ ],
+ [
+ "▁earning",
+ -11.898090362548828
+ ],
+ [
+ "▁nutrients",
+ -11.898580551147461
+ ],
+ [
+ "pru",
+ -11.898633003234863
+ ],
+ [
+ "▁offensive",
+ -11.898696899414062
+ ],
+ [
+ "▁shelves",
+ -11.898711204528809
+ ],
+ [
+ "▁încâ",
+ -11.898726463317871
+ ],
+ [
+ "▁execute",
+ -11.898923873901367
+ ],
+ [
+ "▁cauz",
+ -11.898966789245605
+ ],
+ [
+ "exist",
+ -11.899179458618164
+ ],
+ [
+ "▁Meter",
+ -11.899191856384277
+ ],
+ [
+ "there",
+ -11.899201393127441
+ ],
+ [
+ "▁réaliser",
+ -11.899249076843262
+ ],
+ [
+ "blog",
+ -11.899362564086914
+ ],
+ [
+ "▁résultats",
+ -11.89937973022461
+ ],
+ [
+ "baren",
+ -11.899391174316406
+ ],
+ [
+ "▁lang",
+ -11.899425506591797
+ ],
+ [
+ "▁mere",
+ -11.899870872497559
+ ],
+ [
+ "▁toti",
+ -11.900079727172852
+ ],
+ [
+ "DN",
+ -11.90017032623291
+ ],
+ [
+ "Hi",
+ -11.900310516357422
+ ],
+ [
+ "▁merg",
+ -11.900359153747559
+ ],
+ [
+ "▁Camera",
+ -11.90054988861084
+ ],
+ [
+ "▁parfum",
+ -11.900697708129883
+ ],
+ [
+ "CG",
+ -11.900701522827148
+ ],
+ [
+ "posed",
+ -11.900713920593262
+ ],
+ [
+ "▁proposals",
+ -11.900732040405273
+ ],
+ [
+ "▁incorrect",
+ -11.900811195373535
+ ],
+ [
+ "▁Denver",
+ -11.901168823242188
+ ],
+ [
+ "▁noapte",
+ -11.901397705078125
+ ],
+ [
+ "▁VPN",
+ -11.901436805725098
+ ],
+ [
+ "▁Oklahoma",
+ -11.90159797668457
+ ],
+ [
+ "horizon",
+ -11.901647567749023
+ ],
+ [
+ "▁villa",
+ -11.901668548583984
+ ],
+ [
+ "duce",
+ -11.901812553405762
+ ],
+ [
+ "Dienst",
+ -11.902042388916016
+ ],
+ [
+ "▁oversee",
+ -11.902511596679688
+ ],
+ [
+ "astr",
+ -11.902548789978027
+ ],
+ [
+ "brand",
+ -11.902713775634766
+ ],
+ [
+ "▁Safe",
+ -11.902746200561523
+ ],
+ [
+ "▁competing",
+ -11.902812004089355
+ ],
+ [
+ "▁subiect",
+ -11.902812004089355
+ ],
+ [
+ "▁équipe",
+ -11.903091430664062
+ ],
+ [
+ "▁Dress",
+ -11.903095245361328
+ ],
+ [
+ "▁Juni",
+ -11.903139114379883
+ ],
+ [
+ "▁repeated",
+ -11.90317153930664
+ ],
+ [
+ "2012",
+ -11.903226852416992
+ ],
+ [
+ "▁départ",
+ -11.903234481811523
+ ],
+ [
+ "immer",
+ -11.903335571289062
+ ],
+ [
+ "▁mondial",
+ -11.903374671936035
+ ],
+ [
+ "▁datelor",
+ -11.903703689575195
+ ],
+ [
+ "▁surgeon",
+ -11.903782844543457
+ ],
+ [
+ "▁demanding",
+ -11.903812408447266
+ ],
+ [
+ "▁concluded",
+ -11.903878211975098
+ ],
+ [
+ "țiile",
+ -11.903950691223145
+ ],
+ [
+ "marin",
+ -11.903999328613281
+ ],
+ [
+ "▁estim",
+ -11.904206275939941
+ ],
+ [
+ "▁Loan",
+ -11.904361724853516
+ ],
+ [
+ "sculpt",
+ -11.904373168945312
+ ],
+ [
+ "▁99",
+ -11.904391288757324
+ ],
+ [
+ "void",
+ -11.904400825500488
+ ],
+ [
+ "▁Empire",
+ -11.904499053955078
+ ],
+ [
+ "▁Brit",
+ -11.90450382232666
+ ],
+ [
+ "▁véhicule",
+ -11.904777526855469
+ ],
+ [
+ "▁dividend",
+ -11.905069351196289
+ ],
+ [
+ "▁refused",
+ -11.905077934265137
+ ],
+ [
+ "▁speaks",
+ -11.905156135559082
+ ],
+ [
+ "▁Morris",
+ -11.905282020568848
+ ],
+ [
+ "dict",
+ -11.905349731445312
+ ],
+ [
+ "▁funeral",
+ -11.905556678771973
+ ],
+ [
+ "▁Behandlung",
+ -11.905763626098633
+ ],
+ [
+ "▁Revolution",
+ -11.905905723571777
+ ],
+ [
+ "▁Sum",
+ -11.905935287475586
+ ],
+ [
+ "einigen",
+ -11.906030654907227
+ ],
+ [
+ "RES",
+ -11.906070709228516
+ ],
+ [
+ "▁vite",
+ -11.906071662902832
+ ],
+ [
+ "▁Captain",
+ -11.906190872192383
+ ],
+ [
+ "▁assurance",
+ -11.9061918258667
+ ],
+ [
+ "uga",
+ -11.906500816345215
+ ],
+ [
+ "▁conserv",
+ -11.906583786010742
+ ],
+ [
+ "▁therapeutic",
+ -11.906641006469727
+ ],
+ [
+ "▁Sweden",
+ -11.906753540039062
+ ],
+ [
+ "▁Lead",
+ -11.906888961791992
+ ],
+ [
+ "ément",
+ -11.907071113586426
+ ],
+ [
+ "▁53",
+ -11.90709114074707
+ ],
+ [
+ "▁fraction",
+ -11.9071683883667
+ ],
+ [
+ "▁magnet",
+ -11.907170295715332
+ ],
+ [
+ "assurer",
+ -11.907184600830078
+ ],
+ [
+ "▁Steuer",
+ -11.90733814239502
+ ],
+ [
+ "▁flori",
+ -11.90735149383545
+ ],
+ [
+ "▁charming",
+ -11.907588958740234
+ ],
+ [
+ "▁athletic",
+ -11.907621383666992
+ ],
+ [
+ "▁membri",
+ -11.907706260681152
+ ],
+ [
+ "▁Sep",
+ -11.907726287841797
+ ],
+ [
+ "ogue",
+ -11.907800674438477
+ ],
+ [
+ "▁familie",
+ -11.907800674438477
+ ],
+ [
+ "▁SW",
+ -11.90796947479248
+ ],
+ [
+ "▁diagnosed",
+ -11.908023834228516
+ ],
+ [
+ "RR",
+ -11.908143997192383
+ ],
+ [
+ "▁Fern",
+ -11.908233642578125
+ ],
+ [
+ "▁rational",
+ -11.908281326293945
+ ],
+ [
+ "▁talents",
+ -11.90828800201416
+ ],
+ [
+ "ziert",
+ -11.908317565917969
+ ],
+ [
+ "▁chemin",
+ -11.908459663391113
+ ],
+ [
+ "sheet",
+ -11.908562660217285
+ ],
+ [
+ "▁outer",
+ -11.908565521240234
+ ],
+ [
+ "▁Kap",
+ -11.908591270446777
+ ],
+ [
+ "▁HERE",
+ -11.908656120300293
+ ],
+ [
+ "▁uman",
+ -11.908824920654297
+ ],
+ [
+ "▁accompany",
+ -11.908880233764648
+ ],
+ [
+ "▁varieties",
+ -11.908881187438965
+ ],
+ [
+ "▁sensors",
+ -11.908957481384277
+ ],
+ [
+ "▁25%",
+ -11.90919017791748
+ ],
+ [
+ "▁tray",
+ -11.909354209899902
+ ],
+ [
+ "▁critique",
+ -11.909459114074707
+ ],
+ [
+ "▁puţin",
+ -11.909515380859375
+ ],
+ [
+ "▁Schüler",
+ -11.90953540802002
+ ],
+ [
+ "▁repar",
+ -11.909744262695312
+ ],
+ [
+ "▁overlook",
+ -11.909931182861328
+ ],
+ [
+ "▁surf",
+ -11.910048484802246
+ ],
+ [
+ "▁tasting",
+ -11.910118103027344
+ ],
+ [
+ "bog",
+ -11.91027545928955
+ ],
+ [
+ "▁Payment",
+ -11.910289764404297
+ ],
+ [
+ "▁Helen",
+ -11.91049575805664
+ ],
+ [
+ "▁Refer",
+ -11.910694122314453
+ ],
+ [
+ "application",
+ -11.910698890686035
+ ],
+ [
+ "lection",
+ -11.910856246948242
+ ],
+ [
+ "▁avril",
+ -11.911042213439941
+ ],
+ [
+ "▁Grace",
+ -11.911109924316406
+ ],
+ [
+ "▁kau",
+ -11.911274909973145
+ ],
+ [
+ "▁libraries",
+ -11.911319732666016
+ ],
+ [
+ "▁closest",
+ -11.911347389221191
+ ],
+ [
+ "▁coating",
+ -11.911351203918457
+ ],
+ [
+ "▁suicide",
+ -11.911364555358887
+ ],
+ [
+ "▁undergraduate",
+ -11.911449432373047
+ ],
+ [
+ "▁stitch",
+ -11.91149616241455
+ ],
+ [
+ "▁reset",
+ -11.911593437194824
+ ],
+ [
+ "▁Greece",
+ -11.911626815795898
+ ],
+ [
+ "▁Fred",
+ -11.91197681427002
+ ],
+ [
+ "▁18.",
+ -11.912047386169434
+ ],
+ [
+ "▁nuit",
+ -11.912087440490723
+ ],
+ [
+ "▁lying",
+ -11.912199974060059
+ ],
+ [
+ "▁cottage",
+ -11.91232681274414
+ ],
+ [
+ "bone",
+ -11.912477493286133
+ ],
+ [
+ "▁milieu",
+ -11.912480354309082
+ ],
+ [
+ "management",
+ -11.912623405456543
+ ],
+ [
+ "▁Freund",
+ -11.912724494934082
+ ],
+ [
+ "▁specially",
+ -11.912841796875
+ ],
+ [
+ "veut",
+ -11.912961959838867
+ ],
+ [
+ "▁necesare",
+ -11.912999153137207
+ ],
+ [
+ "▁cert",
+ -11.913081169128418
+ ],
+ [
+ "articul",
+ -11.913151741027832
+ ],
+ [
+ "150",
+ -11.913174629211426
+ ],
+ [
+ "rounded",
+ -11.913180351257324
+ ],
+ [
+ "▁longue",
+ -11.913193702697754
+ ],
+ [
+ "▁Quel",
+ -11.913240432739258
+ ],
+ [
+ "Until",
+ -11.913322448730469
+ ],
+ [
+ "▁700",
+ -11.913398742675781
+ ],
+ [
+ "▁installations",
+ -11.913423538208008
+ ],
+ [
+ "▁boats",
+ -11.913467407226562
+ ],
+ [
+ "Fig",
+ -11.913609504699707
+ ],
+ [
+ "▁cocktail",
+ -11.913613319396973
+ ],
+ [
+ "▁rocks",
+ -11.91366958618164
+ ],
+ [
+ "meinen",
+ -11.91374683380127
+ ],
+ [
+ "entrepreneur",
+ -11.913780212402344
+ ],
+ [
+ "schwarz",
+ -11.913924217224121
+ ],
+ [
+ "▁diesel",
+ -11.91392993927002
+ ],
+ [
+ "▁villages",
+ -11.913969039916992
+ ],
+ [
+ "▁cups",
+ -11.914076805114746
+ ],
+ [
+ "▁stairs",
+ -11.914241790771484
+ ],
+ [
+ "▁Match",
+ -11.914350509643555
+ ],
+ [
+ "Taking",
+ -11.914437294006348
+ ],
+ [
+ "prin",
+ -11.914469718933105
+ ],
+ [
+ "▁penal",
+ -11.91472053527832
+ ],
+ [
+ "partner",
+ -11.914867401123047
+ ],
+ [
+ "wave",
+ -11.91497802734375
+ ],
+ [
+ "▁baie",
+ -11.91515064239502
+ ],
+ [
+ "LAN",
+ -11.915151596069336
+ ],
+ [
+ "fix",
+ -11.915202140808105
+ ],
+ [
+ "▁surveillance",
+ -11.915295600891113
+ ],
+ [
+ "▁Register",
+ -11.915343284606934
+ ],
+ [
+ "oara",
+ -11.915536880493164
+ ],
+ [
+ "▁Phoenix",
+ -11.915602684020996
+ ],
+ [
+ "aktuellen",
+ -11.915613174438477
+ ],
+ [
+ "▁livres",
+ -11.915618896484375
+ ],
+ [
+ "▁entities",
+ -11.916102409362793
+ ],
+ [
+ "▁Regard",
+ -11.916112899780273
+ ],
+ [
+ "▁Jazz",
+ -11.91614055633545
+ ],
+ [
+ "▁flame",
+ -11.91616153717041
+ ],
+ [
+ "▁independence",
+ -11.916215896606445
+ ],
+ [
+ "▁Adventure",
+ -11.916341781616211
+ ],
+ [
+ "▁assign",
+ -11.916399955749512
+ ],
+ [
+ "▁Adult",
+ -11.916579246520996
+ ],
+ [
+ "kehr",
+ -11.916666984558105
+ ],
+ [
+ "▁ordering",
+ -11.916850090026855
+ ],
+ [
+ "▁charts",
+ -11.91687297821045
+ ],
+ [
+ "▁Român",
+ -11.916936874389648
+ ],
+ [
+ "bauen",
+ -11.916982650756836
+ ],
+ [
+ "▁Floor",
+ -11.917065620422363
+ ],
+ [
+ "▁Meet",
+ -11.917101860046387
+ ],
+ [
+ "▁compromise",
+ -11.917158126831055
+ ],
+ [
+ "regarded",
+ -11.917171478271484
+ ],
+ [
+ "02.",
+ -11.917215347290039
+ ],
+ [
+ "▁granite",
+ -11.917299270629883
+ ],
+ [
+ "▁Judge",
+ -11.917314529418945
+ ],
+ [
+ "opti",
+ -11.917373657226562
+ ],
+ [
+ "liste",
+ -11.917379379272461
+ ],
+ [
+ "▁capacité",
+ -11.917427062988281
+ ],
+ [
+ "▁criticism",
+ -11.917450904846191
+ ],
+ [
+ "LES",
+ -11.918198585510254
+ ],
+ [
+ "▁Century",
+ -11.918211936950684
+ ],
+ [
+ "▁mobility",
+ -11.918252944946289
+ ],
+ [
+ "▁variation",
+ -11.918622016906738
+ ],
+ [
+ "▁Utah",
+ -11.91867446899414
+ ],
+ [
+ "▁seminar",
+ -11.918678283691406
+ ],
+ [
+ "▁experiments",
+ -11.918803215026855
+ ],
+ [
+ "midst",
+ -11.918943405151367
+ ],
+ [
+ "▁Psycho",
+ -11.919002532958984
+ ],
+ [
+ "▁choses",
+ -11.919121742248535
+ ],
+ [
+ "▁Karl",
+ -11.919175148010254
+ ],
+ [
+ "▁ruling",
+ -11.919286727905273
+ ],
+ [
+ "▁Voice",
+ -11.919404983520508
+ ],
+ [
+ "▁împotriv",
+ -11.919442176818848
+ ],
+ [
+ "▁mesaj",
+ -11.919500350952148
+ ],
+ [
+ "▁vrei",
+ -11.919594764709473
+ ],
+ [
+ "fan",
+ -11.919601440429688
+ ],
+ [
+ "parent",
+ -11.919648170471191
+ ],
+ [
+ "▁oraș",
+ -11.919770240783691
+ ],
+ [
+ "▁printable",
+ -11.919777870178223
+ ],
+ [
+ "▁diver",
+ -11.919859886169434
+ ],
+ [
+ "▁ochi",
+ -11.919949531555176
+ ],
+ [
+ "▁teenager",
+ -11.920125961303711
+ ],
+ [
+ "▁Death",
+ -11.920150756835938
+ ],
+ [
+ "▁manque",
+ -11.920289993286133
+ ],
+ [
+ "ască",
+ -11.920345306396484
+ ],
+ [
+ "▁prob",
+ -11.9203519821167
+ ],
+ [
+ "▁télé",
+ -11.920354843139648
+ ],
+ [
+ "cursul",
+ -11.920378684997559
+ ],
+ [
+ "pion",
+ -11.92052173614502
+ ],
+ [
+ "▁dedication",
+ -11.920644760131836
+ ],
+ [
+ "▁opr",
+ -11.920687675476074
+ ],
+ [
+ "führung",
+ -11.920761108398438
+ ],
+ [
+ "▁cognitive",
+ -11.920827865600586
+ ],
+ [
+ "soft",
+ -11.920868873596191
+ ],
+ [
+ "▁19,",
+ -11.9209623336792
+ ],
+ [
+ "▁24-",
+ -11.921197891235352
+ ],
+ [
+ "▁legitimate",
+ -11.921220779418945
+ ],
+ [
+ "▁comedy",
+ -11.921277046203613
+ ],
+ [
+ "▁violation",
+ -11.921327590942383
+ ],
+ [
+ "▁disposal",
+ -11.921472549438477
+ ],
+ [
+ "▁liegen",
+ -11.921605110168457
+ ],
+ [
+ "ко",
+ -11.921878814697266
+ ],
+ [
+ "▁martie",
+ -11.921931266784668
+ ],
+ [
+ "▁Vas",
+ -11.92212200164795
+ ],
+ [
+ "rash",
+ -11.922134399414062
+ ],
+ [
+ "▁hadn",
+ -11.922174453735352
+ ],
+ [
+ "▁connu",
+ -11.922204971313477
+ ],
+ [
+ "▁regelmäßig",
+ -11.922216415405273
+ ],
+ [
+ "▁Webseite",
+ -11.922224998474121
+ ],
+ [
+ "▁failing",
+ -11.922273635864258
+ ],
+ [
+ "explique",
+ -11.922449111938477
+ ],
+ [
+ "▁Player",
+ -11.922513961791992
+ ],
+ [
+ "vul",
+ -11.922560691833496
+ ],
+ [
+ "camp",
+ -11.922992706298828
+ ],
+ [
+ "▁erreicht",
+ -11.922996520996094
+ ],
+ [
+ "▁tags",
+ -11.922998428344727
+ ],
+ [
+ "▁headline",
+ -11.923210144042969
+ ],
+ [
+ "▁banc",
+ -11.923253059387207
+ ],
+ [
+ "▁Mayor",
+ -11.923309326171875
+ ],
+ [
+ "trop",
+ -11.923395156860352
+ ],
+ [
+ "AK",
+ -11.9235258102417
+ ],
+ [
+ "▁lighter",
+ -11.923602104187012
+ ],
+ [
+ "▁syndrome",
+ -11.923604965209961
+ ],
+ [
+ "▁Adrian",
+ -11.92365550994873
+ ],
+ [
+ "▁EUR",
+ -11.923759460449219
+ ],
+ [
+ "▁Missouri",
+ -11.923916816711426
+ ],
+ [
+ "▁Chan",
+ -11.924108505249023
+ ],
+ [
+ "topped",
+ -11.924233436584473
+ ],
+ [
+ "▁nationwide",
+ -11.924276351928711
+ ],
+ [
+ "▁6-",
+ -11.924302101135254
+ ],
+ [
+ "final",
+ -11.924408912658691
+ ],
+ [
+ "ttes",
+ -11.924485206604004
+ ],
+ [
+ "▁FO",
+ -11.924537658691406
+ ],
+ [
+ "▁legi",
+ -11.924556732177734
+ ],
+ [
+ "▁Hum",
+ -11.924575805664062
+ ],
+ [
+ "vita",
+ -11.924662590026855
+ ],
+ [
+ "▁Regen",
+ -11.924695014953613
+ ],
+ [
+ "▁confusion",
+ -11.92498779296875
+ ],
+ [
+ "▁valori",
+ -11.925142288208008
+ ],
+ [
+ "mill",
+ -11.92516803741455
+ ],
+ [
+ "did",
+ -11.925237655639648
+ ],
+ [
+ "pid",
+ -11.925253868103027
+ ],
+ [
+ "▁implications",
+ -11.925284385681152
+ ],
+ [
+ "▁Value",
+ -11.92552375793457
+ ],
+ [
+ "lângă",
+ -11.925666809082031
+ ],
+ [
+ "▁véritable",
+ -11.92577075958252
+ ],
+ [
+ "▁Stick",
+ -11.925814628601074
+ ],
+ [
+ "zol",
+ -11.925835609436035
+ ],
+ [
+ "▁ebenso",
+ -11.925863265991211
+ ],
+ [
+ "west",
+ -11.925895690917969
+ ],
+ [
+ "▁auszu",
+ -11.92600154876709
+ ],
+ [
+ "▁adorable",
+ -11.926016807556152
+ ],
+ [
+ "▁clarity",
+ -11.92605209350586
+ ],
+ [
+ "▁Wash",
+ -11.926335334777832
+ ],
+ [
+ "▁alien",
+ -11.926423072814941
+ ],
+ [
+ "usement",
+ -11.926626205444336
+ ],
+ [
+ "▁bones",
+ -11.9266357421875
+ ],
+ [
+ "▁Beau",
+ -11.926726341247559
+ ],
+ [
+ "▁Jet",
+ -11.926727294921875
+ ],
+ [
+ "▁visibility",
+ -11.927034378051758
+ ],
+ [
+ "impose",
+ -11.927063941955566
+ ],
+ [
+ "food",
+ -11.927133560180664
+ ],
+ [
+ "▁duce",
+ -11.927361488342285
+ ],
+ [
+ "▁Format",
+ -11.927386283874512
+ ],
+ [
+ "▁durability",
+ -11.927424430847168
+ ],
+ [
+ "▁Prim",
+ -11.927614212036133
+ ],
+ [
+ "▁mele",
+ -11.927629470825195
+ ],
+ [
+ "▁dürfen",
+ -11.927631378173828
+ ],
+ [
+ "▁Angebote",
+ -11.92765998840332
+ ],
+ [
+ "▁discharge",
+ -11.927745819091797
+ ],
+ [
+ "▁Justin",
+ -11.928055763244629
+ ],
+ [
+ "▁shame",
+ -11.928228378295898
+ ],
+ [
+ "▁heated",
+ -11.928282737731934
+ ],
+ [
+ "ères",
+ -11.92856216430664
+ ],
+ [
+ "human",
+ -11.928810119628906
+ ],
+ [
+ "4.5",
+ -11.928831100463867
+ ],
+ [
+ "▁lien",
+ -11.928955078125
+ ],
+ [
+ "▁Alan",
+ -11.92896556854248
+ ],
+ [
+ "▁transmis",
+ -11.929130554199219
+ ],
+ [
+ "▁Bul",
+ -11.929137229919434
+ ],
+ [
+ "plu",
+ -11.929169654846191
+ ],
+ [
+ "acul",
+ -11.929337501525879
+ ],
+ [
+ "merk",
+ -11.929434776306152
+ ],
+ [
+ "▁altfel",
+ -11.929566383361816
+ ],
+ [
+ "deli",
+ -11.929689407348633
+ ],
+ [
+ "▁Cru",
+ -11.930001258850098
+ ],
+ [
+ "▁hommes",
+ -11.930127143859863
+ ],
+ [
+ "aurait",
+ -11.930137634277344
+ ],
+ [
+ "cca",
+ -11.930187225341797
+ ],
+ [
+ "▁Path",
+ -11.930208206176758
+ ],
+ [
+ "astronom",
+ -11.930241584777832
+ ],
+ [
+ "▁détail",
+ -11.930276870727539
+ ],
+ [
+ "▁blocked",
+ -11.930394172668457
+ ],
+ [
+ "iding",
+ -11.93044376373291
+ ],
+ [
+ "schä",
+ -11.930500030517578
+ ],
+ [
+ "▁30-",
+ -11.930624008178711
+ ],
+ [
+ "diction",
+ -11.930813789367676
+ ],
+ [
+ "▁pulling",
+ -11.930868148803711
+ ],
+ [
+ "▁Sample",
+ -11.930924415588379
+ ],
+ [
+ "▁renewable",
+ -11.930997848510742
+ ],
+ [
+ "▁Pinterest",
+ -11.93106746673584
+ ],
+ [
+ "▁Tages",
+ -11.93106746673584
+ ],
+ [
+ "▁shed",
+ -11.931171417236328
+ ],
+ [
+ "▁hart",
+ -11.931188583374023
+ ],
+ [
+ "▁serie",
+ -11.931200981140137
+ ],
+ [
+ "▁documentary",
+ -11.931208610534668
+ ],
+ [
+ "gebaut",
+ -11.931220054626465
+ ],
+ [
+ "▁Hause",
+ -11.931272506713867
+ ],
+ [
+ "share",
+ -11.931303977966309
+ ],
+ [
+ "▁inflation",
+ -11.93138599395752
+ ],
+ [
+ "▁gall",
+ -11.931504249572754
+ ],
+ [
+ "▁adjacent",
+ -11.931673049926758
+ ],
+ [
+ "jer",
+ -11.93173885345459
+ ],
+ [
+ "▁Universal",
+ -11.931946754455566
+ ],
+ [
+ "▁disabilities",
+ -11.931984901428223
+ ],
+ [
+ "▁proposition",
+ -11.93204116821289
+ ],
+ [
+ "Work",
+ -11.932293891906738
+ ],
+ [
+ "▁closure",
+ -11.932306289672852
+ ],
+ [
+ "▁separated",
+ -11.932496070861816
+ ],
+ [
+ "▁soda",
+ -11.932549476623535
+ ],
+ [
+ "▁elite",
+ -11.93263053894043
+ ],
+ [
+ "appro",
+ -11.93265438079834
+ ],
+ [
+ "▁acute",
+ -11.93266487121582
+ ],
+ [
+ "utton",
+ -11.932938575744629
+ ],
+ [
+ "▁facă",
+ -11.933053016662598
+ ],
+ [
+ "▁collector",
+ -11.933121681213379
+ ],
+ [
+ "▁unlock",
+ -11.933249473571777
+ ],
+ [
+ "▁Alpha",
+ -11.933267593383789
+ ],
+ [
+ "▁Used",
+ -11.933267593383789
+ ],
+ [
+ "▁applicants",
+ -11.933302879333496
+ ],
+ [
+ "▁înseamn",
+ -11.933387756347656
+ ],
+ [
+ "▁inclu",
+ -11.933414459228516
+ ],
+ [
+ "▁disclosure",
+ -11.933544158935547
+ ],
+ [
+ "▁Fahr",
+ -11.933995246887207
+ ],
+ [
+ "AST",
+ -11.934061050415039
+ ],
+ [
+ "▁vivre",
+ -11.934069633483887
+ ],
+ [
+ "»,",
+ -11.934167861938477
+ ],
+ [
+ "laud",
+ -11.93430233001709
+ ],
+ [
+ "▁soir",
+ -11.934365272521973
+ ],
+ [
+ "▁barrier",
+ -11.934405326843262
+ ],
+ [
+ "înd",
+ -11.934470176696777
+ ],
+ [
+ "▁ambition",
+ -11.93451976776123
+ ],
+ [
+ "asta",
+ -11.934550285339355
+ ],
+ [
+ "occupied",
+ -11.934747695922852
+ ],
+ [
+ "▁Gau",
+ -11.934774398803711
+ ],
+ [
+ "four",
+ -11.93481159210205
+ ],
+ [
+ "▁nap",
+ -11.934887886047363
+ ],
+ [
+ "iez",
+ -11.934922218322754
+ ],
+ [
+ "endra",
+ -11.935242652893066
+ ],
+ [
+ "gaben",
+ -11.935464859008789
+ ],
+ [
+ "▁Carol",
+ -11.935481071472168
+ ],
+ [
+ "▁Switzerland",
+ -11.935575485229492
+ ],
+ [
+ "▁Bond",
+ -11.935617446899414
+ ],
+ [
+ "▁crossing",
+ -11.935630798339844
+ ],
+ [
+ "▁Palace",
+ -11.9359769821167
+ ],
+ [
+ "NG",
+ -11.935986518859863
+ ],
+ [
+ "▁Budget",
+ -11.93622875213623
+ ],
+ [
+ "▁lid",
+ -11.936372756958008
+ ],
+ [
+ "bab",
+ -11.936393737792969
+ ],
+ [
+ "▁polish",
+ -11.936416625976562
+ ],
+ [
+ "▁herbs",
+ -11.93673038482666
+ ],
+ [
+ "▁dear",
+ -11.936747550964355
+ ],
+ [
+ "▁devrai",
+ -11.936846733093262
+ ],
+ [
+ "walk",
+ -11.936864852905273
+ ],
+ [
+ "▁humanity",
+ -11.936897277832031
+ ],
+ [
+ "▁tires",
+ -11.936978340148926
+ ],
+ [
+ "égal",
+ -11.936994552612305
+ ],
+ [
+ "▁bow",
+ -11.937032699584961
+ ],
+ [
+ "▁debris",
+ -11.937201499938965
+ ],
+ [
+ "▁keywords",
+ -11.937273025512695
+ ],
+ [
+ "irk",
+ -11.937345504760742
+ ],
+ [
+ "▁suspend",
+ -11.937360763549805
+ ],
+ [
+ "▁pourra",
+ -11.93738079071045
+ ],
+ [
+ "migran",
+ -11.937454223632812
+ ],
+ [
+ "thereby",
+ -11.937570571899414
+ ],
+ [
+ "▁Harris",
+ -11.937943458557129
+ ],
+ [
+ "ateurs",
+ -11.937956809997559
+ ],
+ [
+ "▁fal",
+ -11.938271522521973
+ ],
+ [
+ "alleged",
+ -11.938355445861816
+ ],
+ [
+ "noch",
+ -11.938494682312012
+ ],
+ [
+ "▁observation",
+ -11.938506126403809
+ ],
+ [
+ "▁București",
+ -11.93855094909668
+ ],
+ [
+ "▁SQL",
+ -11.938624382019043
+ ],
+ [
+ "▁Phase",
+ -11.938760757446289
+ ],
+ [
+ "▁adventures",
+ -11.93881607055664
+ ],
+ [
+ "▁Kol",
+ -11.938885688781738
+ ],
+ [
+ "▁professionnel",
+ -11.938916206359863
+ ],
+ [
+ "crit",
+ -11.939026832580566
+ ],
+ [
+ "LR",
+ -11.939313888549805
+ ],
+ [
+ "▁preview",
+ -11.939464569091797
+ ],
+ [
+ "▁highlighted",
+ -11.939942359924316
+ ],
+ [
+ "▁Stud",
+ -11.939949035644531
+ ],
+ [
+ "▁labour",
+ -11.939956665039062
+ ],
+ [
+ "MV",
+ -11.9399995803833
+ ],
+ [
+ "click",
+ -11.940049171447754
+ ],
+ [
+ "approche",
+ -11.94016170501709
+ ],
+ [
+ "tian",
+ -11.940183639526367
+ ],
+ [
+ "cité",
+ -11.940192222595215
+ ],
+ [
+ "▁Rain",
+ -11.94028377532959
+ ],
+ [
+ "typ",
+ -11.94032096862793
+ ],
+ [
+ "Usually",
+ -11.940435409545898
+ ],
+ [
+ "▁outlet",
+ -11.940513610839844
+ ],
+ [
+ "logging",
+ -11.940814018249512
+ ],
+ [
+ "▁Temperatur",
+ -11.940906524658203
+ ],
+ [
+ "▁Scottish",
+ -11.94090747833252
+ ],
+ [
+ "iga",
+ -11.940942764282227
+ ],
+ [
+ "▁glory",
+ -11.941086769104004
+ ],
+ [
+ "▁Rom",
+ -11.941242218017578
+ ],
+ [
+ "zeug",
+ -11.941337585449219
+ ],
+ [
+ "establishing",
+ -11.941339492797852
+ ],
+ [
+ "▁imaging",
+ -11.941926002502441
+ ],
+ [
+ "▁Beauty",
+ -11.942015647888184
+ ],
+ [
+ "igan",
+ -11.942042350769043
+ ],
+ [
+ "après",
+ -11.94224739074707
+ ],
+ [
+ "Adresse",
+ -11.942267417907715
+ ],
+ [
+ "cliff",
+ -11.942349433898926
+ ],
+ [
+ "▁unnecessary",
+ -11.943267822265625
+ ],
+ [
+ "▁slim",
+ -11.943324089050293
+ ],
+ [
+ "dir",
+ -11.943490982055664
+ ],
+ [
+ "▁leisure",
+ -11.943660736083984
+ ],
+ [
+ "▁principale",
+ -11.94368839263916
+ ],
+ [
+ "▁Viele",
+ -11.943770408630371
+ ],
+ [
+ "▁2007.",
+ -11.943802833557129
+ ],
+ [
+ "Hopefully",
+ -11.943829536437988
+ ],
+ [
+ "cola",
+ -11.943851470947266
+ ],
+ [
+ "▁Planet",
+ -11.943927764892578
+ ],
+ [
+ "▁orientation",
+ -11.943933486938477
+ ],
+ [
+ "▁angry",
+ -11.94419002532959
+ ],
+ [
+ "MIT",
+ -11.944234848022461
+ ],
+ [
+ "▁Kenya",
+ -11.944265365600586
+ ],
+ [
+ "▁bless",
+ -11.94435977935791
+ ],
+ [
+ "▁Fill",
+ -11.944524765014648
+ ],
+ [
+ "▁compar",
+ -11.944664001464844
+ ],
+ [
+ "▁curtain",
+ -11.94473934173584
+ ],
+ [
+ "ţei",
+ -11.944754600524902
+ ],
+ [
+ "▁Az",
+ -11.94482421875
+ ],
+ [
+ "▁Rang",
+ -11.944908142089844
+ ],
+ [
+ "▁dominant",
+ -11.944974899291992
+ ],
+ [
+ "race",
+ -11.944985389709473
+ ],
+ [
+ "▁Target",
+ -11.944987297058105
+ ],
+ [
+ "▁manually",
+ -11.944987297058105
+ ],
+ [
+ "objet",
+ -11.945024490356445
+ ],
+ [
+ "thrown",
+ -11.945131301879883
+ ],
+ [
+ "NF",
+ -11.945149421691895
+ ],
+ [
+ "durant",
+ -11.945185661315918
+ ],
+ [
+ "rect",
+ -11.945302963256836
+ ],
+ [
+ "▁Größe",
+ -11.945320129394531
+ ],
+ [
+ "VM",
+ -11.9453763961792
+ ],
+ [
+ "▁aprilie",
+ -11.945476531982422
+ ],
+ [
+ "▁Welche",
+ -11.945639610290527
+ ],
+ [
+ "▁verde",
+ -11.946157455444336
+ ],
+ [
+ "▁Portugal",
+ -11.946266174316406
+ ],
+ [
+ "▁algorithm",
+ -11.94627571105957
+ ],
+ [
+ "ăț",
+ -11.946328163146973
+ ],
+ [
+ "▁Grey",
+ -11.946371078491211
+ ],
+ [
+ "▁cleaned",
+ -11.94644832611084
+ ],
+ [
+ "▁modes",
+ -11.946463584899902
+ ],
+ [
+ "▁relaxation",
+ -11.946599006652832
+ ],
+ [
+ "mbr",
+ -11.946786880493164
+ ],
+ [
+ "étique",
+ -11.946821212768555
+ ],
+ [
+ "Her",
+ -11.946904182434082
+ ],
+ [
+ "▁beta",
+ -11.946952819824219
+ ],
+ [
+ "▁nobody",
+ -11.94699764251709
+ ],
+ [
+ "▁aplic",
+ -11.947060585021973
+ ],
+ [
+ "present",
+ -11.947080612182617
+ ],
+ [
+ "emis",
+ -11.947197914123535
+ ],
+ [
+ "éléments",
+ -11.947257995605469
+ ],
+ [
+ "▁lately",
+ -11.947303771972656
+ ],
+ [
+ "fab",
+ -11.94732666015625
+ ],
+ [
+ "▁aluminiu",
+ -11.947373390197754
+ ],
+ [
+ "▁vest",
+ -11.947524070739746
+ ],
+ [
+ "▁statue",
+ -11.947558403015137
+ ],
+ [
+ "▁publice",
+ -11.947586059570312
+ ],
+ [
+ "▁merchandise",
+ -11.9476900100708
+ ],
+ [
+ "▁relat",
+ -11.947810173034668
+ ],
+ [
+ "git",
+ -11.94796371459961
+ ],
+ [
+ "▁interne",
+ -11.948281288146973
+ ],
+ [
+ "▁Tokyo",
+ -11.948325157165527
+ ],
+ [
+ "chal",
+ -11.948348045349121
+ ],
+ [
+ "contacted",
+ -11.948430061340332
+ ],
+ [
+ "▁tras",
+ -11.948455810546875
+ ],
+ [
+ "▁Clinic",
+ -11.948626518249512
+ ],
+ [
+ "▁unbe",
+ -11.948633193969727
+ ],
+ [
+ "▁dumneavoastra",
+ -11.948798179626465
+ ],
+ [
+ "float",
+ -11.949078559875488
+ ],
+ [
+ "isson",
+ -11.94909381866455
+ ],
+ [
+ "▁vessel",
+ -11.949126243591309
+ ],
+ [
+ "attempting",
+ -11.949161529541016
+ ],
+ [
+ "▁doute",
+ -11.94918441772461
+ ],
+ [
+ "▁Leadership",
+ -11.949322700500488
+ ],
+ [
+ "▁sustain",
+ -11.94947338104248
+ ],
+ [
+ "▁textile",
+ -11.949666023254395
+ ],
+ [
+ "auer",
+ -11.949702262878418
+ ],
+ [
+ "▁90%",
+ -11.949899673461914
+ ],
+ [
+ "garten",
+ -11.949911117553711
+ ],
+ [
+ "▁adauga",
+ -11.949991226196289
+ ],
+ [
+ "▁Kil",
+ -11.950061798095703
+ ],
+ [
+ "▁troops",
+ -11.950420379638672
+ ],
+ [
+ "▁pale",
+ -11.950568199157715
+ ],
+ [
+ "host",
+ -11.950743675231934
+ ],
+ [
+ "▁cry",
+ -11.950757026672363
+ ],
+ [
+ "▁Alb",
+ -11.950793266296387
+ ],
+ [
+ "▁Brad",
+ -11.95089340209961
+ ],
+ [
+ "▁bicycle",
+ -11.951054573059082
+ ],
+ [
+ "▁24/7",
+ -11.951217651367188
+ ],
+ [
+ "▁с",
+ -11.951228141784668
+ ],
+ [
+ "▁stimul",
+ -11.951401710510254
+ ],
+ [
+ "gler",
+ -11.951445579528809
+ ],
+ [
+ "▁notwendig",
+ -11.951496124267578
+ ],
+ [
+ "▁cousin",
+ -11.95158863067627
+ ],
+ [
+ "cheie",
+ -11.951600074768066
+ ],
+ [
+ "hay",
+ -11.951751708984375
+ ],
+ [
+ "▁rezolv",
+ -11.952134132385254
+ ],
+ [
+ "▁THIS",
+ -11.952143669128418
+ ],
+ [
+ "ordre",
+ -11.952157974243164
+ ],
+ [
+ "iști",
+ -11.952173233032227
+ ],
+ [
+ "▁conclude",
+ -11.952310562133789
+ ],
+ [
+ "▁Lage",
+ -11.952327728271484
+ ],
+ [
+ "▁Entertainment",
+ -11.952454566955566
+ ],
+ [
+ "▁valued",
+ -11.952478408813477
+ ],
+ [
+ "ktion",
+ -11.95253849029541
+ ],
+ [
+ "▁priorities",
+ -11.95268440246582
+ ],
+ [
+ "▁1986",
+ -11.952770233154297
+ ],
+ [
+ "▁fatal",
+ -11.952934265136719
+ ],
+ [
+ "▁accurately",
+ -11.952988624572754
+ ],
+ [
+ "▁1987",
+ -11.953022956848145
+ ],
+ [
+ "▁folk",
+ -11.953073501586914
+ ],
+ [
+ "7)",
+ -11.953163146972656
+ ],
+ [
+ "führer",
+ -11.95360279083252
+ ],
+ [
+ "▁knot",
+ -11.953612327575684
+ ],
+ [
+ "haltung",
+ -11.953720092773438
+ ],
+ [
+ "▁Charlie",
+ -11.953733444213867
+ ],
+ [
+ "âge",
+ -11.95376205444336
+ ],
+ [
+ "▁threshold",
+ -11.954041481018066
+ ],
+ [
+ "▁assault",
+ -11.954130172729492
+ ],
+ [
+ "▁meist",
+ -11.954141616821289
+ ],
+ [
+ "bine",
+ -11.954155921936035
+ ],
+ [
+ "surprisingly",
+ -11.954171180725098
+ ],
+ [
+ "▁Protect",
+ -11.954180717468262
+ ],
+ [
+ "▁Hack",
+ -11.954258918762207
+ ],
+ [
+ "▁Quant",
+ -11.954537391662598
+ ],
+ [
+ "▁Cet",
+ -11.954782485961914
+ ],
+ [
+ "▁convinced",
+ -11.95481014251709
+ ],
+ [
+ "▁muncă",
+ -11.955033302307129
+ ],
+ [
+ "dging",
+ -11.955066680908203
+ ],
+ [
+ "▁Millionen",
+ -11.955129623413086
+ ],
+ [
+ "zahlung",
+ -11.955148696899414
+ ],
+ [
+ "▁anticipated",
+ -11.955192565917969
+ ],
+ [
+ "▁brass",
+ -11.9552001953125
+ ],
+ [
+ "KO",
+ -11.955244064331055
+ ],
+ [
+ "▁culori",
+ -11.955286979675293
+ ],
+ [
+ "▁Aero",
+ -11.955326080322266
+ ],
+ [
+ "▁intermediu",
+ -11.955373764038086
+ ],
+ [
+ "▁Philippines",
+ -11.955381393432617
+ ],
+ [
+ "▁jury",
+ -11.955387115478516
+ ],
+ [
+ "▁Funktion",
+ -11.95569896697998
+ ],
+ [
+ "▁probe",
+ -11.955704689025879
+ ],
+ [
+ "TL",
+ -11.955748558044434
+ ],
+ [
+ "1.0",
+ -11.955804824829102
+ ],
+ [
+ "ELL",
+ -11.95581340789795
+ ],
+ [
+ "She",
+ -11.956001281738281
+ ],
+ [
+ "▁Blood",
+ -11.956073760986328
+ ],
+ [
+ "▁Dean",
+ -11.956111907958984
+ ],
+ [
+ "▁scène",
+ -11.9561185836792
+ ],
+ [
+ "volu",
+ -11.95621395111084
+ ],
+ [
+ "▁Epi",
+ -11.95621395111084
+ ],
+ [
+ "▁séjour",
+ -11.95627498626709
+ ],
+ [
+ "▁Smartphone",
+ -11.956306457519531
+ ],
+ [
+ "▁fired",
+ -11.956357955932617
+ ],
+ [
+ "beat",
+ -11.95650577545166
+ ],
+ [
+ "▁pockets",
+ -11.956506729125977
+ ],
+ [
+ "▁serviciu",
+ -11.956624031066895
+ ],
+ [
+ "▁affairs",
+ -11.95678424835205
+ ],
+ [
+ "▁Ry",
+ -11.956842422485352
+ ],
+ [
+ "▁Stadium",
+ -11.956954956054688
+ ],
+ [
+ "▁snacks",
+ -11.957182884216309
+ ],
+ [
+ "▁efectu",
+ -11.957221031188965
+ ],
+ [
+ "▁Richtung",
+ -11.957273483276367
+ ],
+ [
+ "▁dresses",
+ -11.957352638244629
+ ],
+ [
+ "▁Medien",
+ -11.95744800567627
+ ],
+ [
+ "writer",
+ -11.95759105682373
+ ],
+ [
+ "changing",
+ -11.957655906677246
+ ],
+ [
+ "▁supportive",
+ -11.957849502563477
+ ],
+ [
+ "▁beneath",
+ -11.957873344421387
+ ],
+ [
+ "paid",
+ -11.958078384399414
+ ],
+ [
+ "▁customize",
+ -11.958155632019043
+ ],
+ [
+ "▁Ferr",
+ -11.958187103271484
+ ],
+ [
+ "reaches",
+ -11.958338737487793
+ ],
+ [
+ "arma",
+ -11.958401679992676
+ ],
+ [
+ "ción",
+ -11.958598136901855
+ ],
+ [
+ "▁elderly",
+ -11.959243774414062
+ ],
+ [
+ "▁modification",
+ -11.95934009552002
+ ],
+ [
+ "▁perfection",
+ -11.959381103515625
+ ],
+ [
+ "▁Allow",
+ -11.959492683410645
+ ],
+ [
+ "▁belonging",
+ -11.959542274475098
+ ],
+ [
+ "▁compound",
+ -11.959589004516602
+ ],
+ [
+ "▁Results",
+ -11.959681510925293
+ ],
+ [
+ "▁astăzi",
+ -11.959793090820312
+ ],
+ [
+ "▁Liber",
+ -11.959818840026855
+ ],
+ [
+ "jor",
+ -11.959850311279297
+ ],
+ [
+ "▁Nin",
+ -11.959980964660645
+ ],
+ [
+ "▁lumina",
+ -11.959992408752441
+ ],
+ [
+ "▁130",
+ -11.960073471069336
+ ],
+ [
+ "▁Platform",
+ -11.960121154785156
+ ],
+ [
+ "▁SMS",
+ -11.960221290588379
+ ],
+ [
+ "▁medic",
+ -11.96024227142334
+ ],
+ [
+ "hör",
+ -11.960315704345703
+ ],
+ [
+ "▁Kas",
+ -11.96038818359375
+ ],
+ [
+ "▁tomato",
+ -11.960403442382812
+ ],
+ [
+ "▁logiciel",
+ -11.960505485534668
+ ],
+ [
+ "php",
+ -11.960654258728027
+ ],
+ [
+ "▁premises",
+ -11.96071720123291
+ ],
+ [
+ "▁Communication",
+ -11.96072769165039
+ ],
+ [
+ "▁reprezintă",
+ -11.960762023925781
+ ],
+ [
+ "▁Partners",
+ -11.960866928100586
+ ],
+ [
+ "▁RV",
+ -11.961090087890625
+ ],
+ [
+ "▁pants",
+ -11.961197853088379
+ ],
+ [
+ "▁envie",
+ -11.961256980895996
+ ],
+ [
+ "▁commerce",
+ -11.961263656616211
+ ],
+ [
+ "▁tears",
+ -11.961298942565918
+ ],
+ [
+ "▁cooler",
+ -11.961494445800781
+ ],
+ [
+ "strand",
+ -11.961556434631348
+ ],
+ [
+ "▁Gil",
+ -11.961588859558105
+ ],
+ [
+ "▁référence",
+ -11.961641311645508
+ ],
+ [
+ "▁electronics",
+ -11.961681365966797
+ ],
+ [
+ "exposition",
+ -11.961700439453125
+ ],
+ [
+ "▁Caribbean",
+ -11.96171760559082
+ ],
+ [
+ "▁compelling",
+ -11.96171760559082
+ ],
+ [
+ "luci",
+ -11.961723327636719
+ ],
+ [
+ "▁Brooklyn",
+ -11.961892127990723
+ ],
+ [
+ "▁Thai",
+ -11.961950302124023
+ ],
+ [
+ "dler",
+ -11.96198844909668
+ ],
+ [
+ "▁supra",
+ -11.962016105651855
+ ],
+ [
+ "centered",
+ -11.962026596069336
+ ],
+ [
+ "▁metro",
+ -11.962081909179688
+ ],
+ [
+ "▁03",
+ -11.962299346923828
+ ],
+ [
+ "▁enrich",
+ -11.962437629699707
+ ],
+ [
+ "▁adevarat",
+ -11.962594985961914
+ ],
+ [
+ "5000",
+ -11.962961196899414
+ ],
+ [
+ "▁bell",
+ -11.96297550201416
+ ],
+ [
+ "▁sine",
+ -11.962996482849121
+ ],
+ [
+ "▁appealing",
+ -11.963088989257812
+ ],
+ [
+ "clam",
+ -11.963116645812988
+ ],
+ [
+ "▁vorhanden",
+ -11.963165283203125
+ ],
+ [
+ "▁pickup",
+ -11.963268280029297
+ ],
+ [
+ "▁Alaska",
+ -11.963269233703613
+ ],
+ [
+ "▁Nacht",
+ -11.963300704956055
+ ],
+ [
+ "borough",
+ -11.9633207321167
+ ],
+ [
+ "▁Blanc",
+ -11.96340274810791
+ ],
+ [
+ "▁apare",
+ -11.963616371154785
+ ],
+ [
+ "▁Works",
+ -11.963798522949219
+ ],
+ [
+ "mettent",
+ -11.963801383972168
+ ],
+ [
+ "atter",
+ -11.96389389038086
+ ],
+ [
+ "terra",
+ -11.963946342468262
+ ],
+ [
+ "▁Bit",
+ -11.964105606079102
+ ],
+ [
+ "RL",
+ -11.964131355285645
+ ],
+ [
+ "▁Wander",
+ -11.964262962341309
+ ],
+ [
+ "▁Hawk",
+ -11.964595794677734
+ ],
+ [
+ "▁Probleme",
+ -11.964665412902832
+ ],
+ [
+ "regel",
+ -11.964729309082031
+ ],
+ [
+ "hne",
+ -11.964739799499512
+ ],
+ [
+ "fass",
+ -11.96486759185791
+ ],
+ [
+ "▁Andy",
+ -11.965014457702637
+ ],
+ [
+ "▁befinde",
+ -11.965179443359375
+ ],
+ [
+ "boo",
+ -11.965265274047852
+ ],
+ [
+ "▁connectivity",
+ -11.965304374694824
+ ],
+ [
+ "▁spielt",
+ -11.965418815612793
+ ],
+ [
+ "zweiten",
+ -11.96547794342041
+ ],
+ [
+ "ţilor",
+ -11.965526580810547
+ ],
+ [
+ "▁confi",
+ -11.96561336517334
+ ],
+ [
+ "▁schlecht",
+ -11.965773582458496
+ ],
+ [
+ "▁Beginn",
+ -11.96581745147705
+ ],
+ [
+ "▁floating",
+ -11.965903282165527
+ ],
+ [
+ "nimmt",
+ -11.966071128845215
+ ],
+ [
+ "▁arbeiten",
+ -11.96611213684082
+ ],
+ [
+ "pillar",
+ -11.966131210327148
+ ],
+ [
+ "sterreich",
+ -11.966347694396973
+ ],
+ [
+ "▁Schule",
+ -11.966446876525879
+ ],
+ [
+ "▁durée",
+ -11.966521263122559
+ ],
+ [
+ "▁honestly",
+ -11.96653938293457
+ ],
+ [
+ "▁acel",
+ -11.9666166305542
+ ],
+ [
+ "▁Prozess",
+ -11.96662425994873
+ ],
+ [
+ "Min",
+ -11.966629028320312
+ ],
+ [
+ "enii",
+ -11.966632843017578
+ ],
+ [
+ "DAY",
+ -11.966758728027344
+ ],
+ [
+ "▁Blo",
+ -11.966806411743164
+ ],
+ [
+ "▁bolt",
+ -11.966946601867676
+ ],
+ [
+ "sicher",
+ -11.967070579528809
+ ],
+ [
+ "▁17,",
+ -11.967122077941895
+ ],
+ [
+ "▁anchor",
+ -11.967215538024902
+ ],
+ [
+ "▁consistency",
+ -11.967241287231445
+ ],
+ [
+ "▁relatives",
+ -11.967263221740723
+ ],
+ [
+ "▁lac",
+ -11.967385292053223
+ ],
+ [
+ "105",
+ -11.967432975769043
+ ],
+ [
+ "▁Craig",
+ -11.967534065246582
+ ],
+ [
+ "▁mandate",
+ -11.967598915100098
+ ],
+ [
+ "▁bedeutet",
+ -11.967674255371094
+ ],
+ [
+ "▁Soviet",
+ -11.967680931091309
+ ],
+ [
+ "▁arguments",
+ -11.967938423156738
+ ],
+ [
+ "▁Gebäude",
+ -11.967997550964355
+ ],
+ [
+ "▁Parliament",
+ -11.968005180358887
+ ],
+ [
+ "▁Kha",
+ -11.968087196350098
+ ],
+ [
+ "nica",
+ -11.968130111694336
+ ],
+ [
+ "▁Amazing",
+ -11.968162536621094
+ ],
+ [
+ "gründe",
+ -11.968179702758789
+ ],
+ [
+ "▁Ott",
+ -11.968269348144531
+ ],
+ [
+ "Exp",
+ -11.968314170837402
+ ],
+ [
+ "▁ianuarie",
+ -11.96848201751709
+ ],
+ [
+ "riot",
+ -11.968571662902832
+ ],
+ [
+ "▁futur",
+ -11.968626976013184
+ ],
+ [
+ "▁Honda",
+ -11.968647956848145
+ ],
+ [
+ "!!!!",
+ -11.96865177154541
+ ],
+ [
+ "▁citit",
+ -11.968689918518066
+ ],
+ [
+ "▁22,",
+ -11.968708992004395
+ ],
+ [
+ "țional",
+ -11.968711853027344
+ ],
+ [
+ "▁lovers",
+ -11.968732833862305
+ ],
+ [
+ "▁Current",
+ -11.968835830688477
+ ],
+ [
+ "▁drone",
+ -11.96927261352539
+ ],
+ [
+ "▁promising",
+ -11.969335556030273
+ ],
+ [
+ "devoted",
+ -11.969443321228027
+ ],
+ [
+ "▁Born",
+ -11.969520568847656
+ ],
+ [
+ "▁viitor",
+ -11.969589233398438
+ ],
+ [
+ "▁ritual",
+ -11.969614028930664
+ ],
+ [
+ "▁Guard",
+ -11.969681739807129
+ ],
+ [
+ "09.",
+ -11.969828605651855
+ ],
+ [
+ "▁Py",
+ -11.970260620117188
+ ],
+ [
+ "▁finds",
+ -11.970380783081055
+ ],
+ [
+ "▁boli",
+ -11.970394134521484
+ ],
+ [
+ "▁Mitglieder",
+ -11.970697402954102
+ ],
+ [
+ "ogni",
+ -11.97107982635498
+ ],
+ [
+ "▁stones",
+ -11.97118854522705
+ ],
+ [
+ "rox",
+ -11.971210479736328
+ ],
+ [
+ "▁dock",
+ -11.971390724182129
+ ],
+ [
+ "▁onion",
+ -11.97144889831543
+ ],
+ [
+ "▁classified",
+ -11.971538543701172
+ ],
+ [
+ "big",
+ -11.971833229064941
+ ],
+ [
+ "RG",
+ -11.971857070922852
+ ],
+ [
+ "influenced",
+ -11.971955299377441
+ ],
+ [
+ "▁sudden",
+ -11.971988677978516
+ ],
+ [
+ "▁ample",
+ -11.97204303741455
+ ],
+ [
+ "án",
+ -11.972095489501953
+ ],
+ [
+ "▁ornament",
+ -11.972122192382812
+ ],
+ [
+ "datele",
+ -11.972227096557617
+ ],
+ [
+ "▁Dad",
+ -11.97225284576416
+ ],
+ [
+ "BER",
+ -11.972278594970703
+ ],
+ [
+ "gerecht",
+ -11.972380638122559
+ ],
+ [
+ "kett",
+ -11.972536087036133
+ ],
+ [
+ "▁Antonio",
+ -11.972572326660156
+ ],
+ [
+ "Nu",
+ -11.972834587097168
+ ],
+ [
+ "dium",
+ -11.97284984588623
+ ],
+ [
+ "CAD",
+ -11.972850799560547
+ ],
+ [
+ "▁bundle",
+ -11.972916603088379
+ ],
+ [
+ "▁Vari",
+ -11.97301197052002
+ ],
+ [
+ "▁thrive",
+ -11.973020553588867
+ ],
+ [
+ "▁Seminar",
+ -11.973071098327637
+ ],
+ [
+ "wire",
+ -11.973084449768066
+ ],
+ [
+ "▁contributing",
+ -11.973114967346191
+ ],
+ [
+ "▁Bour",
+ -11.97320556640625
+ ],
+ [
+ "▁dori",
+ -11.973206520080566
+ ],
+ [
+ "▁packing",
+ -11.97343921661377
+ ],
+ [
+ "▁colleges",
+ -11.973459243774414
+ ],
+ [
+ "▁garbage",
+ -11.97366714477539
+ ],
+ [
+ "▁vector",
+ -11.973837852478027
+ ],
+ [
+ "▁suggestion",
+ -11.973897933959961
+ ],
+ [
+ "borne",
+ -11.973904609680176
+ ],
+ [
+ "▁Listen",
+ -11.973938941955566
+ ],
+ [
+ "▁Prix",
+ -11.973957061767578
+ ],
+ [
+ "viennent",
+ -11.974162101745605
+ ],
+ [
+ "insbesondere",
+ -11.97426700592041
+ ],
+ [
+ "▁fonctionne",
+ -11.974435806274414
+ ],
+ [
+ "▁mainstream",
+ -11.974485397338867
+ ],
+ [
+ "▁merci",
+ -11.974574089050293
+ ],
+ [
+ "oko",
+ -11.97460651397705
+ ],
+ [
+ "▁Commerce",
+ -11.97493839263916
+ ],
+ [
+ "▁droits",
+ -11.975115776062012
+ ],
+ [
+ "▁muzica",
+ -11.975141525268555
+ ],
+ [
+ "▁profesor",
+ -11.9751558303833
+ ],
+ [
+ "▁epic",
+ -11.97518253326416
+ ],
+ [
+ "▁intuitive",
+ -11.975186347961426
+ ],
+ [
+ "▁aggregate",
+ -11.975223541259766
+ ],
+ [
+ "▁vaccine",
+ -11.97529411315918
+ ],
+ [
+ "▁dank",
+ -11.975459098815918
+ ],
+ [
+ "▁situ",
+ -11.975578308105469
+ ],
+ [
+ "▁Cand",
+ -11.975593566894531
+ ],
+ [
+ "▁Ganz",
+ -11.97562313079834
+ ],
+ [
+ "▁Crystal",
+ -11.97578239440918
+ ],
+ [
+ "▁discretion",
+ -11.975825309753418
+ ],
+ [
+ "mug",
+ -11.975997924804688
+ ],
+ [
+ "▁anzu",
+ -11.976144790649414
+ ],
+ [
+ "▁cement",
+ -11.97616958618164
+ ],
+ [
+ "▁priest",
+ -11.97625732421875
+ ],
+ [
+ "▁rejected",
+ -11.976298332214355
+ ],
+ [
+ "▁Summit",
+ -11.976325988769531
+ ],
+ [
+ "▁Sara",
+ -11.976424217224121
+ ],
+ [
+ "▁palette",
+ -11.976527214050293
+ ],
+ [
+ "▁continuare",
+ -11.976569175720215
+ ],
+ [
+ "uge",
+ -11.976676940917969
+ ],
+ [
+ "ryl",
+ -11.976844787597656
+ ],
+ [
+ "▁Solid",
+ -11.977142333984375
+ ],
+ [
+ "▁meilleure",
+ -11.977177619934082
+ ],
+ [
+ "▁Tennessee",
+ -11.977248191833496
+ ],
+ [
+ "rail",
+ -11.977326393127441
+ ],
+ [
+ "▁attributes",
+ -11.9773530960083
+ ],
+ [
+ "▁vessels",
+ -11.977840423583984
+ ],
+ [
+ "cylinder",
+ -11.977900505065918
+ ],
+ [
+ "▁parfait",
+ -11.977916717529297
+ ],
+ [
+ "abb",
+ -11.97801399230957
+ ],
+ [
+ "▁Julie",
+ -11.97806167602539
+ ],
+ [
+ "▁pièces",
+ -11.978120803833008
+ ],
+ [
+ "▁proiecte",
+ -11.978142738342285
+ ],
+ [
+ "médi",
+ -11.978273391723633
+ ],
+ [
+ "▁décembre",
+ -11.9783935546875
+ ],
+ [
+ "Per",
+ -11.97841739654541
+ ],
+ [
+ "1/",
+ -11.978520393371582
+ ],
+ [
+ "regulated",
+ -11.978601455688477
+ ],
+ [
+ "▁Dy",
+ -11.978633880615234
+ ],
+ [
+ "▁23,",
+ -11.978694915771484
+ ],
+ [
+ "beck",
+ -11.978763580322266
+ ],
+ [
+ "tură",
+ -11.97885513305664
+ ],
+ [
+ "▁Chiar",
+ -11.978931427001953
+ ],
+ [
+ "▁isolated",
+ -11.979012489318848
+ ],
+ [
+ "▁kennen",
+ -11.979259490966797
+ ],
+ [
+ "Du",
+ -11.979260444641113
+ ],
+ [
+ "reflected",
+ -11.979482650756836
+ ],
+ [
+ "▁belong",
+ -11.979571342468262
+ ],
+ [
+ "▁welcomed",
+ -11.97969913482666
+ ],
+ [
+ "▁Rate",
+ -11.979776382446289
+ ],
+ [
+ "prestigious",
+ -11.979859352111816
+ ],
+ [
+ "▁1/4",
+ -11.979930877685547
+ ],
+ [
+ "▁distinction",
+ -11.979966163635254
+ ],
+ [
+ "▁boring",
+ -11.980001449584961
+ ],
+ [
+ "▁booked",
+ -11.980369567871094
+ ],
+ [
+ "▁citizen",
+ -11.980441093444824
+ ],
+ [
+ "▁comprises",
+ -11.980498313903809
+ ],
+ [
+ "▁aufge",
+ -11.98051929473877
+ ],
+ [
+ "GL",
+ -11.980566024780273
+ ],
+ [
+ "▁nearest",
+ -11.980616569519043
+ ],
+ [
+ "▁printr",
+ -11.980692863464355
+ ],
+ [
+ "▁département",
+ -11.981318473815918
+ ],
+ [
+ "▁planner",
+ -11.981510162353516
+ ],
+ [
+ "▁Rai",
+ -11.981817245483398
+ ],
+ [
+ "▁Broad",
+ -11.981934547424316
+ ],
+ [
+ "▁pastor",
+ -11.981947898864746
+ ],
+ [
+ "▁reservation",
+ -11.982243537902832
+ ],
+ [
+ "▁decembrie",
+ -11.982315063476562
+ ],
+ [
+ "▁suficient",
+ -11.982501983642578
+ ],
+ [
+ "geld",
+ -11.982560157775879
+ ],
+ [
+ "training",
+ -11.982620239257812
+ ],
+ [
+ "deshalb",
+ -11.982634544372559
+ ],
+ [
+ "▁chaud",
+ -11.982651710510254
+ ],
+ [
+ "Cor",
+ -11.982662200927734
+ ],
+ [
+ "▁Grade",
+ -11.982769966125488
+ ],
+ [
+ "▁faţă",
+ -11.982809066772461
+ ],
+ [
+ "story",
+ -11.982839584350586
+ ],
+ [
+ "gericht",
+ -11.98286247253418
+ ],
+ [
+ "▁Got",
+ -11.982954025268555
+ ],
+ [
+ "particulièrement",
+ -11.982976913452148
+ ],
+ [
+ "▁bump",
+ -11.983051300048828
+ ],
+ [
+ "▁fatigue",
+ -11.983160018920898
+ ],
+ [
+ "Activ",
+ -11.983250617980957
+ ],
+ [
+ "▁numéro",
+ -11.983302116394043
+ ],
+ [
+ "▁stranger",
+ -11.983312606811523
+ ],
+ [
+ "▁Skin",
+ -11.983327865600586
+ ],
+ [
+ "add",
+ -11.98344898223877
+ ],
+ [
+ "Ainsi",
+ -11.98357105255127
+ ],
+ [
+ "▁assists",
+ -11.983684539794922
+ ],
+ [
+ "▁zusätzlich",
+ -11.983943939208984
+ ],
+ [
+ "▁vede",
+ -11.983979225158691
+ ],
+ [
+ "RON",
+ -11.984108924865723
+ ],
+ [
+ "▁seemingly",
+ -11.984126091003418
+ ],
+ [
+ "▁NU",
+ -11.98417854309082
+ ],
+ [
+ "geb",
+ -11.984273910522461
+ ],
+ [
+ "▁Release",
+ -11.984353065490723
+ ],
+ [
+ "▁throwing",
+ -11.984427452087402
+ ],
+ [
+ "▁Alabama",
+ -11.984447479248047
+ ],
+ [
+ "▁Something",
+ -11.984590530395508
+ ],
+ [
+ "▁Cuba",
+ -11.98464584350586
+ ],
+ [
+ "▁Verbindung",
+ -11.984649658203125
+ ],
+ [
+ "▁Cir",
+ -11.984654426574707
+ ],
+ [
+ "your",
+ -11.984713554382324
+ ],
+ [
+ "-13",
+ -11.984748840332031
+ ],
+ [
+ "▁Delta",
+ -11.984801292419434
+ ],
+ [
+ "▁Twin",
+ -11.98504638671875
+ ],
+ [
+ "▁governance",
+ -11.985156059265137
+ ],
+ [
+ "▁groom",
+ -11.985310554504395
+ ],
+ [
+ "▁conception",
+ -11.98533821105957
+ ],
+ [
+ "▁governor",
+ -11.985383033752441
+ ],
+ [
+ "▁Spar",
+ -11.985416412353516
+ ],
+ [
+ "▁coastal",
+ -11.985652923583984
+ ],
+ [
+ "▁Seven",
+ -11.985856056213379
+ ],
+ [
+ "▁inclusive",
+ -11.986002922058105
+ ],
+ [
+ "cili",
+ -11.986035346984863
+ ],
+ [
+ "▁Ridge",
+ -11.986100196838379
+ ],
+ [
+ "teller",
+ -11.986224174499512
+ ],
+ [
+ "▁Kin",
+ -11.986247062683105
+ ],
+ [
+ "leiter",
+ -11.986279487609863
+ ],
+ [
+ "stern",
+ -11.986364364624023
+ ],
+ [
+ "change",
+ -11.986404418945312
+ ],
+ [
+ "▁presidential",
+ -11.986433982849121
+ ],
+ [
+ "▁composer",
+ -11.986544609069824
+ ],
+ [
+ "Stu",
+ -11.986560821533203
+ ],
+ [
+ "▁Frankfurt",
+ -11.986584663391113
+ ],
+ [
+ "prä",
+ -11.986639976501465
+ ],
+ [
+ "▁Ideal",
+ -11.986644744873047
+ ],
+ [
+ "▁linear",
+ -11.986857414245605
+ ],
+ [
+ "▁bloom",
+ -11.986879348754883
+ ],
+ [
+ "▁grades",
+ -11.986881256103516
+ ],
+ [
+ "mettant",
+ -11.98692512512207
+ ],
+ [
+ "▁finishes",
+ -11.986952781677246
+ ],
+ [
+ "holz",
+ -11.987086296081543
+ ],
+ [
+ "▁dirty",
+ -11.987317085266113
+ ],
+ [
+ "▁Roh",
+ -11.987386703491211
+ ],
+ [
+ "▁Praxis",
+ -11.987408638000488
+ ],
+ [
+ "tempo",
+ -11.987433433532715
+ ],
+ [
+ "▁attempted",
+ -11.987433433532715
+ ],
+ [
+ "▁primar",
+ -11.987434387207031
+ ],
+ [
+ "▁pomp",
+ -11.987528800964355
+ ],
+ [
+ "▁tolle",
+ -11.987614631652832
+ ],
+ [
+ "▁adres",
+ -11.988011360168457
+ ],
+ [
+ "▁Between",
+ -11.988066673278809
+ ],
+ [
+ "▁ruin",
+ -11.988432884216309
+ ],
+ [
+ "▁matériel",
+ -11.988561630249023
+ ],
+ [
+ "MER",
+ -11.988913536071777
+ ],
+ [
+ "Nevertheless",
+ -11.989055633544922
+ ],
+ [
+ "▁corruption",
+ -11.989119529724121
+ ],
+ [
+ "spire",
+ -11.989180564880371
+ ],
+ [
+ "▁mou",
+ -11.989208221435547
+ ],
+ [
+ "ROM",
+ -11.989278793334961
+ ],
+ [
+ "▁underground",
+ -11.98935604095459
+ ],
+ [
+ "▁relativ",
+ -11.989389419555664
+ ],
+ [
+ "waited",
+ -11.989462852478027
+ ],
+ [
+ "▁speeds",
+ -11.989468574523926
+ ],
+ [
+ "▁adjusted",
+ -11.989486694335938
+ ],
+ [
+ "▁Flat",
+ -11.989514350891113
+ ],
+ [
+ "UND",
+ -11.98965835571289
+ ],
+ [
+ "▁individuelle",
+ -11.989744186401367
+ ],
+ [
+ "▁anybody",
+ -11.98978042602539
+ ],
+ [
+ "EO",
+ -11.989790916442871
+ ],
+ [
+ "->",
+ -11.989791870117188
+ ],
+ [
+ "▁Spend",
+ -11.989876747131348
+ ],
+ [
+ "aktion",
+ -11.990011215209961
+ ],
+ [
+ "édit",
+ -11.99006462097168
+ ],
+ [
+ "▁quest",
+ -11.990078926086426
+ ],
+ [
+ "rind",
+ -11.990541458129883
+ ],
+ [
+ "▁mediu",
+ -11.99057388305664
+ ],
+ [
+ "▁barriers",
+ -11.99062442779541
+ ],
+ [
+ "▁répondre",
+ -11.990633010864258
+ ],
+ [
+ "▁novembre",
+ -11.990708351135254
+ ],
+ [
+ "▁champ",
+ -11.990736961364746
+ ],
+ [
+ "saw",
+ -11.990757942199707
+ ],
+ [
+ "▁fed",
+ -11.990804672241211
+ ],
+ [
+ "▁favorites",
+ -11.990939140319824
+ ],
+ [
+ "▁shield",
+ -11.991055488586426
+ ],
+ [
+ "▁Wide",
+ -11.991146087646484
+ ],
+ [
+ "▁problema",
+ -11.991445541381836
+ ],
+ [
+ "▁Asta",
+ -11.991525650024414
+ ],
+ [
+ "▁refreshing",
+ -11.99168872833252
+ ],
+ [
+ "hey",
+ -11.991692543029785
+ ],
+ [
+ "obtaining",
+ -11.991788864135742
+ ],
+ [
+ "▁parler",
+ -11.992072105407715
+ ],
+ [
+ "▁Cele",
+ -11.992134094238281
+ ],
+ [
+ "frage",
+ -11.992136001586914
+ ],
+ [
+ "écran",
+ -11.992324829101562
+ ],
+ [
+ "▁cleared",
+ -11.992448806762695
+ ],
+ [
+ "zehn",
+ -11.992594718933105
+ ],
+ [
+ "parmi",
+ -11.992647171020508
+ ],
+ [
+ "änder",
+ -11.992691993713379
+ ],
+ [
+ "▁Defense",
+ -11.992693901062012
+ ],
+ [
+ "tatea",
+ -11.992696762084961
+ ],
+ [
+ "▁reasonably",
+ -11.992939949035645
+ ],
+ [
+ "▁Idee",
+ -11.992985725402832
+ ],
+ [
+ "nehm",
+ -11.993000030517578
+ ],
+ [
+ "technologie",
+ -11.993020057678223
+ ],
+ [
+ "atura",
+ -11.993048667907715
+ ],
+ [
+ "▁slope",
+ -11.993332862854004
+ ],
+ [
+ "Hence",
+ -11.993351936340332
+ ],
+ [
+ "▁40%",
+ -11.993391990661621
+ ],
+ [
+ "▁jewe",
+ -11.993448257446289
+ ],
+ [
+ "▁queries",
+ -11.993470191955566
+ ],
+ [
+ "▁$8",
+ -11.994096755981445
+ ],
+ [
+ "▁Parker",
+ -11.994107246398926
+ ],
+ [
+ "▁publique",
+ -11.994488716125488
+ ],
+ [
+ "quant",
+ -11.994529724121094
+ ],
+ [
+ "issue",
+ -11.994690895080566
+ ],
+ [
+ "▁Cleveland",
+ -11.994847297668457
+ ],
+ [
+ "4,000",
+ -11.995071411132812
+ ],
+ [
+ "IDE",
+ -11.995145797729492
+ ],
+ [
+ "▁Barbara",
+ -11.995233535766602
+ ],
+ [
+ "udge",
+ -11.995477676391602
+ ],
+ [
+ "corn",
+ -11.99554443359375
+ ],
+ [
+ "veți",
+ -11.995588302612305
+ ],
+ [
+ "▁proteins",
+ -11.995707511901855
+ ],
+ [
+ "▁trăi",
+ -11.995793342590332
+ ],
+ [
+ "▁mijloc",
+ -11.995842933654785
+ ],
+ [
+ "logie",
+ -11.995884895324707
+ ],
+ [
+ "▁Walter",
+ -11.995884895324707
+ ],
+ [
+ "heißt",
+ -11.99593448638916
+ ],
+ [
+ "search",
+ -11.995946884155273
+ ],
+ [
+ "▁hochwertige",
+ -11.996010780334473
+ ],
+ [
+ "▁încerc",
+ -11.996014595031738
+ ],
+ [
+ "▁administrator",
+ -11.99608039855957
+ ],
+ [
+ "tension",
+ -11.996133804321289
+ ],
+ [
+ "▁homemade",
+ -11.996438026428223
+ ],
+ [
+ "▁$20",
+ -11.99651050567627
+ ],
+ [
+ "▁leben",
+ -11.996662139892578
+ ],
+ [
+ "netz",
+ -11.996665954589844
+ ],
+ [
+ "▁intensity",
+ -11.996882438659668
+ ],
+ [
+ "▁clever",
+ -11.996891975402832
+ ],
+ [
+ "▁installer",
+ -11.996999740600586
+ ],
+ [
+ "▁Wand",
+ -11.997087478637695
+ ],
+ [
+ "meister",
+ -11.997130393981934
+ ],
+ [
+ "ziel",
+ -11.99744701385498
+ ],
+ [
+ "▁architect",
+ -11.99748706817627
+ ],
+ [
+ "▁crede",
+ -11.997512817382812
+ ],
+ [
+ "▁Sleep",
+ -11.997675895690918
+ ],
+ [
+ "▁demonstr",
+ -11.997745513916016
+ ],
+ [
+ "cake",
+ -11.997781753540039
+ ],
+ [
+ "▁Cheap",
+ -11.997783660888672
+ ],
+ [
+ "pool",
+ -11.9979829788208
+ ],
+ [
+ "▁gadget",
+ -11.998004913330078
+ ],
+ [
+ "▁Anbieter",
+ -11.998005867004395
+ ],
+ [
+ "▁Jonathan",
+ -11.998170852661133
+ ],
+ [
+ "ül",
+ -11.998492240905762
+ ],
+ [
+ "▁Harvard",
+ -11.998503684997559
+ ],
+ [
+ "▁1985",
+ -11.998773574829102
+ ],
+ [
+ "HP",
+ -11.998839378356934
+ ],
+ [
+ "▁afara",
+ -11.99893569946289
+ ],
+ [
+ "▁halten",
+ -11.999008178710938
+ ],
+ [
+ "▁Technik",
+ -11.999042510986328
+ ],
+ [
+ "▁dressed",
+ -11.999149322509766
+ ],
+ [
+ "weis",
+ -11.999165534973145
+ ],
+ [
+ "▁donated",
+ -11.9993314743042
+ ],
+ [
+ "also",
+ -11.99938678741455
+ ],
+ [
+ "▁EN",
+ -11.999405860900879
+ ],
+ [
+ "▁imprim",
+ -11.99942398071289
+ ],
+ [
+ "▁onions",
+ -11.999458312988281
+ ],
+ [
+ "Par",
+ -11.99950122833252
+ ],
+ [
+ "▁donate",
+ -11.99958324432373
+ ],
+ [
+ "▁mice",
+ -11.999610900878906
+ ],
+ [
+ "referring",
+ -11.999897956848145
+ ],
+ [
+ "▁restored",
+ -12.00003433227539
+ ],
+ [
+ "▁amateur",
+ -12.0000581741333
+ ],
+ [
+ "▁Switch",
+ -12.000075340270996
+ ],
+ [
+ "appel",
+ -12.00013542175293
+ ],
+ [
+ "▁idéal",
+ -12.0001859664917
+ ],
+ [
+ "▁wheat",
+ -12.000199317932129
+ ],
+ [
+ "▁lime",
+ -12.000240325927734
+ ],
+ [
+ "REA",
+ -12.00027084350586
+ ],
+ [
+ "riti",
+ -12.000357627868652
+ ],
+ [
+ "ţiile",
+ -12.00058364868164
+ ],
+ [
+ "▁machinery",
+ -12.00064754486084
+ ],
+ [
+ "UNE",
+ -12.00089168548584
+ ],
+ [
+ "▁Cont",
+ -12.000971794128418
+ ],
+ [
+ "▁attendees",
+ -12.001014709472656
+ ],
+ [
+ "▁aparat",
+ -12.001080513000488
+ ],
+ [
+ "freundlich",
+ -12.00117301940918
+ ],
+ [
+ "▁zilnic",
+ -12.001175880432129
+ ],
+ [
+ "▁spark",
+ -12.001421928405762
+ ],
+ [
+ "▁Gast",
+ -12.001459121704102
+ ],
+ [
+ "▁Issue",
+ -12.00147533416748
+ ],
+ [
+ "▁scam",
+ -12.001566886901855
+ ],
+ [
+ "▁bonds",
+ -12.001618385314941
+ ],
+ [
+ "owner",
+ -12.001641273498535
+ ],
+ [
+ "▁empfehlen",
+ -12.001673698425293
+ ],
+ [
+ "elia",
+ -12.001749992370605
+ ],
+ [
+ "cic",
+ -12.001757621765137
+ ],
+ [
+ "▁honored",
+ -12.001800537109375
+ ],
+ [
+ "▁castle",
+ -12.001846313476562
+ ],
+ [
+ "avand",
+ -12.002058982849121
+ ],
+ [
+ "rough",
+ -12.002108573913574
+ ],
+ [
+ "▁Address",
+ -12.002116203308105
+ ],
+ [
+ "angle",
+ -12.00217342376709
+ ],
+ [
+ "leton",
+ -12.002259254455566
+ ],
+ [
+ "▁locked",
+ -12.002392768859863
+ ],
+ [
+ "▁consolid",
+ -12.00248908996582
+ ],
+ [
+ "▁voucher",
+ -12.003011703491211
+ ],
+ [
+ "ației",
+ -12.003201484680176
+ ],
+ [
+ "wachsen",
+ -12.003211975097656
+ ],
+ [
+ "▁magazines",
+ -12.003287315368652
+ ],
+ [
+ "▁Schools",
+ -12.003318786621094
+ ],
+ [
+ "▁voices",
+ -12.003362655639648
+ ],
+ [
+ "▁Dry",
+ -12.003479957580566
+ ],
+ [
+ "▁tricks",
+ -12.00349235534668
+ ],
+ [
+ "schließlich",
+ -12.003546714782715
+ ],
+ [
+ "▁loyalty",
+ -12.003687858581543
+ ],
+ [
+ "risk",
+ -12.003764152526855
+ ],
+ [
+ "▁Vers",
+ -12.003786087036133
+ ],
+ [
+ "chester",
+ -12.003802299499512
+ ],
+ [
+ "▁decorated",
+ -12.003830909729004
+ ],
+ [
+ "▁copiilor",
+ -12.003969192504883
+ ],
+ [
+ "riz",
+ -12.003994941711426
+ ],
+ [
+ "03.",
+ -12.004013061523438
+ ],
+ [
+ "▁Hur",
+ -12.004016876220703
+ ],
+ [
+ "▁archive",
+ -12.004021644592285
+ ],
+ [
+ "▁Continue",
+ -12.004042625427246
+ ],
+ [
+ "▁Nähe",
+ -12.004043579101562
+ ],
+ [
+ "jit",
+ -12.004090309143066
+ ],
+ [
+ "gekommen",
+ -12.004301071166992
+ ],
+ [
+ "▁conjunction",
+ -12.004349708557129
+ ],
+ [
+ "combining",
+ -12.004404067993164
+ ],
+ [
+ "▁Unterstützung",
+ -12.004517555236816
+ ],
+ [
+ "oza",
+ -12.004593849182129
+ ],
+ [
+ "▁sketch",
+ -12.004720687866211
+ ],
+ [
+ "▁arată",
+ -12.004731178283691
+ ],
+ [
+ "▁Mining",
+ -12.004765510559082
+ ],
+ [
+ "uous",
+ -12.004791259765625
+ ],
+ [
+ "▁devis",
+ -12.004834175109863
+ ],
+ [
+ "Almost",
+ -12.004862785339355
+ ],
+ [
+ "Hu",
+ -12.005037307739258
+ ],
+ [
+ "▁Om",
+ -12.005366325378418
+ ],
+ [
+ "MF",
+ -12.00544548034668
+ ],
+ [
+ "liz",
+ -12.005451202392578
+ ],
+ [
+ "▁fails",
+ -12.005456924438477
+ ],
+ [
+ "▁comparable",
+ -12.005459785461426
+ ],
+ [
+ "▁vein",
+ -12.005547523498535
+ ],
+ [
+ "▁Vis",
+ -12.00561809539795
+ ],
+ [
+ "▁viagra",
+ -12.005654335021973
+ ],
+ [
+ "▁farming",
+ -12.005678176879883
+ ],
+ [
+ "▁Late",
+ -12.005765914916992
+ ],
+ [
+ "geschrieben",
+ -12.006033897399902
+ ],
+ [
+ "hrew",
+ -12.006103515625
+ ],
+ [
+ "▁melt",
+ -12.006120681762695
+ ],
+ [
+ "lager",
+ -12.006168365478516
+ ],
+ [
+ "halte",
+ -12.006240844726562
+ ],
+ [
+ "▁Hotels",
+ -12.006266593933105
+ ],
+ [
+ "▁facebook",
+ -12.0064058303833
+ ],
+ [
+ "▁défi",
+ -12.006550788879395
+ ],
+ [
+ "shore",
+ -12.006802558898926
+ ],
+ [
+ "▁membrane",
+ -12.006866455078125
+ ],
+ [
+ "▁sixth",
+ -12.006903648376465
+ ],
+ [
+ "api",
+ -12.007003784179688
+ ],
+ [
+ "▁Owner",
+ -12.007222175598145
+ ],
+ [
+ "▁(\"",
+ -12.007234573364258
+ ],
+ [
+ "▁$50",
+ -12.007280349731445
+ ],
+ [
+ "▁protective",
+ -12.007420539855957
+ ],
+ [
+ "/2",
+ -12.007548332214355
+ ],
+ [
+ "▁Girls",
+ -12.007562637329102
+ ],
+ [
+ "Gri",
+ -12.00769329071045
+ ],
+ [
+ "▁nouă",
+ -12.007708549499512
+ ],
+ [
+ "▁infections",
+ -12.007813453674316
+ ],
+ [
+ "rân",
+ -12.007868766784668
+ ],
+ [
+ "▁Geb",
+ -12.0078763961792
+ ],
+ [
+ "▁Conseil",
+ -12.007905006408691
+ ],
+ [
+ "▁imagini",
+ -12.007909774780273
+ ],
+ [
+ "▁promotions",
+ -12.00794792175293
+ ],
+ [
+ "▁enforce",
+ -12.00795841217041
+ ],
+ [
+ "▁applicant",
+ -12.007965087890625
+ ],
+ [
+ "▁Apart",
+ -12.008087158203125
+ ],
+ [
+ "▁progression",
+ -12.008151054382324
+ ],
+ [
+ "▁careers",
+ -12.008511543273926
+ ],
+ [
+ "▁litigation",
+ -12.008533477783203
+ ],
+ [
+ "▁Menge",
+ -12.00866413116455
+ ],
+ [
+ "▁Contract",
+ -12.00871753692627
+ ],
+ [
+ "▁Kel",
+ -12.0087308883667
+ ],
+ [
+ "▁réserve",
+ -12.008769035339355
+ ],
+ [
+ "▁Cold",
+ -12.008870124816895
+ ],
+ [
+ "▁larg",
+ -12.009040832519531
+ ],
+ [
+ "▁microwave",
+ -12.009090423583984
+ ],
+ [
+ "▁Whit",
+ -12.009212493896484
+ ],
+ [
+ "▁Technologies",
+ -12.009381294250488
+ ],
+ [
+ "OU",
+ -12.00949478149414
+ ],
+ [
+ "itudine",
+ -12.00959587097168
+ ],
+ [
+ "▁handles",
+ -12.009895324707031
+ ],
+ [
+ "▁proceedings",
+ -12.009982109069824
+ ],
+ [
+ "▁prizes",
+ -12.010043144226074
+ ],
+ [
+ "▁unterstützen",
+ -12.010062217712402
+ ],
+ [
+ "▁piele",
+ -12.010090827941895
+ ],
+ [
+ "▁profound",
+ -12.010153770446777
+ ],
+ [
+ "schließen",
+ -12.0101957321167
+ ],
+ [
+ "▁trafic",
+ -12.01025104522705
+ ],
+ [
+ "▁Nar",
+ -12.010441780090332
+ ],
+ [
+ "▁Gesamt",
+ -12.0106201171875
+ ],
+ [
+ "▁bugs",
+ -12.010720252990723
+ ],
+ [
+ "▁Amy",
+ -12.010764122009277
+ ],
+ [
+ "▁eastern",
+ -12.010775566101074
+ ],
+ [
+ "nice",
+ -12.010784149169922
+ ],
+ [
+ "▁Besuch",
+ -12.010835647583008
+ ],
+ [
+ "▁synth",
+ -12.010892868041992
+ ],
+ [
+ "▁clasa",
+ -12.011194229125977
+ ],
+ [
+ "Book",
+ -12.01134204864502
+ ],
+ [
+ "▁ribbon",
+ -12.011415481567383
+ ],
+ [
+ "▁neues",
+ -12.011431694030762
+ ],
+ [
+ "ZE",
+ -12.011504173278809
+ ],
+ [
+ "▁peers",
+ -12.011613845825195
+ ],
+ [
+ "leistung",
+ -12.011730194091797
+ ],
+ [
+ "▁internship",
+ -12.011808395385742
+ ],
+ [
+ "count",
+ -12.011850357055664
+ ],
+ [
+ "nam",
+ -12.01193618774414
+ ],
+ [
+ "▁12-",
+ -12.012072563171387
+ ],
+ [
+ "acked",
+ -12.012146949768066
+ ],
+ [
+ "gonna",
+ -12.012146949768066
+ ],
+ [
+ "▁Dinge",
+ -12.01215648651123
+ ],
+ [
+ "Time",
+ -12.012299537658691
+ ],
+ [
+ "▁twelve",
+ -12.01242446899414
+ ],
+ [
+ "eye",
+ -12.012432098388672
+ ],
+ [
+ "▁avantaj",
+ -12.01253604888916
+ ],
+ [
+ "▁Glas",
+ -12.012731552124023
+ ],
+ [
+ "aucune",
+ -12.0127534866333
+ ],
+ [
+ "▁boil",
+ -12.012763977050781
+ ],
+ [
+ "▁Gray",
+ -12.012773513793945
+ ],
+ [
+ "adapt",
+ -12.01288890838623
+ ],
+ [
+ "occ",
+ -12.012895584106445
+ ],
+ [
+ "▁prieten",
+ -12.012897491455078
+ ],
+ [
+ "▁trai",
+ -12.01296615600586
+ ],
+ [
+ "▁Scal",
+ -12.013009071350098
+ ],
+ [
+ "▁conscious",
+ -12.013057708740234
+ ],
+ [
+ "▁charter",
+ -12.013093948364258
+ ],
+ [
+ "KS",
+ -12.013242721557617
+ ],
+ [
+ "▁Barr",
+ -12.013404846191406
+ ],
+ [
+ "▁summit",
+ -12.013411521911621
+ ],
+ [
+ "▁inflammation",
+ -12.013439178466797
+ ],
+ [
+ "tungs",
+ -12.013440132141113
+ ],
+ [
+ "ovic",
+ -12.013449668884277
+ ],
+ [
+ "▁conduit",
+ -12.013465881347656
+ ],
+ [
+ "▁Alice",
+ -12.013702392578125
+ ],
+ [
+ "▁veterans",
+ -12.013850212097168
+ ],
+ [
+ "Während",
+ -12.013944625854492
+ ],
+ [
+ "▁maximal",
+ -12.014013290405273
+ ],
+ [
+ "▁Hawaii",
+ -12.014037132263184
+ ],
+ [
+ "▁Pine",
+ -12.01432991027832
+ ],
+ [
+ "acelasi",
+ -12.014391899108887
+ ],
+ [
+ "hyp",
+ -12.014424324035645
+ ],
+ [
+ "sensitivity",
+ -12.01445198059082
+ ],
+ [
+ "pour",
+ -12.014481544494629
+ ],
+ [
+ "ре",
+ -12.014493942260742
+ ],
+ [
+ "▁Kentucky",
+ -12.015129089355469
+ ],
+ [
+ "▁badge",
+ -12.015276908874512
+ ],
+ [
+ "affecting",
+ -12.015310287475586
+ ],
+ [
+ "▁chairman",
+ -12.015311241149902
+ ],
+ [
+ "▁München",
+ -12.015467643737793
+ ],
+ [
+ "▁Hersteller",
+ -12.015469551086426
+ ],
+ [
+ "▁urmat",
+ -12.015615463256836
+ ],
+ [
+ "tels",
+ -12.015654563903809
+ ],
+ [
+ "▁FM",
+ -12.015701293945312
+ ],
+ [
+ "▁Basis",
+ -12.015732765197754
+ ],
+ [
+ "▁erklärt",
+ -12.015809059143066
+ ],
+ [
+ "▁changer",
+ -12.015859603881836
+ ],
+ [
+ "tischen",
+ -12.0159330368042
+ ],
+ [
+ "▁brave",
+ -12.015960693359375
+ ],
+ [
+ "▁siguranta",
+ -12.015986442565918
+ ],
+ [
+ "▁partnerships",
+ -12.015989303588867
+ ],
+ [
+ "ților",
+ -12.015999794006348
+ ],
+ [
+ "▁breathe",
+ -12.016141891479492
+ ],
+ [
+ "rink",
+ -12.016551971435547
+ ],
+ [
+ "▁footage",
+ -12.016654014587402
+ ],
+ [
+ "▁transformed",
+ -12.016658782958984
+ ],
+ [
+ "▁prep",
+ -12.016866683959961
+ ],
+ [
+ "▁upset",
+ -12.016901969909668
+ ],
+ [
+ "▁Native",
+ -12.017059326171875
+ ],
+ [
+ "▁Prima",
+ -12.017154693603516
+ ],
+ [
+ "▁jersey",
+ -12.017163276672363
+ ],
+ [
+ "230",
+ -12.017182350158691
+ ],
+ [
+ "▁lucrurile",
+ -12.017393112182617
+ ],
+ [
+ "▁divine",
+ -12.017502784729004
+ ],
+ [
+ "▁Pit",
+ -12.017593383789062
+ ],
+ [
+ "RIS",
+ -12.01765251159668
+ ],
+ [
+ "▁Cultural",
+ -12.017672538757324
+ ],
+ [
+ "▁exotic",
+ -12.017786979675293
+ ],
+ [
+ "▁tastes",
+ -12.017881393432617
+ ],
+ [
+ "▁bargain",
+ -12.017913818359375
+ ],
+ [
+ "▁optimize",
+ -12.017985343933105
+ ],
+ [
+ "▁électrique",
+ -12.018012046813965
+ ],
+ [
+ "deuxième",
+ -12.018030166625977
+ ],
+ [
+ "▁Gary",
+ -12.018085479736328
+ ],
+ [
+ "▁projection",
+ -12.018122673034668
+ ],
+ [
+ "▁sliding",
+ -12.018195152282715
+ ],
+ [
+ "club",
+ -12.018216133117676
+ ],
+ [
+ "association",
+ -12.01823902130127
+ ],
+ [
+ "▁LG",
+ -12.018259048461914
+ ],
+ [
+ "▁capsule",
+ -12.018291473388672
+ ],
+ [
+ "▁politicians",
+ -12.018397331237793
+ ],
+ [
+ "▁thumb",
+ -12.018423080444336
+ ],
+ [
+ "▁globally",
+ -12.018743515014648
+ ],
+ [
+ "positioned",
+ -12.018796920776367
+ ],
+ [
+ "▁Hamilton",
+ -12.018861770629883
+ ],
+ [
+ "arme",
+ -12.018881797790527
+ ],
+ [
+ "▁efectuat",
+ -12.018881797790527
+ ],
+ [
+ "zip",
+ -12.019111633300781
+ ],
+ [
+ "▁welfare",
+ -12.019201278686523
+ ],
+ [
+ "Leistung",
+ -12.019230842590332
+ ],
+ [
+ "▁Bac",
+ -12.019316673278809
+ ],
+ [
+ "▁fizic",
+ -12.019338607788086
+ ],
+ [
+ "OK",
+ -12.019454002380371
+ ],
+ [
+ "▁limba",
+ -12.019545555114746
+ ],
+ [
+ "▁wardrobe",
+ -12.019549369812012
+ ],
+ [
+ "▁offline",
+ -12.019627571105957
+ ],
+ [
+ "▁fortune",
+ -12.019665718078613
+ ],
+ [
+ "▁dialog",
+ -12.019681930541992
+ ],
+ [
+ "▁dramatically",
+ -12.01997184753418
+ ],
+ [
+ "▁NYC",
+ -12.020045280456543
+ ],
+ [
+ "▁Rem",
+ -12.02017593383789
+ ],
+ [
+ "▁bronze",
+ -12.020455360412598
+ ],
+ [
+ "▁pulse",
+ -12.02053451538086
+ ],
+ [
+ "Fortunately",
+ -12.020562171936035
+ ],
+ [
+ "▁glue",
+ -12.020596504211426
+ ],
+ [
+ "▁Expo",
+ -12.020720481872559
+ ],
+ [
+ "▁profitable",
+ -12.020776748657227
+ ],
+ [
+ "▁distributor",
+ -12.020845413208008
+ ],
+ [
+ "abilité",
+ -12.020869255065918
+ ],
+ [
+ "▁lyrics",
+ -12.020913124084473
+ ],
+ [
+ "▁mesh",
+ -12.02114486694336
+ ],
+ [
+ "▁organizational",
+ -12.021157264709473
+ ],
+ [
+ "▁vanilla",
+ -12.021249771118164
+ ],
+ [
+ "▁foc",
+ -12.021355628967285
+ ],
+ [
+ "▁1984",
+ -12.02147388458252
+ ],
+ [
+ "▁créé",
+ -12.02172565460205
+ ],
+ [
+ "▁servi",
+ -12.022027969360352
+ ],
+ [
+ "▁underneath",
+ -12.022095680236816
+ ],
+ [
+ "▁surveys",
+ -12.022143363952637
+ ],
+ [
+ "▁genes",
+ -12.022238731384277
+ ],
+ [
+ "▁limite",
+ -12.02224349975586
+ ],
+ [
+ "oder",
+ -12.022247314453125
+ ],
+ [
+ "▁mandatory",
+ -12.022269248962402
+ ],
+ [
+ "▁hospitality",
+ -12.022303581237793
+ ],
+ [
+ "▁bikes",
+ -12.022309303283691
+ ],
+ [
+ "▁Quote",
+ -12.022358894348145
+ ],
+ [
+ "glu",
+ -12.02241039276123
+ ],
+ [
+ "▁activitatea",
+ -12.022513389587402
+ ],
+ [
+ "preventing",
+ -12.022584915161133
+ ],
+ [
+ "▁Kh",
+ -12.02259635925293
+ ],
+ [
+ "économie",
+ -12.022616386413574
+ ],
+ [
+ "▁visite",
+ -12.022757530212402
+ ],
+ [
+ "▁spectacle",
+ -12.022778511047363
+ ],
+ [
+ "▁tract",
+ -12.022860527038574
+ ],
+ [
+ "▁quant",
+ -12.022862434387207
+ ],
+ [
+ "▁evolu",
+ -12.022866249084473
+ ],
+ [
+ "▁invata",
+ -12.023070335388184
+ ],
+ [
+ "▁homo",
+ -12.02311897277832
+ ],
+ [
+ "▁Users",
+ -12.02344799041748
+ ],
+ [
+ "introducing",
+ -12.023632049560547
+ ],
+ [
+ "hibi",
+ -12.023661613464355
+ ],
+ [
+ "▁Instrument",
+ -12.023805618286133
+ ],
+ [
+ "▁ép",
+ -12.023839950561523
+ ],
+ [
+ "▁Raj",
+ -12.023869514465332
+ ],
+ [
+ "▁executives",
+ -12.023881912231445
+ ],
+ [
+ "atoire",
+ -12.023885726928711
+ ],
+ [
+ "▁erforderlich",
+ -12.02397346496582
+ ],
+ [
+ "male",
+ -12.024211883544922
+ ],
+ [
+ "umble",
+ -12.024271011352539
+ ],
+ [
+ "erson",
+ -12.024277687072754
+ ],
+ [
+ "▁Treatment",
+ -12.024286270141602
+ ],
+ [
+ "▁Representative",
+ -12.024314880371094
+ ],
+ [
+ "▁corners",
+ -12.024409294128418
+ ],
+ [
+ "▁Petit",
+ -12.024599075317383
+ ],
+ [
+ "8)",
+ -12.02464771270752
+ ],
+ [
+ "▁Walker",
+ -12.024714469909668
+ ],
+ [
+ "▁Stir",
+ -12.02476692199707
+ ],
+ [
+ "/19",
+ -12.024767875671387
+ ],
+ [
+ "▁Stelle",
+ -12.024979591369629
+ ],
+ [
+ "ără",
+ -12.025009155273438
+ ],
+ [
+ "osse",
+ -12.025166511535645
+ ],
+ [
+ "2000",
+ -12.025189399719238
+ ],
+ [
+ "▁McG",
+ -12.025580406188965
+ ],
+ [
+ "DV",
+ -12.025773048400879
+ ],
+ [
+ "▁Firm",
+ -12.025862693786621
+ ],
+ [
+ "▁packet",
+ -12.025904655456543
+ ],
+ [
+ "Toate",
+ -12.02640438079834
+ ],
+ [
+ "▁institutional",
+ -12.026479721069336
+ ],
+ [
+ "rug",
+ -12.026663780212402
+ ],
+ [
+ "DG",
+ -12.026837348937988
+ ],
+ [
+ "fine",
+ -12.026837348937988
+ ],
+ [
+ "bringen",
+ -12.026856422424316
+ ],
+ [
+ "▁Horse",
+ -12.026921272277832
+ ],
+ [
+ "▁premiere",
+ -12.026937484741211
+ ],
+ [
+ "▁Că",
+ -12.027026176452637
+ ],
+ [
+ "acheter",
+ -12.02703857421875
+ ],
+ [
+ "▁Afghanistan",
+ -12.027053833007812
+ ],
+ [
+ "▁Prop",
+ -12.027085304260254
+ ],
+ [
+ "ühr",
+ -12.02715015411377
+ ],
+ [
+ "▁braucht",
+ -12.027398109436035
+ ],
+ [
+ "▁sunny",
+ -12.027424812316895
+ ],
+ [
+ "▁Sach",
+ -12.027461051940918
+ ],
+ [
+ "▁volumes",
+ -12.02753734588623
+ ],
+ [
+ "tinut",
+ -12.02759838104248
+ ],
+ [
+ "▁Sho",
+ -12.027722358703613
+ ],
+ [
+ "▁winds",
+ -12.027735710144043
+ ],
+ [
+ "▁Mall",
+ -12.027873992919922
+ ],
+ [
+ "ledge",
+ -12.027937889099121
+ ],
+ [
+ "▁sciences",
+ -12.027997016906738
+ ],
+ [
+ "plication",
+ -12.028024673461914
+ ],
+ [
+ "VR",
+ -12.028068542480469
+ ],
+ [
+ "destin",
+ -12.028234481811523
+ ],
+ [
+ "▁früh",
+ -12.02833366394043
+ ],
+ [
+ "▁tongue",
+ -12.028359413146973
+ ],
+ [
+ "▁Jennifer",
+ -12.028425216674805
+ ],
+ [
+ "▁bracket",
+ -12.028427124023438
+ ],
+ [
+ "▁episodes",
+ -12.02845287322998
+ ],
+ [
+ "breite",
+ -12.028461456298828
+ ],
+ [
+ "▁stoc",
+ -12.028635025024414
+ ],
+ [
+ "ilia",
+ -12.028728485107422
+ ],
+ [
+ "▁Gulf",
+ -12.02874755859375
+ ],
+ [
+ "▁transparency",
+ -12.028768539428711
+ ],
+ [
+ "Industrie",
+ -12.028853416442871
+ ],
+ [
+ "▁viewers",
+ -12.028916358947754
+ ],
+ [
+ "AIN",
+ -12.029129981994629
+ ],
+ [
+ "▁Registration",
+ -12.029149055480957
+ ],
+ [
+ "/4",
+ -12.029309272766113
+ ],
+ [
+ "▁fera",
+ -12.029337882995605
+ ],
+ [
+ "▁06",
+ -12.029351234436035
+ ],
+ [
+ "▁einzu",
+ -12.029391288757324
+ ],
+ [
+ "enburg",
+ -12.02944278717041
+ ],
+ [
+ "▁eff",
+ -12.029449462890625
+ ],
+ [
+ "▁Stage",
+ -12.029558181762695
+ ],
+ [
+ "▁Cour",
+ -12.029685020446777
+ ],
+ [
+ "indu",
+ -12.029836654663086
+ ],
+ [
+ "▁Tools",
+ -12.029909133911133
+ ],
+ [
+ "IST",
+ -12.029921531677246
+ ],
+ [
+ "grund",
+ -12.030105590820312
+ ],
+ [
+ "seitig",
+ -12.030153274536133
+ ],
+ [
+ "pai",
+ -12.030250549316406
+ ],
+ [
+ "▁waist",
+ -12.030350685119629
+ ],
+ [
+ "▁Therapy",
+ -12.03049373626709
+ ],
+ [
+ "▁nomination",
+ -12.030599594116211
+ ],
+ [
+ "▁seama",
+ -12.030790328979492
+ ],
+ [
+ "▁analyse",
+ -12.030975341796875
+ ],
+ [
+ "▁emerge",
+ -12.031044006347656
+ ],
+ [
+ "▁adjustment",
+ -12.031106948852539
+ ],
+ [
+ "▁stroll",
+ -12.031106948852539
+ ],
+ [
+ "▁Beyond",
+ -12.031174659729004
+ ],
+ [
+ "▁legally",
+ -12.03122615814209
+ ],
+ [
+ "▁gauge",
+ -12.03123664855957
+ ],
+ [
+ "▁26,",
+ -12.031360626220703
+ ],
+ [
+ "Tex",
+ -12.031390190124512
+ ],
+ [
+ "economic",
+ -12.031488418579102
+ ],
+ [
+ "stoffe",
+ -12.031532287597656
+ ],
+ [
+ "Wir",
+ -12.031559944152832
+ ],
+ [
+ "ffen",
+ -12.031601905822754
+ ],
+ [
+ "▁acoperi",
+ -12.031609535217285
+ ],
+ [
+ "▁finale",
+ -12.031792640686035
+ ],
+ [
+ "▁theoretical",
+ -12.031864166259766
+ ],
+ [
+ "1.3",
+ -12.031875610351562
+ ],
+ [
+ "anim",
+ -12.031888008117676
+ ],
+ [
+ "▁separation",
+ -12.031928062438965
+ ],
+ [
+ "agence",
+ -12.031937599182129
+ ],
+ [
+ "▁réalisé",
+ -12.032069206237793
+ ],
+ [
+ "sprech",
+ -12.03215503692627
+ ],
+ [
+ "▁embedded",
+ -12.032208442687988
+ ],
+ [
+ "▁defence",
+ -12.032242774963379
+ ],
+ [
+ "éni",
+ -12.032569885253906
+ ],
+ [
+ "▁Norman",
+ -12.032613754272461
+ ],
+ [
+ "▁insgesamt",
+ -12.032621383666992
+ ],
+ [
+ "▁reminde",
+ -12.032631874084473
+ ],
+ [
+ "▁timeline",
+ -12.032703399658203
+ ],
+ [
+ "▁symbols",
+ -12.032770156860352
+ ],
+ [
+ "▁booth",
+ -12.032783508300781
+ ],
+ [
+ "▁Window",
+ -12.032788276672363
+ ],
+ [
+ "▁Titan",
+ -12.032910346984863
+ ],
+ [
+ "înt",
+ -12.033021926879883
+ ],
+ [
+ "▁langa",
+ -12.033021926879883
+ ],
+ [
+ "isant",
+ -12.03303337097168
+ ],
+ [
+ "hart",
+ -12.033113479614258
+ ],
+ [
+ "broader",
+ -12.033266067504883
+ ],
+ [
+ "▁stays",
+ -12.033288955688477
+ ],
+ [
+ "dur",
+ -12.033488273620605
+ ],
+ [
+ "▁Actually",
+ -12.033514022827148
+ ],
+ [
+ "works",
+ -12.03351879119873
+ ],
+ [
+ "▁réussi",
+ -12.03357219696045
+ ],
+ [
+ "▁performant",
+ -12.033658981323242
+ ],
+ [
+ "▁banana",
+ -12.033788681030273
+ ],
+ [
+ "▁baked",
+ -12.033870697021484
+ ],
+ [
+ "▁Parlament",
+ -12.033931732177734
+ ],
+ [
+ "▁Legend",
+ -12.033967018127441
+ ],
+ [
+ "toata",
+ -12.034172058105469
+ ],
+ [
+ "platte",
+ -12.03419017791748
+ ],
+ [
+ "▁Mou",
+ -12.034192085266113
+ ],
+ [
+ "HL",
+ -12.034235000610352
+ ],
+ [
+ "▁(8",
+ -12.034290313720703
+ ],
+ [
+ "▁accepting",
+ -12.034313201904297
+ ],
+ [
+ "▁Senator",
+ -12.034340858459473
+ ],
+ [
+ "▁consciousness",
+ -12.034396171569824
+ ],
+ [
+ "▁conducting",
+ -12.0344820022583
+ ],
+ [
+ "▁panic",
+ -12.034833908081055
+ ],
+ [
+ "▁FDA",
+ -12.035112380981445
+ ],
+ [
+ "▁(7",
+ -12.035163879394531
+ ],
+ [
+ "tool",
+ -12.035300254821777
+ ],
+ [
+ "▁Shipping",
+ -12.03538703918457
+ ],
+ [
+ "▁hop",
+ -12.035545349121094
+ ],
+ [
+ "▁conferences",
+ -12.03564167022705
+ ],
+ [
+ "▁pork",
+ -12.035661697387695
+ ],
+ [
+ "▁spam",
+ -12.035730361938477
+ ],
+ [
+ "▁interesant",
+ -12.035815238952637
+ ],
+ [
+ "▁Tagen",
+ -12.03581714630127
+ ],
+ [
+ "sig",
+ -12.035886764526367
+ ],
+ [
+ "étro",
+ -12.036044120788574
+ ],
+ [
+ "▁legendary",
+ -12.036449432373047
+ ],
+ [
+ "▁Alternative",
+ -12.036643981933594
+ ],
+ [
+ "iana",
+ -12.036704063415527
+ ],
+ [
+ "▁responsable",
+ -12.036888122558594
+ ],
+ [
+ "▁Mihai",
+ -12.037237167358398
+ ],
+ [
+ "▁decreased",
+ -12.037345886230469
+ ],
+ [
+ "▁organised",
+ -12.037485122680664
+ ],
+ [
+ "▁Lamp",
+ -12.037589073181152
+ ],
+ [
+ "litz",
+ -12.037622451782227
+ ],
+ [
+ "ohn",
+ -12.037622451782227
+ ],
+ [
+ "▁moteur",
+ -12.0376615524292
+ ],
+ [
+ "III",
+ -12.03768539428711
+ ],
+ [
+ "▁Montag",
+ -12.037755012512207
+ ],
+ [
+ "▁naturel",
+ -12.037814140319824
+ ],
+ [
+ "▁Hus",
+ -12.037842750549316
+ ],
+ [
+ "▁Schl",
+ -12.037884712219238
+ ],
+ [
+ "ains",
+ -12.037968635559082
+ ],
+ [
+ "▁dying",
+ -12.0380859375
+ ],
+ [
+ "▁HIV",
+ -12.038115501403809
+ ],
+ [
+ "],",
+ -12.038164138793945
+ ],
+ [
+ "alität",
+ -12.03818416595459
+ ],
+ [
+ "▁institute",
+ -12.038249015808105
+ ],
+ [
+ "mix",
+ -12.038433074951172
+ ],
+ [
+ "▁Regulation",
+ -12.038453102111816
+ ],
+ [
+ "▁pagina",
+ -12.03857707977295
+ ],
+ [
+ "▁Awesome",
+ -12.03860092163086
+ ],
+ [
+ "▁Official",
+ -12.03860092163086
+ ],
+ [
+ "▁Minute",
+ -12.038601875305176
+ ],
+ [
+ "▁dairy",
+ -12.038787841796875
+ ],
+ [
+ "▁carti",
+ -12.038881301879883
+ ],
+ [
+ "isk",
+ -12.039091110229492
+ ],
+ [
+ "▁thrilled",
+ -12.039138793945312
+ ],
+ [
+ "▁german",
+ -12.039172172546387
+ ],
+ [
+ "▁frustration",
+ -12.039228439331055
+ ],
+ [
+ "▁forums",
+ -12.03927230834961
+ ],
+ [
+ "command",
+ -12.039361000061035
+ ],
+ [
+ "▁router",
+ -12.039399147033691
+ ],
+ [
+ "▁Lösung",
+ -12.039423942565918
+ ],
+ [
+ "white",
+ -12.039470672607422
+ ],
+ [
+ "▁synthetic",
+ -12.039487838745117
+ ],
+ [
+ "▁retrouver",
+ -12.039554595947266
+ ],
+ [
+ "alle",
+ -12.039621353149414
+ ],
+ [
+ "daran",
+ -12.039653778076172
+ ],
+ [
+ "▁wahr",
+ -12.039697647094727
+ ],
+ [
+ "▁paths",
+ -12.039875984191895
+ ],
+ [
+ "▁unver",
+ -12.039962768554688
+ ],
+ [
+ "▁Environment",
+ -12.0400972366333
+ ],
+ [
+ "▁médecin",
+ -12.040510177612305
+ ],
+ [
+ "crypt",
+ -12.040572166442871
+ ],
+ [
+ "▁pursuit",
+ -12.040595054626465
+ ],
+ [
+ "flat",
+ -12.040611267089844
+ ],
+ [
+ "bron",
+ -12.040698051452637
+ ],
+ [
+ "▁Specialist",
+ -12.040852546691895
+ ],
+ [
+ "▁Vent",
+ -12.041157722473145
+ ],
+ [
+ "Gen",
+ -12.04132080078125
+ ],
+ [
+ "▁attraction",
+ -12.04132080078125
+ ],
+ [
+ "▁piese",
+ -12.041372299194336
+ ],
+ [
+ "CHE",
+ -12.041665077209473
+ ],
+ [
+ "fähig",
+ -12.04172420501709
+ ],
+ [
+ "▁28,",
+ -12.041773796081543
+ ],
+ [
+ "defender",
+ -12.041810989379883
+ ],
+ [
+ "▁stupid",
+ -12.04181957244873
+ ],
+ [
+ "enfin",
+ -12.04185962677002
+ ],
+ [
+ "▁composite",
+ -12.04207706451416
+ ],
+ [
+ "fragen",
+ -12.042202949523926
+ ],
+ [
+ "Part",
+ -12.042232513427734
+ ],
+ [
+ "may",
+ -12.042238235473633
+ ],
+ [
+ "▁Bucureşti",
+ -12.042248725891113
+ ],
+ [
+ "▁février",
+ -12.042248725891113
+ ],
+ [
+ "RED",
+ -12.042417526245117
+ ],
+ [
+ "▁makers",
+ -12.042462348937988
+ ],
+ [
+ "▁guns",
+ -12.042594909667969
+ ],
+ [
+ "▁pasta",
+ -12.042706489562988
+ ],
+ [
+ "STR",
+ -12.04271125793457
+ ],
+ [
+ "▁worthy",
+ -12.042760848999023
+ ],
+ [
+ "Poate",
+ -12.042783737182617
+ ],
+ [
+ "▁101",
+ -12.04286003112793
+ ],
+ [
+ "▁souhaitez",
+ -12.04299545288086
+ ],
+ [
+ "GN",
+ -12.043449401855469
+ ],
+ [
+ "drive",
+ -12.043499946594238
+ ],
+ [
+ "▁aveti",
+ -12.043582916259766
+ ],
+ [
+ "▁eventual",
+ -12.043591499328613
+ ],
+ [
+ "▁américain",
+ -12.043642044067383
+ ],
+ [
+ "▁Mine",
+ -12.043678283691406
+ ],
+ [
+ "▁sunset",
+ -12.043729782104492
+ ],
+ [
+ "▁Choice",
+ -12.043844223022461
+ ],
+ [
+ "▁offset",
+ -12.043944358825684
+ ],
+ [
+ "APP",
+ -12.04410457611084
+ ],
+ [
+ "▁suchen",
+ -12.044130325317383
+ ],
+ [
+ "▁aduc",
+ -12.044228553771973
+ ],
+ [
+ "▁Unternehmens",
+ -12.044342041015625
+ ],
+ [
+ "▁//",
+ -12.044651985168457
+ ],
+ [
+ "▁astept",
+ -12.044678688049316
+ ],
+ [
+ "▁Birthday",
+ -12.045061111450195
+ ],
+ [
+ "▁barn",
+ -12.045083999633789
+ ],
+ [
+ "apport",
+ -12.045105934143066
+ ],
+ [
+ "▁collar",
+ -12.045212745666504
+ ],
+ [
+ "▁gefunden",
+ -12.045294761657715
+ ],
+ [
+ "▁Hai",
+ -12.045429229736328
+ ],
+ [
+ "▁Soul",
+ -12.045441627502441
+ ],
+ [
+ "ismus",
+ -12.045654296875
+ ],
+ [
+ "letzt",
+ -12.045754432678223
+ ],
+ [
+ "▁maker",
+ -12.045841217041016
+ ],
+ [
+ "▁executed",
+ -12.045857429504395
+ ],
+ [
+ "▁Forschung",
+ -12.045915603637695
+ ],
+ [
+ "▁täglich",
+ -12.045958518981934
+ ],
+ [
+ "▁tailor",
+ -12.045960426330566
+ ],
+ [
+ "▁headquarters",
+ -12.0460844039917
+ ],
+ [
+ "▁physicians",
+ -12.046112060546875
+ ],
+ [
+ "▁Scout",
+ -12.046126365661621
+ ],
+ [
+ "folgen",
+ -12.046175003051758
+ ],
+ [
+ "▁cycling",
+ -12.046184539794922
+ ],
+ [
+ "mindestens",
+ -12.04620361328125
+ ],
+ [
+ "▁joli",
+ -12.046216011047363
+ ],
+ [
+ "▁classification",
+ -12.046225547790527
+ ],
+ [
+ "▁Führung",
+ -12.046258926391602
+ ],
+ [
+ "▁peau",
+ -12.04629135131836
+ ],
+ [
+ "INT",
+ -12.046502113342285
+ ],
+ [
+ "▁Garage",
+ -12.046664237976074
+ ],
+ [
+ "teile",
+ -12.046714782714844
+ ],
+ [
+ "util",
+ -12.046716690063477
+ ],
+ [
+ "▁petrec",
+ -12.046751022338867
+ ],
+ [
+ "▁Nevada",
+ -12.046826362609863
+ ],
+ [
+ "▁laisser",
+ -12.04706859588623
+ ],
+ [
+ "▁territoire",
+ -12.047131538391113
+ ],
+ [
+ "▁fichier",
+ -12.047154426574707
+ ],
+ [
+ "▁Formula",
+ -12.047343254089355
+ ],
+ [
+ "scopul",
+ -12.047379493713379
+ ],
+ [
+ "▁Tee",
+ -12.047486305236816
+ ],
+ [
+ "▁Monte",
+ -12.047529220581055
+ ],
+ [
+ "▁pumpkin",
+ -12.04757022857666
+ ],
+ [
+ "▁picnic",
+ -12.047589302062988
+ ],
+ [
+ "▁occupation",
+ -12.047652244567871
+ ],
+ [
+ "▁numérique",
+ -12.047831535339355
+ ],
+ [
+ "linie",
+ -12.04786491394043
+ ],
+ [
+ "▁masina",
+ -12.048117637634277
+ ],
+ [
+ "▁Prä",
+ -12.048173904418945
+ ],
+ [
+ "▁dezvoltare",
+ -12.048177719116211
+ ],
+ [
+ "▁vient",
+ -12.048291206359863
+ ],
+ [
+ "▁ranks",
+ -12.048295021057129
+ ],
+ [
+ "▁Bruce",
+ -12.048420906066895
+ ],
+ [
+ "▁seara",
+ -12.048433303833008
+ ],
+ [
+ "▁hungry",
+ -12.048563003540039
+ ],
+ [
+ "▁resolved",
+ -12.048650741577148
+ ],
+ [
+ "paired",
+ -12.048735618591309
+ ],
+ [
+ "▁Congratulations",
+ -12.048881530761719
+ ],
+ [
+ "▁religi",
+ -12.048918724060059
+ ],
+ [
+ "sätze",
+ -12.04897689819336
+ ],
+ [
+ "▁Eat",
+ -12.049172401428223
+ ],
+ [
+ "▁dense",
+ -12.049442291259766
+ ],
+ [
+ "▁slice",
+ -12.049447059631348
+ ],
+ [
+ "▁mulți",
+ -12.049463272094727
+ ],
+ [
+ "▁vorbe",
+ -12.049517631530762
+ ],
+ [
+ "▁terminate",
+ -12.049779891967773
+ ],
+ [
+ "worm",
+ -12.049880981445312
+ ],
+ [
+ "ignon",
+ -12.0499267578125
+ ],
+ [
+ "▁Howard",
+ -12.049992561340332
+ ],
+ [
+ "▁toddler",
+ -12.050017356872559
+ ],
+ [
+ "▁waters",
+ -12.050033569335938
+ ],
+ [
+ "▁graduates",
+ -12.0501708984375
+ ],
+ [
+ "▁fundraising",
+ -12.050298690795898
+ ],
+ [
+ "06.",
+ -12.05031967163086
+ ],
+ [
+ "▁scent",
+ -12.050346374511719
+ ],
+ [
+ "▁CPU",
+ -12.050406455993652
+ ],
+ [
+ "▁Kid",
+ -12.05045223236084
+ ],
+ [
+ "▁Years",
+ -12.050460815429688
+ ],
+ [
+ "▁Oktober",
+ -12.05063533782959
+ ],
+ [
+ "filled",
+ -12.050726890563965
+ ],
+ [
+ "▁Laser",
+ -12.05079460144043
+ ],
+ [
+ "▁tut",
+ -12.051032066345215
+ ],
+ [
+ "ively",
+ -12.051101684570312
+ ],
+ [
+ "▁WiFi",
+ -12.051161766052246
+ ],
+ [
+ "standen",
+ -12.051176071166992
+ ],
+ [
+ "▁publié",
+ -12.051243782043457
+ ],
+ [
+ "▁explaining",
+ -12.051279067993164
+ ],
+ [
+ "trieb",
+ -12.051288604736328
+ ],
+ [
+ "▁Rapid",
+ -12.0513334274292
+ ],
+ [
+ "▁unterstützt",
+ -12.051352500915527
+ ],
+ [
+ "▁Sonnen",
+ -12.051401138305664
+ ],
+ [
+ "▁lenses",
+ -12.05141544342041
+ ],
+ [
+ "▁pressing",
+ -12.051477432250977
+ ],
+ [
+ "▁respected",
+ -12.051657676696777
+ ],
+ [
+ "adapted",
+ -12.051706314086914
+ ],
+ [
+ "Don",
+ -12.051726341247559
+ ],
+ [
+ "▁mun",
+ -12.051733016967773
+ ],
+ [
+ "MAR",
+ -12.05180835723877
+ ],
+ [
+ "▁seam",
+ -12.051852226257324
+ ],
+ [
+ "chev",
+ -12.052140235900879
+ ],
+ [
+ "▁Sozial",
+ -12.052424430847168
+ ],
+ [
+ "▁Arabia",
+ -12.052485466003418
+ ],
+ [
+ "▁equation",
+ -12.05257511138916
+ ],
+ [
+ "▁elevi",
+ -12.052780151367188
+ ],
+ [
+ "▁piata",
+ -12.052868843078613
+ ],
+ [
+ "JA",
+ -12.052873611450195
+ ],
+ [
+ "▁wholesale",
+ -12.052887916564941
+ ],
+ [
+ "▁faithful",
+ -12.05296516418457
+ ],
+ [
+ "legal",
+ -12.053092002868652
+ ],
+ [
+ "▁Brexit",
+ -12.053095817565918
+ ],
+ [
+ "vention",
+ -12.053120613098145
+ ],
+ [
+ "▁adhere",
+ -12.053221702575684
+ ],
+ [
+ "▁Associate",
+ -12.053257942199707
+ ],
+ [
+ "▁decorations",
+ -12.053272247314453
+ ],
+ [
+ "▁crois",
+ -12.053359985351562
+ ],
+ [
+ "buck",
+ -12.053370475769043
+ ],
+ [
+ "▁smartphones",
+ -12.053421020507812
+ ],
+ [
+ "Regardless",
+ -12.053427696228027
+ ],
+ [
+ "center",
+ -12.053434371948242
+ ],
+ [
+ "eiß",
+ -12.053481101989746
+ ],
+ [
+ "▁emotion",
+ -12.053584098815918
+ ],
+ [
+ "▁Gespräch",
+ -12.053797721862793
+ ],
+ [
+ "▁Avi",
+ -12.053963661193848
+ ],
+ [
+ "▁loft",
+ -12.054059982299805
+ ],
+ [
+ "▁Wissen",
+ -12.054391860961914
+ ],
+ [
+ "▁orchestra",
+ -12.05439567565918
+ ],
+ [
+ "▁gehören",
+ -12.054421424865723
+ ],
+ [
+ "▁Reich",
+ -12.054532051086426
+ ],
+ [
+ "▁abandoned",
+ -12.054548263549805
+ ],
+ [
+ "▁Lanka",
+ -12.054586410522461
+ ],
+ [
+ "pala",
+ -12.054832458496094
+ ],
+ [
+ "▁Stell",
+ -12.054838180541992
+ ],
+ [
+ "logged",
+ -12.054924964904785
+ ],
+ [
+ "terie",
+ -12.054935455322266
+ ],
+ [
+ "▁educa",
+ -12.054954528808594
+ ],
+ [
+ "1).",
+ -12.055097579956055
+ ],
+ [
+ "▁disponibil",
+ -12.055119514465332
+ ],
+ [
+ "IND",
+ -12.055197715759277
+ ],
+ [
+ "▁Pont",
+ -12.055288314819336
+ ],
+ [
+ "▁téléphone",
+ -12.055398941040039
+ ],
+ [
+ "▁rope",
+ -12.055595397949219
+ ],
+ [
+ "ève",
+ -12.055622100830078
+ ],
+ [
+ "▁Trainer",
+ -12.056062698364258
+ ],
+ [
+ "▁présence",
+ -12.0560941696167
+ ],
+ [
+ "▁Oscar",
+ -12.056121826171875
+ ],
+ [
+ "▁VR",
+ -12.056342124938965
+ ],
+ [
+ "▁Besucher",
+ -12.056357383728027
+ ],
+ [
+ "▁disponibles",
+ -12.056447982788086
+ ],
+ [
+ "▁gelten",
+ -12.056604385375977
+ ],
+ [
+ "▁ports",
+ -12.056645393371582
+ ],
+ [
+ "Invest",
+ -12.056693077087402
+ ],
+ [
+ "ésormais",
+ -12.056795120239258
+ ],
+ [
+ "schauen",
+ -12.056880950927734
+ ],
+ [
+ "▁Command",
+ -12.056958198547363
+ ],
+ [
+ "▁alternate",
+ -12.05709171295166
+ ],
+ [
+ "citation",
+ -12.05713939666748
+ ],
+ [
+ "évolution",
+ -12.05714225769043
+ ],
+ [
+ "▁Maine",
+ -12.057145118713379
+ ],
+ [
+ "pflege",
+ -12.057174682617188
+ ],
+ [
+ "2011",
+ -12.057343482971191
+ ],
+ [
+ "▁Ground",
+ -12.057364463806152
+ ],
+ [
+ "▁ghost",
+ -12.057418823242188
+ ],
+ [
+ "lebt",
+ -12.057530403137207
+ ],
+ [
+ "▁scenarios",
+ -12.057595252990723
+ ],
+ [
+ "▁mall",
+ -12.057634353637695
+ ],
+ [
+ "▁Kings",
+ -12.057653427124023
+ ],
+ [
+ "▁15%",
+ -12.057848930358887
+ ],
+ [
+ "▁Paint",
+ -12.057848930358887
+ ],
+ [
+ "FD",
+ -12.057849884033203
+ ],
+ [
+ "ugg",
+ -12.058011054992676
+ ],
+ [
+ "▁Leon",
+ -12.058023452758789
+ ],
+ [
+ "▁grows",
+ -12.058135032653809
+ ],
+ [
+ "▁pharmacy",
+ -12.058384895324707
+ ],
+ [
+ "▁situat",
+ -12.0584135055542
+ ],
+ [
+ "20,000",
+ -12.05855941772461
+ ],
+ [
+ "▁10,000",
+ -12.058760643005371
+ ],
+ [
+ "▁membre",
+ -12.058771133422852
+ ],
+ [
+ "▁facilement",
+ -12.058806419372559
+ ],
+ [
+ "▁Analytics",
+ -12.058915138244629
+ ],
+ [
+ "▁Marvel",
+ -12.058930397033691
+ ],
+ [
+ "▁survived",
+ -12.059097290039062
+ ],
+ [
+ "▁conviction",
+ -12.059124946594238
+ ],
+ [
+ "▁Produktion",
+ -12.059260368347168
+ ],
+ [
+ "▁professionally",
+ -12.059293746948242
+ ],
+ [
+ "▁contributor",
+ -12.059486389160156
+ ],
+ [
+ "▁Kurs",
+ -12.059503555297852
+ ],
+ [
+ "▁humor",
+ -12.059549331665039
+ ],
+ [
+ "▁cinci",
+ -12.059609413146973
+ ],
+ [
+ "▁Different",
+ -12.059670448303223
+ ],
+ [
+ "▁Verarbeitung",
+ -12.059800148010254
+ ],
+ [
+ "▁inexpensive",
+ -12.059800148010254
+ ],
+ [
+ "▁sortie",
+ -12.05980110168457
+ ],
+ [
+ "▁thankful",
+ -12.059951782226562
+ ],
+ [
+ "▁vacances",
+ -12.059978485107422
+ ],
+ [
+ "▁vergangen",
+ -12.059979438781738
+ ],
+ [
+ "▁wings",
+ -12.05998420715332
+ ],
+ [
+ "▁nano",
+ -12.06003475189209
+ ],
+ [
+ "▁touches",
+ -12.060088157653809
+ ],
+ [
+ "▁Notice",
+ -12.060348510742188
+ ],
+ [
+ "▁reprezinta",
+ -12.060466766357422
+ ],
+ [
+ "▁rewarding",
+ -12.060555458068848
+ ],
+ [
+ "▁Kurz",
+ -12.060580253601074
+ ],
+ [
+ "▁mega",
+ -12.060611724853516
+ ],
+ [
+ "▁secrets",
+ -12.060646057128906
+ ],
+ [
+ "▁vorher",
+ -12.060667037963867
+ ],
+ [
+ "▁crescut",
+ -12.06074333190918
+ ],
+ [
+ "▁coordination",
+ -12.060754776000977
+ ],
+ [
+ "▁dissertation",
+ -12.060863494873047
+ ],
+ [
+ "▁header",
+ -12.060873985290527
+ ],
+ [
+ "existent",
+ -12.061070442199707
+ ],
+ [
+ "thal",
+ -12.061185836791992
+ ],
+ [
+ "▁translate",
+ -12.061214447021484
+ ],
+ [
+ "vertrag",
+ -12.06124210357666
+ ],
+ [
+ "GU",
+ -12.06126594543457
+ ],
+ [
+ "▁Arthur",
+ -12.061315536499023
+ ],
+ [
+ "wahl",
+ -12.061534881591797
+ ],
+ [
+ "▁octobre",
+ -12.061573028564453
+ ],
+ [
+ "▁bother",
+ -12.06157398223877
+ ],
+ [
+ "▁pencil",
+ -12.061580657958984
+ ],
+ [
+ "▁Dyna",
+ -12.061604499816895
+ ],
+ [
+ "▁complimentary",
+ -12.061651229858398
+ ],
+ [
+ "écoute",
+ -12.061676979064941
+ ],
+ [
+ "PB",
+ -12.061722755432129
+ ],
+ [
+ "▁independently",
+ -12.061759948730469
+ ],
+ [
+ "▁targeting",
+ -12.061840057373047
+ ],
+ [
+ "fought",
+ -12.061944961547852
+ ],
+ [
+ "mental",
+ -12.062112808227539
+ ],
+ [
+ "▁Veranstaltung",
+ -12.062300682067871
+ ],
+ [
+ "▁tatsächlich",
+ -12.062314987182617
+ ],
+ [
+ "▁Features",
+ -12.0625
+ ],
+ [
+ "▁1920",
+ -12.062554359436035
+ ],
+ [
+ "▁Domain",
+ -12.062885284423828
+ ],
+ [
+ "▁rally",
+ -12.062901496887207
+ ],
+ [
+ "▁iunie",
+ -12.063036918640137
+ ],
+ [
+ "▁fabrics",
+ -12.063070297241211
+ ],
+ [
+ "▁mint",
+ -12.063331604003906
+ ],
+ [
+ "▁antioxidant",
+ -12.063347816467285
+ ],
+ [
+ "hut",
+ -12.063432693481445
+ ],
+ [
+ "EPA",
+ -12.063496589660645
+ ],
+ [
+ "▁rigid",
+ -12.063498497009277
+ ],
+ [
+ "▁evit",
+ -12.063549995422363
+ ],
+ [
+ "▁personnage",
+ -12.063977241516113
+ ],
+ [
+ "▁garanti",
+ -12.0640287399292
+ ],
+ [
+ "▁Hä",
+ -12.064042091369629
+ ],
+ [
+ "▁Days",
+ -12.064048767089844
+ ],
+ [
+ "boarding",
+ -12.064050674438477
+ ],
+ [
+ "jemand",
+ -12.064166069030762
+ ],
+ [
+ "▁Pos",
+ -12.064262390136719
+ ],
+ [
+ "▁wool",
+ -12.064288139343262
+ ],
+ [
+ "▁boom",
+ -12.064349174499512
+ ],
+ [
+ "▁wichtige",
+ -12.06447982788086
+ ],
+ [
+ "▁emerged",
+ -12.064517974853516
+ ],
+ [
+ "▁smoothly",
+ -12.064802169799805
+ ],
+ [
+ "▁Interview",
+ -12.064942359924316
+ ],
+ [
+ "gemäß",
+ -12.06505012512207
+ ],
+ [
+ "▁suivi",
+ -12.065064430236816
+ ],
+ [
+ "▁missions",
+ -12.065129280090332
+ ],
+ [
+ "▁Kreis",
+ -12.065328598022461
+ ],
+ [
+ "century",
+ -12.065348625183105
+ ],
+ [
+ "▁tuned",
+ -12.065370559692383
+ ],
+ [
+ "isieren",
+ -12.065407752990723
+ ],
+ [
+ "▁Branch",
+ -12.065427780151367
+ ],
+ [
+ "▁Russell",
+ -12.065483093261719
+ ],
+ [
+ "▁**",
+ -12.065519332885742
+ ],
+ [
+ "▁Lehr",
+ -12.065617561340332
+ ],
+ [
+ "▁perspectives",
+ -12.065690040588379
+ ],
+ [
+ "▁handed",
+ -12.06570816040039
+ ],
+ [
+ "▁apporte",
+ -12.065743446350098
+ ],
+ [
+ "unta",
+ -12.065959930419922
+ ],
+ [
+ "▁contemplat",
+ -12.066255569458008
+ ],
+ [
+ "riel",
+ -12.06633472442627
+ ],
+ [
+ "▁freely",
+ -12.066341400146484
+ ],
+ [
+ "▁loyal",
+ -12.066451072692871
+ ],
+ [
+ "▁evolved",
+ -12.066518783569336
+ ],
+ [
+ "▁Cafe",
+ -12.066548347473145
+ ],
+ [
+ "▁assignments",
+ -12.066598892211914
+ ],
+ [
+ "▁Cream",
+ -12.066718101501465
+ ],
+ [
+ "▁Build",
+ -12.066731452941895
+ ],
+ [
+ "▁exams",
+ -12.066746711730957
+ ],
+ [
+ "▁graduation",
+ -12.066765785217285
+ ],
+ [
+ "▁Dining",
+ -12.066773414611816
+ ],
+ [
+ "inne",
+ -12.06684398651123
+ ],
+ [
+ "▁propriu",
+ -12.067055702209473
+ ],
+ [
+ "▁accordingly",
+ -12.067241668701172
+ ],
+ [
+ "▁seniors",
+ -12.067484855651855
+ ],
+ [
+ "▁sisters",
+ -12.067505836486816
+ ],
+ [
+ "formerly",
+ -12.067658424377441
+ ],
+ [
+ "▁fleur",
+ -12.067702293395996
+ ],
+ [
+ "▁alten",
+ -12.067802429199219
+ ],
+ [
+ "▁Gefühl",
+ -12.06797981262207
+ ],
+ [
+ "▁freeze",
+ -12.068222045898438
+ ],
+ [
+ "▁structured",
+ -12.068312644958496
+ ],
+ [
+ "▁reserved",
+ -12.068367004394531
+ ],
+ [
+ "stellt",
+ -12.068638801574707
+ ],
+ [
+ "▁foto",
+ -12.068668365478516
+ ],
+ [
+ "linger",
+ -12.06871223449707
+ ],
+ [
+ "▁profiter",
+ -12.068737030029297
+ ],
+ [
+ "▁trup",
+ -12.068862915039062
+ ],
+ [
+ "▁Hunter",
+ -12.068974494934082
+ ],
+ [
+ "▁widespread",
+ -12.069050788879395
+ ],
+ [
+ "entretien",
+ -12.069242477416992
+ ],
+ [
+ "▁Truck",
+ -12.06958293914795
+ ],
+ [
+ "Can",
+ -12.069656372070312
+ ],
+ [
+ "péri",
+ -12.06976318359375
+ ],
+ [
+ "▁>>",
+ -12.069926261901855
+ ],
+ [
+ "▁trains",
+ -12.070141792297363
+ ],
+ [
+ "▁faca",
+ -12.070149421691895
+ ],
+ [
+ "▁Patienten",
+ -12.070170402526855
+ ],
+ [
+ "▁scor",
+ -12.070361137390137
+ ],
+ [
+ "▁perceived",
+ -12.070384979248047
+ ],
+ [
+ "setzung",
+ -12.070393562316895
+ ],
+ [
+ "▁Robin",
+ -12.070558547973633
+ ],
+ [
+ "▁geboren",
+ -12.07060718536377
+ ],
+ [
+ "lons",
+ -12.070687294006348
+ ],
+ [
+ "inţa",
+ -12.070836067199707
+ ],
+ [
+ "glob",
+ -12.070887565612793
+ ],
+ [
+ "subsequently",
+ -12.07111930847168
+ ],
+ [
+ "▁vet",
+ -12.071170806884766
+ ],
+ [
+ "▁Holland",
+ -12.071328163146973
+ ],
+ [
+ "▁Clinical",
+ -12.071370124816895
+ ],
+ [
+ "▁uncertainty",
+ -12.071381568908691
+ ],
+ [
+ "hohen",
+ -12.071386337280273
+ ],
+ [
+ "uza",
+ -12.071431159973145
+ ],
+ [
+ "▁kleiner",
+ -12.071518898010254
+ ],
+ [
+ "▁substances",
+ -12.07155704498291
+ ],
+ [
+ "ados",
+ -12.071627616882324
+ ],
+ [
+ "wheel",
+ -12.07178020477295
+ ],
+ [
+ "▁cone",
+ -12.071990966796875
+ ],
+ [
+ "▁castig",
+ -12.072218894958496
+ ],
+ [
+ "▁Conditions",
+ -12.072242736816406
+ ],
+ [
+ "minus",
+ -12.072643280029297
+ ],
+ [
+ "▁permits",
+ -12.07265853881836
+ ],
+ [
+ "fond",
+ -12.072784423828125
+ ],
+ [
+ "▁reactions",
+ -12.07278823852539
+ ],
+ [
+ "▁Mario",
+ -12.072819709777832
+ ],
+ [
+ "▁materiale",
+ -12.07291030883789
+ ],
+ [
+ "AH",
+ -12.072924613952637
+ ],
+ [
+ "▁juillet",
+ -12.073172569274902
+ ],
+ [
+ "▁juridic",
+ -12.073182106018066
+ ],
+ [
+ "▁dropping",
+ -12.073200225830078
+ ],
+ [
+ "expérience",
+ -12.073225021362305
+ ],
+ [
+ "▁depot",
+ -12.073345184326172
+ ],
+ [
+ "▁plea",
+ -12.073490142822266
+ ],
+ [
+ "dezvoltarea",
+ -12.073512077331543
+ ],
+ [
+ "▁Independent",
+ -12.07363224029541
+ ],
+ [
+ "▁Homes",
+ -12.073674201965332
+ ],
+ [
+ "▁crust",
+ -12.073808670043945
+ ],
+ [
+ "▁pillow",
+ -12.073899269104004
+ ],
+ [
+ "kreis",
+ -12.073920249938965
+ ],
+ [
+ "▁boiler",
+ -12.073928833007812
+ ],
+ [
+ "latin",
+ -12.073978424072266
+ ],
+ [
+ "▁stet",
+ -12.074131965637207
+ ],
+ [
+ "GH",
+ -12.074143409729004
+ ],
+ [
+ "▁absent",
+ -12.074334144592285
+ ],
+ [
+ "▁Directors",
+ -12.074501037597656
+ ],
+ [
+ "zwischen",
+ -12.07462215423584
+ ],
+ [
+ "▁comprendre",
+ -12.07465648651123
+ ],
+ [
+ "▁25,",
+ -12.074832916259766
+ ],
+ [
+ "▁pharmaceutical",
+ -12.075145721435547
+ ],
+ [
+ "▁placeholder",
+ -12.075174331665039
+ ],
+ [
+ "KI",
+ -12.075176239013672
+ ],
+ [
+ "▁români",
+ -12.07540225982666
+ ],
+ [
+ "▁Dollar",
+ -12.075509071350098
+ ],
+ [
+ "▁Operations",
+ -12.075525283813477
+ ],
+ [
+ "▁Dublin",
+ -12.075550079345703
+ ],
+ [
+ "▁drawings",
+ -12.0756196975708
+ ],
+ [
+ "▁respir",
+ -12.075769424438477
+ ],
+ [
+ "▁haul",
+ -12.0758056640625
+ ],
+ [
+ "Obviously",
+ -12.075864791870117
+ ],
+ [
+ "▁Beat",
+ -12.075864791870117
+ ],
+ [
+ "▁jeans",
+ -12.07590103149414
+ ],
+ [
+ "▁Masters",
+ -12.075927734375
+ ],
+ [
+ "▁bits",
+ -12.076213836669922
+ ],
+ [
+ "poți",
+ -12.076226234436035
+ ],
+ [
+ "▁asigur",
+ -12.076228141784668
+ ],
+ [
+ "▁intampla",
+ -12.076228141784668
+ ],
+ [
+ "▁marc",
+ -12.076282501220703
+ ],
+ [
+ "......",
+ -12.076404571533203
+ ],
+ [
+ "▁districts",
+ -12.076437950134277
+ ],
+ [
+ "cru",
+ -12.076457023620605
+ ],
+ [
+ "nav",
+ -12.076608657836914
+ ],
+ [
+ "huile",
+ -12.076644897460938
+ ],
+ [
+ "▁limitation",
+ -12.076647758483887
+ ],
+ [
+ "boat",
+ -12.076712608337402
+ ],
+ [
+ "IRE",
+ -12.076720237731934
+ ],
+ [
+ "Unis",
+ -12.07675838470459
+ ],
+ [
+ "dated",
+ -12.0769624710083
+ ],
+ [
+ "▁consultants",
+ -12.07699203491211
+ ],
+ [
+ "▁Josh",
+ -12.077007293701172
+ ],
+ [
+ "tanz",
+ -12.077184677124023
+ ],
+ [
+ "launching",
+ -12.0772066116333
+ ],
+ [
+ "▁browsing",
+ -12.077310562133789
+ ],
+ [
+ "▁incerc",
+ -12.077314376831055
+ ],
+ [
+ "▁27,",
+ -12.077375411987305
+ ],
+ [
+ "не",
+ -12.077398300170898
+ ],
+ [
+ "wig",
+ -12.077415466308594
+ ],
+ [
+ "▁spar",
+ -12.077458381652832
+ ],
+ [
+ "▁token",
+ -12.077547073364258
+ ],
+ [
+ "▁09",
+ -12.077548027038574
+ ],
+ [
+ "spa",
+ -12.07766056060791
+ ],
+ [
+ "ometer",
+ -12.07772159576416
+ ],
+ [
+ "▁riders",
+ -12.077869415283203
+ ],
+ [
+ "▁Drop",
+ -12.077898979187012
+ ],
+ [
+ "RN",
+ -12.078103065490723
+ ],
+ [
+ "▁pairs",
+ -12.07815933227539
+ ],
+ [
+ "▁psychology",
+ -12.078420639038086
+ ],
+ [
+ "▁Douglas",
+ -12.078437805175781
+ ],
+ [
+ "▁verwenden",
+ -12.078516960144043
+ ],
+ [
+ "▁(9",
+ -12.07857894897461
+ ],
+ [
+ "▁Rental",
+ -12.078728675842285
+ ],
+ [
+ "▁délai",
+ -12.078847885131836
+ ],
+ [
+ "▁sooner",
+ -12.078882217407227
+ ],
+ [
+ "▁bankruptcy",
+ -12.079109191894531
+ ],
+ [
+ "04.",
+ -12.079110145568848
+ ],
+ [
+ "abend",
+ -12.079194068908691
+ ],
+ [
+ "çon",
+ -12.079237937927246
+ ],
+ [
+ "▁Ple",
+ -12.079243659973145
+ ],
+ [
+ "fug",
+ -12.079337120056152
+ ],
+ [
+ "▁Wohnung",
+ -12.079410552978516
+ ],
+ [
+ "▁Preise",
+ -12.079424858093262
+ ],
+ [
+ "▁Kay",
+ -12.079427719116211
+ ],
+ [
+ "▁notify",
+ -12.079474449157715
+ ],
+ [
+ "▁Brain",
+ -12.079534530639648
+ ],
+ [
+ "▁optical",
+ -12.079580307006836
+ ],
+ [
+ "▁modifications",
+ -12.079727172851562
+ ],
+ [
+ "▁repos",
+ -12.07999324798584
+ ],
+ [
+ "▁worksheet",
+ -12.0800142288208
+ ],
+ [
+ "continu",
+ -12.08005428314209
+ ],
+ [
+ "▁assumed",
+ -12.08059024810791
+ ],
+ [
+ "varying",
+ -12.080626487731934
+ ],
+ [
+ "feier",
+ -12.080643653869629
+ ],
+ [
+ "▁Freedom",
+ -12.080717086791992
+ ],
+ [
+ "▁Inhalte",
+ -12.080740928649902
+ ],
+ [
+ "▁observations",
+ -12.080755233764648
+ ],
+ [
+ "▁Gruppe",
+ -12.080791473388672
+ ],
+ [
+ "▁Cyber",
+ -12.080883979797363
+ ],
+ [
+ "hort",
+ -12.080889701843262
+ ],
+ [
+ "▁langue",
+ -12.080915451049805
+ ],
+ [
+ "führen",
+ -12.08110523223877
+ ],
+ [
+ "ganze",
+ -12.081254005432129
+ ],
+ [
+ "▁forte",
+ -12.081327438354492
+ ],
+ [
+ "▁Stefan",
+ -12.081376075744629
+ ],
+ [
+ "▁Jetzt",
+ -12.081463813781738
+ ],
+ [
+ "mehr",
+ -12.081489562988281
+ ],
+ [
+ "trip",
+ -12.081549644470215
+ ],
+ [
+ "▁poem",
+ -12.081583976745605
+ ],
+ [
+ "▁practitioners",
+ -12.081720352172852
+ ],
+ [
+ "▁connector",
+ -12.08177661895752
+ ],
+ [
+ "ECT",
+ -12.081794738769531
+ ],
+ [
+ "▁inseamna",
+ -12.081820487976074
+ ],
+ [
+ "addressing",
+ -12.081867218017578
+ ],
+ [
+ "▁beliebt",
+ -12.081908226013184
+ ],
+ [
+ "▁Mama",
+ -12.082002639770508
+ ],
+ [
+ "▁fade",
+ -12.08204460144043
+ ],
+ [
+ "messen",
+ -12.08205509185791
+ ],
+ [
+ "▁Visa",
+ -12.082080841064453
+ ],
+ [
+ "▁Meta",
+ -12.082154273986816
+ ],
+ [
+ "lene",
+ -12.082188606262207
+ ],
+ [
+ "▁remembered",
+ -12.082334518432617
+ ],
+ [
+ "/3",
+ -12.082337379455566
+ ],
+ [
+ "apte",
+ -12.082347869873047
+ ],
+ [
+ "▁uncomfortable",
+ -12.082364082336426
+ ],
+ [
+ "▁romance",
+ -12.08253002166748
+ ],
+ [
+ "▁réalis",
+ -12.082601547241211
+ ],
+ [
+ "▁Vincent",
+ -12.082706451416016
+ ],
+ [
+ "▁ABC",
+ -12.08275318145752
+ ],
+ [
+ "▁handicap",
+ -12.082756042480469
+ ],
+ [
+ "▁Shin",
+ -12.082801818847656
+ ],
+ [
+ "▁Hunde",
+ -12.082847595214844
+ ],
+ [
+ "▁Ach",
+ -12.083131790161133
+ ],
+ [
+ "▁Questions",
+ -12.083136558532715
+ ],
+ [
+ "▁particles",
+ -12.083226203918457
+ ],
+ [
+ "usch",
+ -12.083230018615723
+ ],
+ [
+ "▁SUV",
+ -12.083279609680176
+ ],
+ [
+ "▁Tous",
+ -12.083301544189453
+ ],
+ [
+ "▁empower",
+ -12.08336067199707
+ ],
+ [
+ "▁Yi",
+ -12.083446502685547
+ ],
+ [
+ "▁LinkedIn",
+ -12.083453178405762
+ ],
+ [
+ "▁Profile",
+ -12.083507537841797
+ ],
+ [
+ "▁surround",
+ -12.083553314208984
+ ],
+ [
+ "▁wh",
+ -12.083560943603516
+ ],
+ [
+ "▁Weiter",
+ -12.083577156066895
+ ],
+ [
+ "▁Weight",
+ -12.083672523498535
+ ],
+ [
+ "▁creatures",
+ -12.083807945251465
+ ],
+ [
+ "Especially",
+ -12.08381462097168
+ ],
+ [
+ "▁repede",
+ -12.08383560180664
+ ],
+ [
+ "▁albums",
+ -12.083885192871094
+ ],
+ [
+ "▁compatibil",
+ -12.0839204788208
+ ],
+ [
+ "▁Interesse",
+ -12.083929061889648
+ ],
+ [
+ "abili",
+ -12.084062576293945
+ ],
+ [
+ "▁roast",
+ -12.084310531616211
+ ],
+ [
+ "▁unii",
+ -12.084310531616211
+ ],
+ [
+ "▁Glad",
+ -12.084421157836914
+ ],
+ [
+ "▁enthusiasm",
+ -12.084539413452148
+ ],
+ [
+ "▁whisk",
+ -12.084547996520996
+ ],
+ [
+ "▁freezer",
+ -12.084712982177734
+ ],
+ [
+ "▁stolen",
+ -12.084715843200684
+ ],
+ [
+ "▁neighbour",
+ -12.084883689880371
+ ],
+ [
+ "▁sake",
+ -12.084967613220215
+ ],
+ [
+ "▁Effect",
+ -12.0850191116333
+ ],
+ [
+ "▁fighter",
+ -12.085044860839844
+ ],
+ [
+ "▁tranquil",
+ -12.085084915161133
+ ],
+ [
+ "▁organizer",
+ -12.085199356079102
+ ],
+ [
+ "pixel",
+ -12.085306167602539
+ ],
+ [
+ "▁Guest",
+ -12.085338592529297
+ ],
+ [
+ "▁Philipp",
+ -12.085369110107422
+ ],
+ [
+ "kunft",
+ -12.085382461547852
+ ],
+ [
+ "▁Meer",
+ -12.085409164428711
+ ],
+ [
+ "▁inviting",
+ -12.085432052612305
+ ],
+ [
+ "gänge",
+ -12.085450172424316
+ ],
+ [
+ "▁Position",
+ -12.085627555847168
+ ],
+ [
+ "giving",
+ -12.085693359375
+ ],
+ [
+ "▁marble",
+ -12.085807800292969
+ ],
+ [
+ "▁neg",
+ -12.085813522338867
+ ],
+ [
+ "▁Haar",
+ -12.085914611816406
+ ],
+ [
+ "Ein",
+ -12.086039543151855
+ ],
+ [
+ "▁buses",
+ -12.086187362670898
+ ],
+ [
+ "▁Lodge",
+ -12.086188316345215
+ ],
+ [
+ "soare",
+ -12.086319923400879
+ ],
+ [
+ "▁Barn",
+ -12.086409568786621
+ ],
+ [
+ "▁captain",
+ -12.086527824401855
+ ],
+ [
+ "▁Fix",
+ -12.08657169342041
+ ],
+ [
+ "ulate",
+ -12.086629867553711
+ ],
+ [
+ "ență",
+ -12.086709022521973
+ ],
+ [
+ "▁finances",
+ -12.086770057678223
+ ],
+ [
+ "▁VIP",
+ -12.086800575256348
+ ],
+ [
+ "▁Adams",
+ -12.086801528930664
+ ],
+ [
+ "▁spécialisé",
+ -12.086960792541504
+ ],
+ [
+ "▁fortunate",
+ -12.087236404418945
+ ],
+ [
+ "ility",
+ -12.087345123291016
+ ],
+ [
+ "▁democracy",
+ -12.08749771118164
+ ],
+ [
+ "shu",
+ -12.087580680847168
+ ],
+ [
+ "▁consiste",
+ -12.087624549865723
+ ],
+ [
+ "▁tort",
+ -12.087692260742188
+ ],
+ [
+ "▁branding",
+ -12.087793350219727
+ ],
+ [
+ "▁porch",
+ -12.08780288696289
+ ],
+ [
+ "UNI",
+ -12.087867736816406
+ ],
+ [
+ "▁placut",
+ -12.087915420532227
+ ],
+ [
+ "▁coupled",
+ -12.088058471679688
+ ],
+ [
+ "▁ministre",
+ -12.088187217712402
+ ],
+ [
+ "▁minerals",
+ -12.088335037231445
+ ],
+ [
+ "▁safer",
+ -12.088335990905762
+ ],
+ [
+ "▁outlets",
+ -12.088438034057617
+ ],
+ [
+ "▁caution",
+ -12.08864688873291
+ ],
+ [
+ "▁lightly",
+ -12.0886869430542
+ ],
+ [
+ "▁utilizator",
+ -12.088700294494629
+ ],
+ [
+ "▁Pala",
+ -12.088959693908691
+ ],
+ [
+ "▁doll",
+ -12.088961601257324
+ ],
+ [
+ "(1)",
+ -12.089065551757812
+ ],
+ [
+ "chol",
+ -12.089120864868164
+ ],
+ [
+ "▁Left",
+ -12.08919620513916
+ ],
+ [
+ "▁roulant",
+ -12.089277267456055
+ ],
+ [
+ "▁propune",
+ -12.089301109313965
+ ],
+ [
+ "▁Cred",
+ -12.089339256286621
+ ],
+ [
+ "▁negotiations",
+ -12.089362144470215
+ ],
+ [
+ "amba",
+ -12.089393615722656
+ ],
+ [
+ "▁grasp",
+ -12.089420318603516
+ ],
+ [
+ "▁Amsterdam",
+ -12.089451789855957
+ ],
+ [
+ "▁Zweck",
+ -12.08945369720459
+ ],
+ [
+ "▁conven",
+ -12.089563369750977
+ ],
+ [
+ "▁organizing",
+ -12.089574813842773
+ ],
+ [
+ "section",
+ -12.089618682861328
+ ],
+ [
+ "▁endeavor",
+ -12.089634895324707
+ ],
+ [
+ "▁basics",
+ -12.089722633361816
+ ],
+ [
+ "jud",
+ -12.089874267578125
+ ],
+ [
+ "▁yarn",
+ -12.090049743652344
+ ],
+ [
+ "▁shout",
+ -12.09009075164795
+ ],
+ [
+ "fällt",
+ -12.090285301208496
+ ],
+ [
+ "▁dragoste",
+ -12.09054946899414
+ ],
+ [
+ "▁Rein",
+ -12.090594291687012
+ ],
+ [
+ "Cal",
+ -12.090688705444336
+ ],
+ [
+ "▁deaths",
+ -12.090729713439941
+ ],
+ [
+ "▁24,",
+ -12.0907564163208
+ ],
+ [
+ "▁măr",
+ -12.090773582458496
+ ],
+ [
+ "server",
+ -12.090825080871582
+ ],
+ [
+ "▁explic",
+ -12.09085464477539
+ ],
+ [
+ "▁sufer",
+ -12.090903282165527
+ ],
+ [
+ "▁lucrări",
+ -12.091097831726074
+ ],
+ [
+ "▁Disease",
+ -12.091126441955566
+ ],
+ [
+ "▁prescribed",
+ -12.091194152832031
+ ],
+ [
+ "prozess",
+ -12.091285705566406
+ ],
+ [
+ "▁dessin",
+ -12.091343879699707
+ ],
+ [
+ "▁refuge",
+ -12.091473579406738
+ ],
+ [
+ "▁cope",
+ -12.091631889343262
+ ],
+ [
+ "pole",
+ -12.09196949005127
+ ],
+ [
+ "▁vacant",
+ -12.091984748840332
+ ],
+ [
+ "▁sezon",
+ -12.092035293579102
+ ],
+ [
+ "▁Carbon",
+ -12.092227935791016
+ ],
+ [
+ "▁goût",
+ -12.092233657836914
+ ],
+ [
+ "Ste",
+ -12.092320442199707
+ ],
+ [
+ "▁surroundings",
+ -12.092754364013672
+ ],
+ [
+ "definite",
+ -12.09284496307373
+ ],
+ [
+ "▁adaptation",
+ -12.093358993530273
+ ],
+ [
+ "cteur",
+ -12.0933837890625
+ ],
+ [
+ "System",
+ -12.093442916870117
+ ],
+ [
+ "▁Burg",
+ -12.093550682067871
+ ],
+ [
+ "▁retention",
+ -12.093579292297363
+ ],
+ [
+ "examen",
+ -12.093618392944336
+ ],
+ [
+ "▁adjustments",
+ -12.093668937683105
+ ],
+ [
+ "nies",
+ -12.094213485717773
+ ],
+ [
+ "▁RSS",
+ -12.094215393066406
+ ],
+ [
+ "▁Umwelt",
+ -12.094259262084961
+ ],
+ [
+ "▁strengths",
+ -12.094326972961426
+ ],
+ [
+ "loom",
+ -12.094401359558105
+ ],
+ [
+ "▁pics",
+ -12.094404220581055
+ ],
+ [
+ "phase",
+ -12.09443187713623
+ ],
+ [
+ "▁Poland",
+ -12.094472885131836
+ ],
+ [
+ "▁practicing",
+ -12.094558715820312
+ ],
+ [
+ "monetary",
+ -12.094756126403809
+ ],
+ [
+ "▁embodiment",
+ -12.094756126403809
+ ],
+ [
+ "▁jocuri",
+ -12.094846725463867
+ ],
+ [
+ "▁impreuna",
+ -12.094939231872559
+ ],
+ [
+ "▁Lyon",
+ -12.094985961914062
+ ],
+ [
+ "keeping",
+ -12.095157623291016
+ ],
+ [
+ "▁Starting",
+ -12.095202445983887
+ ],
+ [
+ "▁începe",
+ -12.095357894897461
+ ],
+ [
+ "▁clay",
+ -12.095440864562988
+ ],
+ [
+ "bildung",
+ -12.095444679260254
+ ],
+ [
+ "Technologie",
+ -12.095513343811035
+ ],
+ [
+ "toxic",
+ -12.095624923706055
+ ],
+ [
+ "▁gasit",
+ -12.095819473266602
+ ],
+ [
+ "rott",
+ -12.095870018005371
+ ],
+ [
+ "brook",
+ -12.095935821533203
+ ],
+ [
+ "▁wann",
+ -12.096029281616211
+ ],
+ [
+ "▁lined",
+ -12.09610366821289
+ ],
+ [
+ "▁Chelsea",
+ -12.096223831176758
+ ],
+ [
+ "▁Orlando",
+ -12.096224784851074
+ ],
+ [
+ "▁Otherwise",
+ -12.096267700195312
+ ],
+ [
+ "▁debit",
+ -12.096273422241211
+ ],
+ [
+ "▁entsprechend",
+ -12.09648323059082
+ ],
+ [
+ "nism",
+ -12.09654426574707
+ ],
+ [
+ "issen",
+ -12.09664535522461
+ ],
+ [
+ "▁rendez",
+ -12.096646308898926
+ ],
+ [
+ "▁processus",
+ -12.096745491027832
+ ],
+ [
+ "mbi",
+ -12.096890449523926
+ ],
+ [
+ "▁Graduate",
+ -12.096960067749023
+ ],
+ [
+ "▁cozy",
+ -12.097119331359863
+ ],
+ [
+ "▁Freunde",
+ -12.097320556640625
+ ],
+ [
+ "▁teme",
+ -12.097389221191406
+ ],
+ [
+ "▁bias",
+ -12.097548484802246
+ ],
+ [
+ "102",
+ -12.09756851196289
+ ],
+ [
+ "terrorism",
+ -12.09770679473877
+ ],
+ [
+ "threatening",
+ -12.097756385803223
+ ],
+ [
+ "ни",
+ -12.097776412963867
+ ],
+ [
+ "▁Sonntag",
+ -12.098062515258789
+ ],
+ [
+ "▁efect",
+ -12.098116874694824
+ ],
+ [
+ "▁prayers",
+ -12.098134994506836
+ ],
+ [
+ "▁backpack",
+ -12.09841537475586
+ ],
+ [
+ "?)",
+ -12.098489761352539
+ ],
+ [
+ "▁searches",
+ -12.098788261413574
+ ],
+ [
+ "ouverture",
+ -12.09880256652832
+ ],
+ [
+ "▁sustained",
+ -12.098865509033203
+ ],
+ [
+ "hawk",
+ -12.098869323730469
+ ],
+ [
+ "messe",
+ -12.098958969116211
+ ],
+ [
+ "▁prototype",
+ -12.098989486694336
+ ],
+ [
+ "▁stră",
+ -12.09903335571289
+ ],
+ [
+ "▁Neo",
+ -12.099040985107422
+ ],
+ [
+ "▁29,",
+ -12.099109649658203
+ ],
+ [
+ "izo",
+ -12.099306106567383
+ ],
+ [
+ "▁Anton",
+ -12.099333763122559
+ ],
+ [
+ "SIS",
+ -12.099564552307129
+ ],
+ [
+ "pendant",
+ -12.099617958068848
+ ],
+ [
+ "▁passive",
+ -12.099813461303711
+ ],
+ [
+ "▁Aaron",
+ -12.099824905395508
+ ],
+ [
+ "▁Karen",
+ -12.099831581115723
+ ],
+ [
+ "▁Bildung",
+ -12.09994888305664
+ ],
+ [
+ "ario",
+ -12.099949836730957
+ ],
+ [
+ "▁regulator",
+ -12.100006103515625
+ ],
+ [
+ "gruppe",
+ -12.100032806396484
+ ],
+ [
+ "stepped",
+ -12.100053787231445
+ ],
+ [
+ "▁interventions",
+ -12.10014533996582
+ ],
+ [
+ "▁rounds",
+ -12.100149154663086
+ ],
+ [
+ "▁Khan",
+ -12.10020637512207
+ ],
+ [
+ "▁railway",
+ -12.10028076171875
+ ],
+ [
+ "▁souvenir",
+ -12.100296974182129
+ ],
+ [
+ "▁Plans",
+ -12.100336074829102
+ ],
+ [
+ "aille",
+ -12.100372314453125
+ ],
+ [
+ "▁billing",
+ -12.100473403930664
+ ],
+ [
+ "▁Spiele",
+ -12.100541114807129
+ ],
+ [
+ "▁supermarket",
+ -12.100556373596191
+ ],
+ [
+ "▁flows",
+ -12.100625991821289
+ ],
+ [
+ "▁PayPal",
+ -12.100641250610352
+ ],
+ [
+ "▁tribe",
+ -12.10067081451416
+ ],
+ [
+ "anni",
+ -12.100780487060547
+ ],
+ [
+ "▁rides",
+ -12.100934982299805
+ ],
+ [
+ "▁Orleans",
+ -12.101009368896484
+ ],
+ [
+ "▁evaluated",
+ -12.101021766662598
+ ],
+ [
+ "founder",
+ -12.10106372833252
+ ],
+ [
+ "▁Feld",
+ -12.101212501525879
+ ],
+ [
+ "▁altele",
+ -12.10122299194336
+ ],
+ [
+ "▁thermo",
+ -12.101290702819824
+ ],
+ [
+ "ugh",
+ -12.101330757141113
+ ],
+ [
+ "▁adus",
+ -12.101375579833984
+ ],
+ [
+ "▁Taiwan",
+ -12.101396560668945
+ ],
+ [
+ "▁clause",
+ -12.101409912109375
+ ],
+ [
+ "oxi",
+ -12.101465225219727
+ ],
+ [
+ "alcool",
+ -12.101495742797852
+ ],
+ [
+ "▁Noi",
+ -12.101531982421875
+ ],
+ [
+ "rub",
+ -12.101540565490723
+ ],
+ [
+ "▁dosar",
+ -12.101582527160645
+ ],
+ [
+ "▁Nelson",
+ -12.101751327514648
+ ],
+ [
+ "fassung",
+ -12.102316856384277
+ ],
+ [
+ "▁Kill",
+ -12.102489471435547
+ ],
+ [
+ "▁Standards",
+ -12.102490425109863
+ ],
+ [
+ "▁upward",
+ -12.102653503417969
+ ],
+ [
+ "▁Coloring",
+ -12.102664947509766
+ ],
+ [
+ "Designed",
+ -12.102754592895508
+ ],
+ [
+ "▁Nou",
+ -12.10281753540039
+ ],
+ [
+ "▁borrow",
+ -12.102940559387207
+ ],
+ [
+ "▁Poll",
+ -12.10321044921875
+ ],
+ [
+ "▁antibiotic",
+ -12.103277206420898
+ ],
+ [
+ "▁fabrication",
+ -12.103388786315918
+ ],
+ [
+ "quo",
+ -12.103432655334473
+ ],
+ [
+ "▁crimes",
+ -12.103464126586914
+ ],
+ [
+ "▁nahe",
+ -12.103484153747559
+ ],
+ [
+ "▁aplicat",
+ -12.103565216064453
+ ],
+ [
+ "OST",
+ -12.1035737991333
+ ],
+ [
+ "▁Beijing",
+ -12.103599548339844
+ ],
+ [
+ "fight",
+ -12.103612899780273
+ ],
+ [
+ "▁lodge",
+ -12.103612899780273
+ ],
+ [
+ "dreh",
+ -12.103922843933105
+ ],
+ [
+ "▁harness",
+ -12.104036331176758
+ ],
+ [
+ "▁noiembrie",
+ -12.104151725769043
+ ],
+ [
+ "ounded",
+ -12.104161262512207
+ ],
+ [
+ "▁Imp",
+ -12.1041841506958
+ ],
+ [
+ "▁nächste",
+ -12.104275703430176
+ ],
+ [
+ "funktion",
+ -12.104476928710938
+ ],
+ [
+ "exploitation",
+ -12.104569435119629
+ ],
+ [
+ "▁Ready",
+ -12.10457706451416
+ ],
+ [
+ "▁Plate",
+ -12.104598999023438
+ ],
+ [
+ "▁octombrie",
+ -12.104706764221191
+ ],
+ [
+ "▁considerat",
+ -12.104982376098633
+ ],
+ [
+ "▁Xbox",
+ -12.105067253112793
+ ],
+ [
+ "mind",
+ -12.105107307434082
+ ],
+ [
+ "▁Lind",
+ -12.105111122131348
+ ],
+ [
+ "runde",
+ -12.105352401733398
+ ],
+ [
+ "mination",
+ -12.105374336242676
+ ],
+ [
+ "▁memori",
+ -12.105377197265625
+ ],
+ [
+ "▁cere",
+ -12.105389595031738
+ ],
+ [
+ "barkeit",
+ -12.105517387390137
+ ],
+ [
+ "▁găsi",
+ -12.105761528015137
+ ],
+ [
+ "2.1",
+ -12.105863571166992
+ ],
+ [
+ "▁Finding",
+ -12.105891227722168
+ ],
+ [
+ "▁static",
+ -12.106405258178711
+ ],
+ [
+ "court",
+ -12.106439590454102
+ ],
+ [
+ "▁Gem",
+ -12.106489181518555
+ ],
+ [
+ "▁pièce",
+ -12.106494903564453
+ ],
+ [
+ "▁reel",
+ -12.10651969909668
+ ],
+ [
+ "▁manuscript",
+ -12.106560707092285
+ ],
+ [
+ "▁complications",
+ -12.106578826904297
+ ],
+ [
+ "▁controlling",
+ -12.106585502624512
+ ],
+ [
+ "▁favour",
+ -12.106738090515137
+ ],
+ [
+ "▁advancement",
+ -12.106739044189453
+ ],
+ [
+ "▁Radi",
+ -12.106870651245117
+ ],
+ [
+ "▁faites",
+ -12.107076644897461
+ ],
+ [
+ "▁ordin",
+ -12.107131958007812
+ ],
+ [
+ "sorted",
+ -12.107152938842773
+ ],
+ [
+ "▁1982",
+ -12.10715389251709
+ ],
+ [
+ "▁brutal",
+ -12.107154846191406
+ ],
+ [
+ "▁Guy",
+ -12.107226371765137
+ ],
+ [
+ "▁accomplishment",
+ -12.107248306274414
+ ],
+ [
+ "▁wer",
+ -12.107329368591309
+ ],
+ [
+ "▁withdraw",
+ -12.107460975646973
+ ],
+ [
+ "abilitate",
+ -12.1075439453125
+ ],
+ [
+ "▁NBA",
+ -12.107625961303711
+ ],
+ [
+ "▁Benefit",
+ -12.107675552368164
+ ],
+ [
+ "▁divide",
+ -12.107824325561523
+ ],
+ [
+ "induced",
+ -12.107913970947266
+ ],
+ [
+ "▁văzut",
+ -12.108049392700195
+ ],
+ [
+ "▁peel",
+ -12.10807991027832
+ ],
+ [
+ "▁joints",
+ -12.108160972595215
+ ],
+ [
+ "▁enthalten",
+ -12.108301162719727
+ ],
+ [
+ "▁spy",
+ -12.108397483825684
+ ],
+ [
+ "▁occasional",
+ -12.108437538146973
+ ],
+ [
+ "warm",
+ -12.108514785766602
+ ],
+ [
+ "ême",
+ -12.108542442321777
+ ],
+ [
+ "▁Betriebs",
+ -12.108551979064941
+ ],
+ [
+ "▁Ioan",
+ -12.1087064743042
+ ],
+ [
+ "▁balloon",
+ -12.108809471130371
+ ],
+ [
+ "▁leap",
+ -12.108869552612305
+ ],
+ [
+ "pelled",
+ -12.109000205993652
+ ],
+ [
+ "▁realise",
+ -12.109073638916016
+ ],
+ [
+ "▁Retail",
+ -12.109118461608887
+ ],
+ [
+ "▁Farben",
+ -12.109151840209961
+ ],
+ [
+ "▁Kennedy",
+ -12.10916519165039
+ ],
+ [
+ "▁Firma",
+ -12.109196662902832
+ ],
+ [
+ "▁tineri",
+ -12.10934066772461
+ ],
+ [
+ "tub",
+ -12.109354019165039
+ ],
+ [
+ "PORT",
+ -12.109381675720215
+ ],
+ [
+ "▁stiff",
+ -12.109416007995605
+ ],
+ [
+ "▁notable",
+ -12.109476089477539
+ ],
+ [
+ "tler",
+ -12.109498023986816
+ ],
+ [
+ "▁utile",
+ -12.10958480834961
+ ],
+ [
+ "▁jouer",
+ -12.109674453735352
+ ],
+ [
+ "▁Primary",
+ -12.109735488891602
+ ],
+ [
+ "▁retailer",
+ -12.109764099121094
+ ],
+ [
+ "▁jederzeit",
+ -12.109808921813965
+ ],
+ [
+ "▁amend",
+ -12.109817504882812
+ ],
+ [
+ "▁sagte",
+ -12.109845161437988
+ ],
+ [
+ "atch",
+ -12.10995864868164
+ ],
+ [
+ "ution",
+ -12.110008239746094
+ ],
+ [
+ "once",
+ -12.110018730163574
+ ],
+ [
+ "ended",
+ -12.1100435256958
+ ],
+ [
+ "▁literary",
+ -12.11013126373291
+ ],
+ [
+ "▁wrist",
+ -12.110281944274902
+ ],
+ [
+ "vii",
+ -12.11036205291748
+ ],
+ [
+ "scriere",
+ -12.110367774963379
+ ],
+ [
+ "▁compassion",
+ -12.110443115234375
+ ],
+ [
+ "▁Milan",
+ -12.110474586486816
+ ],
+ [
+ "▁Dach",
+ -12.110490798950195
+ ],
+ [
+ "▁problèmes",
+ -12.110630989074707
+ ],
+ [
+ "▁Pré",
+ -12.110687255859375
+ ],
+ [
+ "▁Feder",
+ -12.110759735107422
+ ],
+ [
+ "Dr",
+ -12.110814094543457
+ ],
+ [
+ "Spr",
+ -12.110908508300781
+ ],
+ [
+ "▁né",
+ -12.110969543457031
+ ],
+ [
+ "François",
+ -12.111023902893066
+ ],
+ [
+ "▁Shu",
+ -12.111115455627441
+ ],
+ [
+ "▁poison",
+ -12.111154556274414
+ ],
+ [
+ "zier",
+ -12.111176490783691
+ ],
+ [
+ "▁attain",
+ -12.11124038696289
+ ],
+ [
+ "▁switching",
+ -12.111310958862305
+ ],
+ [
+ "▁vibration",
+ -12.111348152160645
+ ],
+ [
+ "▁Tablet",
+ -12.11136531829834
+ ],
+ [
+ "▁Lern",
+ -12.11148452758789
+ ],
+ [
+ "offrir",
+ -12.111660957336426
+ ],
+ [
+ "123",
+ -12.11168098449707
+ ],
+ [
+ "cheapest",
+ -12.11173152923584
+ ],
+ [
+ "▁numărul",
+ -12.111764907836914
+ ],
+ [
+ "break",
+ -12.11180305480957
+ ],
+ [
+ "cyto",
+ -12.111836433410645
+ ],
+ [
+ "▁Mississippi",
+ -12.111955642700195
+ ],
+ [
+ "▁dragon",
+ -12.11207389831543
+ ],
+ [
+ "fir",
+ -12.112176895141602
+ ],
+ [
+ "▁fête",
+ -12.112180709838867
+ ],
+ [
+ "▁Wait",
+ -12.112350463867188
+ ],
+ [
+ "buy",
+ -12.112359046936035
+ ],
+ [
+ "având",
+ -12.112391471862793
+ ],
+ [
+ "▁Scar",
+ -12.112517356872559
+ ],
+ [
+ "▁Hund",
+ -12.112586975097656
+ ],
+ [
+ "bug",
+ -12.112807273864746
+ ],
+ [
+ "▁classique",
+ -12.112811088562012
+ ],
+ [
+ "▁tenant",
+ -12.112860679626465
+ ],
+ [
+ "▁Walt",
+ -12.11296272277832
+ ],
+ [
+ "▁timber",
+ -12.11296272277832
+ ],
+ [
+ "inscription",
+ -12.11300277709961
+ ],
+ [
+ "BD",
+ -12.113016128540039
+ ],
+ [
+ "▁Commissioner",
+ -12.113018989562988
+ ],
+ [
+ "▁casinos",
+ -12.11306095123291
+ ],
+ [
+ "▁prochain",
+ -12.113168716430664
+ ],
+ [
+ "▁rustic",
+ -12.11349868774414
+ ],
+ [
+ "▁Kent",
+ -12.113607406616211
+ ],
+ [
+ "▁Deci",
+ -12.113761901855469
+ ],
+ [
+ "ли",
+ -12.113855361938477
+ ],
+ [
+ "▁crossed",
+ -12.113861083984375
+ ],
+ [
+ "▁delightful",
+ -12.113869667053223
+ ],
+ [
+ "▁metres",
+ -12.113872528076172
+ ],
+ [
+ "▁scandal",
+ -12.113906860351562
+ ],
+ [
+ "▁activitate",
+ -12.113986015319824
+ ],
+ [
+ "▁nimeni",
+ -12.114009857177734
+ ],
+ [
+ "ease",
+ -12.11402416229248
+ ],
+ [
+ "▁revenues",
+ -12.1140775680542
+ ],
+ [
+ "▁partially",
+ -12.114187240600586
+ ],
+ [
+ "AE",
+ -12.114263534545898
+ ],
+ [
+ "nique",
+ -12.114410400390625
+ ],
+ [
+ "▁fixtures",
+ -12.114426612854004
+ ],
+ [
+ "▁pupils",
+ -12.114694595336914
+ ],
+ [
+ "Lib",
+ -12.11471176147461
+ ],
+ [
+ "analyse",
+ -12.114739418029785
+ ],
+ [
+ "▁Oracle",
+ -12.114767074584961
+ ],
+ [
+ "troph",
+ -12.114859580993652
+ ],
+ [
+ "▁detected",
+ -12.114879608154297
+ ],
+ [
+ "▁servant",
+ -12.11507797241211
+ ],
+ [
+ "▁badly",
+ -12.115121841430664
+ ],
+ [
+ "comparing",
+ -12.115150451660156
+ ],
+ [
+ "abs",
+ -12.115238189697266
+ ],
+ [
+ "▁fotografi",
+ -12.115443229675293
+ ],
+ [
+ "▁Million",
+ -12.115541458129883
+ ],
+ [
+ "▁Gordon",
+ -12.11557388305664
+ ],
+ [
+ "▁Smok",
+ -12.115592002868652
+ ],
+ [
+ "▁Essay",
+ -12.11565113067627
+ ],
+ [
+ "eptic",
+ -12.115665435791016
+ ],
+ [
+ "▁Transportation",
+ -12.115728378295898
+ ],
+ [
+ "/2019",
+ -12.115767478942871
+ ],
+ [
+ "▁alignment",
+ -12.115778923034668
+ ],
+ [
+ "▁laut",
+ -12.11578369140625
+ ],
+ [
+ "stände",
+ -12.115791320800781
+ ],
+ [
+ "▁concerts",
+ -12.115811347961426
+ ],
+ [
+ "▁weekends",
+ -12.11589241027832
+ ],
+ [
+ "▁obstacles",
+ -12.115941047668457
+ ],
+ [
+ "wür",
+ -12.115964889526367
+ ],
+ [
+ "▁Fisher",
+ -12.116219520568848
+ ],
+ [
+ "▁supervisor",
+ -12.116242408752441
+ ],
+ [
+ "▁traders",
+ -12.116262435913086
+ ],
+ [
+ "▁scary",
+ -12.116484642028809
+ ],
+ [
+ "▁Grove",
+ -12.116538047790527
+ ],
+ [
+ "▁expose",
+ -12.116583824157715
+ ],
+ [
+ "▁enemies",
+ -12.116630554199219
+ ],
+ [
+ "▁Lux",
+ -12.11667537689209
+ ],
+ [
+ "▁Berufs",
+ -12.11672306060791
+ ],
+ [
+ "▁Sheet",
+ -12.116780281066895
+ ],
+ [
+ "▁Natürlich",
+ -12.116819381713867
+ ],
+ [
+ "▁examined",
+ -12.116886138916016
+ ],
+ [
+ "pursuing",
+ -12.116920471191406
+ ],
+ [
+ "▁pools",
+ -12.116923332214355
+ ],
+ [
+ "▁Thompson",
+ -12.117005348205566
+ ],
+ [
+ "▁SAP",
+ -12.117010116577148
+ ],
+ [
+ "claiming",
+ -12.117053985595703
+ ],
+ [
+ "buried",
+ -12.117055892944336
+ ],
+ [
+ "assurance",
+ -12.117138862609863
+ ],
+ [
+ "▁sandwich",
+ -12.117195129394531
+ ],
+ [
+ "uber",
+ -12.117310523986816
+ ],
+ [
+ "▁laisse",
+ -12.117321968078613
+ ],
+ [
+ "peak",
+ -12.117348670959473
+ ],
+ [
+ "spring",
+ -12.1173677444458
+ ],
+ [
+ "▁august",
+ -12.117369651794434
+ ],
+ [
+ "▁benötigt",
+ -12.11738109588623
+ ],
+ [
+ "▁achievements",
+ -12.117470741271973
+ ],
+ [
+ "coala",
+ -12.117478370666504
+ ],
+ [
+ "▁scr",
+ -12.117842674255371
+ ],
+ [
+ "gesagt",
+ -12.118122100830078
+ ],
+ [
+ "▁envelope",
+ -12.118141174316406
+ ],
+ [
+ "▁mapping",
+ -12.118169784545898
+ ],
+ [
+ "▁Suche",
+ -12.118298530578613
+ ],
+ [
+ "first",
+ -12.118329048156738
+ ],
+ [
+ "▁Quin",
+ -12.118447303771973
+ ],
+ [
+ "räu",
+ -12.118561744689941
+ ],
+ [
+ "▁răs",
+ -12.118583679199219
+ ],
+ [
+ "chemical",
+ -12.118597984313965
+ ],
+ [
+ "dad",
+ -12.118927955627441
+ ],
+ [
+ "formation",
+ -12.118983268737793
+ ],
+ [
+ "▁cushion",
+ -12.119026184082031
+ ],
+ [
+ "▁Maß",
+ -12.119046211242676
+ ],
+ [
+ "07.",
+ -12.119184494018555
+ ],
+ [
+ "▁perioadă",
+ -12.119257926940918
+ ],
+ [
+ "▁Wunsch",
+ -12.11925983428955
+ ],
+ [
+ "▁joi",
+ -12.119423866271973
+ ],
+ [
+ "▁$25",
+ -12.119482040405273
+ ],
+ [
+ "▁uploaded",
+ -12.11952018737793
+ ],
+ [
+ "▁hobby",
+ -12.119633674621582
+ ],
+ [
+ "▁septembrie",
+ -12.119633674621582
+ ],
+ [
+ "▁Dimension",
+ -12.119634628295898
+ ],
+ [
+ "▁domeniu",
+ -12.119661331176758
+ ],
+ [
+ "▁Tourism",
+ -12.119747161865234
+ ],
+ [
+ "▁fais",
+ -12.119800567626953
+ ],
+ [
+ "aches",
+ -12.119919776916504
+ ],
+ [
+ "neck",
+ -12.119969367980957
+ ],
+ [
+ "▁Chip",
+ -12.119982719421387
+ ],
+ [
+ "▁Tisch",
+ -12.1199951171875
+ ],
+ [
+ "▁Pai",
+ -12.120006561279297
+ ],
+ [
+ "▁Butter",
+ -12.120083808898926
+ ],
+ [
+ "▁altor",
+ -12.120133399963379
+ ],
+ [
+ "cultural",
+ -12.120182991027832
+ ],
+ [
+ "▁bases",
+ -12.12028980255127
+ ],
+ [
+ "▁Christopher",
+ -12.120396614074707
+ ],
+ [
+ "Kindle",
+ -12.120401382446289
+ ],
+ [
+ "▁bathrooms",
+ -12.12049388885498
+ ],
+ [
+ "▁civilian",
+ -12.12052059173584
+ ],
+ [
+ "▁Architecture",
+ -12.12058162689209
+ ],
+ [
+ "heiten",
+ -12.120641708374023
+ ],
+ [
+ "otte",
+ -12.120763778686523
+ ],
+ [
+ "ри",
+ -12.120784759521484
+ ],
+ [
+ "wash",
+ -12.120792388916016
+ ],
+ [
+ "▁evenimente",
+ -12.12086296081543
+ ],
+ [
+ "lade",
+ -12.121132850646973
+ ],
+ [
+ "▁ermöglicht",
+ -12.121140480041504
+ ],
+ [
+ "Port",
+ -12.121149063110352
+ ],
+ [
+ "▁Horn",
+ -12.12119197845459
+ ],
+ [
+ "▁Housing",
+ -12.121232032775879
+ ],
+ [
+ "▁Profit",
+ -12.121304512023926
+ ],
+ [
+ "▁stressed",
+ -12.12136459350586
+ ],
+ [
+ "▁70%",
+ -12.121431350708008
+ ],
+ [
+ "laying",
+ -12.121458053588867
+ ],
+ [
+ "▁specialize",
+ -12.121490478515625
+ ],
+ [
+ "▁Published",
+ -12.121519088745117
+ ],
+ [
+ "corp",
+ -12.121554374694824
+ ],
+ [
+ "▁revision",
+ -12.121611595153809
+ ],
+ [
+ "▁sail",
+ -12.121804237365723
+ ],
+ [
+ "courtesy",
+ -12.121909141540527
+ ],
+ [
+ "tax",
+ -12.1219482421875
+ ],
+ [
+ "▁perfekt",
+ -12.122018814086914
+ ],
+ [
+ "▁Risk",
+ -12.122088432312012
+ ],
+ [
+ "▁chaleur",
+ -12.122129440307617
+ ],
+ [
+ "ych",
+ -12.122132301330566
+ ],
+ [
+ "▁spine",
+ -12.12218189239502
+ ],
+ [
+ "▁holders",
+ -12.122264862060547
+ ],
+ [
+ "▁Speaking",
+ -12.122271537780762
+ ],
+ [
+ "▁Bernard",
+ -12.122400283813477
+ ],
+ [
+ "incarc",
+ -12.122532844543457
+ ],
+ [
+ "shalb",
+ -12.122639656066895
+ ],
+ [
+ "Potrivit",
+ -12.12264633178711
+ ],
+ [
+ "arising",
+ -12.122654914855957
+ ],
+ [
+ "▁kingdom",
+ -12.122665405273438
+ ],
+ [
+ "▁potato",
+ -12.122766494750977
+ ],
+ [
+ "▁promoted",
+ -12.122814178466797
+ ],
+ [
+ "▁judges",
+ -12.1228609085083
+ ],
+ [
+ "▁naturelle",
+ -12.122992515563965
+ ],
+ [
+ "▁Kindern",
+ -12.123022079467773
+ ],
+ [
+ "schicht",
+ -12.123047828674316
+ ],
+ [
+ "▁Drag",
+ -12.123066902160645
+ ],
+ [
+ "atta",
+ -12.123132705688477
+ ],
+ [
+ "soient",
+ -12.123249053955078
+ ],
+ [
+ "INS",
+ -12.12336540222168
+ ],
+ [
+ "▁legislative",
+ -12.123642921447754
+ ],
+ [
+ "▁teens",
+ -12.123785018920898
+ ],
+ [
+ "▁Fotos",
+ -12.123842239379883
+ ],
+ [
+ "▁illustrations",
+ -12.12392520904541
+ ],
+ [
+ "möglichkeiten",
+ -12.12415599822998
+ ],
+ [
+ "Votre",
+ -12.124194145202637
+ ],
+ [
+ "▁tarif",
+ -12.124195098876953
+ ],
+ [
+ "cli",
+ -12.124488830566406
+ ],
+ [
+ "▁landlord",
+ -12.12473201751709
+ ],
+ [
+ "cine",
+ -12.124743461608887
+ ],
+ [
+ "▁bot",
+ -12.124798774719238
+ ],
+ [
+ "enhancing",
+ -12.12491226196289
+ ],
+ [
+ "▁März",
+ -12.12491226196289
+ ],
+ [
+ "▁succès",
+ -12.125106811523438
+ ],
+ [
+ "▁disclose",
+ -12.125120162963867
+ ],
+ [
+ "▁Geräte",
+ -12.125321388244629
+ ],
+ [
+ "▁Magn",
+ -12.125422477722168
+ ],
+ [
+ "dessous",
+ -12.12580680847168
+ ],
+ [
+ "▁miracle",
+ -12.125862121582031
+ ],
+ [
+ "▁travailler",
+ -12.125933647155762
+ ],
+ [
+ "▁herb",
+ -12.125945091247559
+ ],
+ [
+ "-01",
+ -12.126049041748047
+ ],
+ [
+ "litre",
+ -12.126104354858398
+ ],
+ [
+ "▁tău",
+ -12.126120567321777
+ ],
+ [
+ "ACC",
+ -12.126190185546875
+ ],
+ [
+ "▁diminu",
+ -12.126275062561035
+ ],
+ [
+ "itzer",
+ -12.126317024230957
+ ],
+ [
+ "▁personenbezogen",
+ -12.126395225524902
+ ],
+ [
+ "▁Pure",
+ -12.126436233520508
+ ],
+ [
+ "▁influences",
+ -12.12668228149414
+ ],
+ [
+ "ană",
+ -12.126765251159668
+ ],
+ [
+ "▁proposer",
+ -12.126856803894043
+ ],
+ [
+ "▁longest",
+ -12.12692642211914
+ ],
+ [
+ "euses",
+ -12.127080917358398
+ ],
+ [
+ "/1",
+ -12.127487182617188
+ ],
+ [
+ "hafte",
+ -12.127716064453125
+ ],
+ [
+ "▁Dich",
+ -12.127761840820312
+ ],
+ [
+ "▁candle",
+ -12.128026962280273
+ ],
+ [
+ "ouche",
+ -12.128191947937012
+ ],
+ [
+ "installation",
+ -12.128241539001465
+ ],
+ [
+ "▁Includes",
+ -12.128280639648438
+ ],
+ [
+ "▁entfernt",
+ -12.12831974029541
+ ],
+ [
+ "traf",
+ -12.128499031066895
+ ],
+ [
+ "▁None",
+ -12.128508567810059
+ ],
+ [
+ "▁produc",
+ -12.128510475158691
+ ],
+ [
+ "held",
+ -12.128519058227539
+ ],
+ [
+ "graphic",
+ -12.128531455993652
+ ],
+ [
+ "▁demographic",
+ -12.128584861755371
+ ],
+ [
+ "ingham",
+ -12.1287841796875
+ ],
+ [
+ "schul",
+ -12.128812789916992
+ ],
+ [
+ "▁sneak",
+ -12.128843307495117
+ ],
+ [
+ "laub",
+ -12.128889083862305
+ ],
+ [
+ "▁thickness",
+ -12.12911605834961
+ ],
+ [
+ "▁killer",
+ -12.129297256469727
+ ],
+ [
+ "▁entsprechende",
+ -12.129344940185547
+ ],
+ [
+ "▁theft",
+ -12.129396438598633
+ ],
+ [
+ "▁Jerusalem",
+ -12.129457473754883
+ ],
+ [
+ "Adapt",
+ -12.129495620727539
+ ],
+ [
+ "▁updating",
+ -12.129497528076172
+ ],
+ [
+ "tete",
+ -12.12954330444336
+ ],
+ [
+ "▁warming",
+ -12.129701614379883
+ ],
+ [
+ "anlage",
+ -12.129739761352539
+ ],
+ [
+ "▁lenders",
+ -12.129814147949219
+ ],
+ [
+ "mobile",
+ -12.130008697509766
+ ],
+ [
+ "▁Package",
+ -12.130080223083496
+ ],
+ [
+ "▁Volume",
+ -12.130152702331543
+ ],
+ [
+ "---",
+ -12.130167007446289
+ ],
+ [
+ "▁Others",
+ -12.130173683166504
+ ],
+ [
+ "content",
+ -12.130188941955566
+ ],
+ [
+ "tement",
+ -12.130253791809082
+ ],
+ [
+ "bildet",
+ -12.13027572631836
+ ],
+ [
+ "▁washer",
+ -12.13053035736084
+ ],
+ [
+ "▁freelance",
+ -12.130623817443848
+ ],
+ [
+ "▁fein",
+ -12.130753517150879
+ ],
+ [
+ "▁catering",
+ -12.130851745605469
+ ],
+ [
+ "▁warmth",
+ -12.130911827087402
+ ],
+ [
+ "▁Month",
+ -12.131103515625
+ ],
+ [
+ "▁Federation",
+ -12.131134033203125
+ ],
+ [
+ "▁editorial",
+ -12.13121223449707
+ ],
+ [
+ "▁Shopping",
+ -12.131241798400879
+ ],
+ [
+ "▁efort",
+ -12.131296157836914
+ ],
+ [
+ "▁damp",
+ -12.131314277648926
+ ],
+ [
+ "▁declined",
+ -12.131332397460938
+ ],
+ [
+ "▁1978",
+ -12.13135051727295
+ ],
+ [
+ "6,000",
+ -12.131355285644531
+ ],
+ [
+ "location",
+ -12.131551742553711
+ ],
+ [
+ "▁blogger",
+ -12.131572723388672
+ ],
+ [
+ "▁goodness",
+ -12.131826400756836
+ ],
+ [
+ "▁Purchase",
+ -12.132119178771973
+ ],
+ [
+ "▁suspended",
+ -12.132159233093262
+ ],
+ [
+ "▁assessed",
+ -12.132201194763184
+ ],
+ [
+ "rada",
+ -12.132286071777344
+ ],
+ [
+ "▁Lac",
+ -12.132291793823242
+ ],
+ [
+ "▁angeboten",
+ -12.13235092163086
+ ],
+ [
+ "▁Wetter",
+ -12.132370948791504
+ ],
+ [
+ "ores",
+ -12.13243579864502
+ ],
+ [
+ "▁fourni",
+ -12.132476806640625
+ ],
+ [
+ "▁retire",
+ -12.13269329071045
+ ],
+ [
+ "▁Baptist",
+ -12.132741928100586
+ ],
+ [
+ "▁Saison",
+ -12.13277530670166
+ ],
+ [
+ "Bar",
+ -12.132794380187988
+ ],
+ [
+ "▁dossier",
+ -12.132979393005371
+ ],
+ [
+ "brow",
+ -12.133044242858887
+ ],
+ [
+ "▁Kaffee",
+ -12.133071899414062
+ ],
+ [
+ "-25",
+ -12.133463859558105
+ ],
+ [
+ "▁festivals",
+ -12.133599281311035
+ ],
+ [
+ "▁sellers",
+ -12.133716583251953
+ ],
+ [
+ "Ü",
+ -12.13393783569336
+ ],
+ [
+ "▁publisher",
+ -12.133960723876953
+ ],
+ [
+ "▁Designs",
+ -12.133970260620117
+ ],
+ [
+ "▁putut",
+ -12.13400936126709
+ ],
+ [
+ "▁Built",
+ -12.134417533874512
+ ],
+ [
+ "▁recreational",
+ -12.134476661682129
+ ],
+ [
+ "▁european",
+ -12.134514808654785
+ ],
+ [
+ "▁binary",
+ -12.134631156921387
+ ],
+ [
+ "▁Nieder",
+ -12.134764671325684
+ ],
+ [
+ "taking",
+ -12.1348237991333
+ ],
+ [
+ "▁Lots",
+ -12.13494873046875
+ ],
+ [
+ "▁recognised",
+ -12.135031700134277
+ ],
+ [
+ "ssant",
+ -12.135063171386719
+ ],
+ [
+ "ITE",
+ -12.135271072387695
+ ],
+ [
+ "oom",
+ -12.135298728942871
+ ],
+ [
+ "▁Kre",
+ -12.135310173034668
+ ],
+ [
+ "▁pipes",
+ -12.135631561279297
+ ],
+ [
+ "▁hinge",
+ -12.135653495788574
+ ],
+ [
+ "▁enterprises",
+ -12.135664939880371
+ ],
+ [
+ "▁texts",
+ -12.13583755493164
+ ],
+ [
+ "Organiz",
+ -12.136080741882324
+ ],
+ [
+ "▁suivre",
+ -12.136124610900879
+ ],
+ [
+ "noc",
+ -12.136157989501953
+ ],
+ [
+ "fair",
+ -12.136194229125977
+ ],
+ [
+ "▁darkness",
+ -12.136305809020996
+ ],
+ [
+ "▁Whi",
+ -12.13631534576416
+ ],
+ [
+ "natural",
+ -12.136321067810059
+ ],
+ [
+ "Bas",
+ -12.136422157287598
+ ],
+ [
+ "▁tribute",
+ -12.136443138122559
+ ],
+ [
+ "▁Naţional",
+ -12.136573791503906
+ ],
+ [
+ "hara",
+ -12.136622428894043
+ ],
+ [
+ "▁catégorie",
+ -12.136697769165039
+ ],
+ [
+ "▁Schedule",
+ -12.136698722839355
+ ],
+ [
+ "▁lernen",
+ -12.13671875
+ ],
+ [
+ "▁Plastic",
+ -12.136725425720215
+ ],
+ [
+ "▁giveaway",
+ -12.13675594329834
+ ],
+ [
+ "▁Ideen",
+ -12.136906623840332
+ ],
+ [
+ "▁circa",
+ -12.13718032836914
+ ],
+ [
+ "▁lice",
+ -12.137242317199707
+ ],
+ [
+ "▁Meinung",
+ -12.137264251708984
+ ],
+ [
+ "▁beside",
+ -12.137566566467285
+ ],
+ [
+ "▁vazut",
+ -12.137673377990723
+ ],
+ [
+ "strom",
+ -12.137749671936035
+ ],
+ [
+ "boro",
+ -12.137775421142578
+ ],
+ [
+ "▁Soon",
+ -12.137796401977539
+ ],
+ [
+ "dozens",
+ -12.137896537780762
+ ],
+ [
+ "▁Arena",
+ -12.137943267822266
+ ],
+ [
+ "▁viața",
+ -12.137989044189453
+ ],
+ [
+ "▁Impact",
+ -12.138082504272461
+ ],
+ [
+ "current",
+ -12.138106346130371
+ ],
+ [
+ "FM",
+ -12.138117790222168
+ ],
+ [
+ "▁coil",
+ -12.138657569885254
+ ],
+ [
+ "gold",
+ -12.138679504394531
+ ],
+ [
+ "▁spate",
+ -12.138679504394531
+ ],
+ [
+ "1.4",
+ -12.13875675201416
+ ],
+ [
+ "solution",
+ -12.138769149780273
+ ],
+ [
+ "▁Wayne",
+ -12.138835906982422
+ ],
+ [
+ "▁queen",
+ -12.138898849487305
+ ],
+ [
+ "illion",
+ -12.139022827148438
+ ],
+ [
+ "greifen",
+ -12.139127731323242
+ ],
+ [
+ "▁Bil",
+ -12.139174461364746
+ ],
+ [
+ "rote",
+ -12.139185905456543
+ ],
+ [
+ "END",
+ -12.13918685913086
+ ],
+ [
+ "äl",
+ -12.139206886291504
+ ],
+ [
+ "▁reçu",
+ -12.139378547668457
+ ],
+ [
+ "flower",
+ -12.139495849609375
+ ],
+ [
+ "▁draws",
+ -12.139519691467285
+ ],
+ [
+ "plant",
+ -12.139605522155762
+ ],
+ [
+ "2010",
+ -12.139702796936035
+ ],
+ [
+ "▁oper",
+ -12.139762878417969
+ ],
+ [
+ "▁conserve",
+ -12.139777183532715
+ ],
+ [
+ "▁sprinkle",
+ -12.13984203338623
+ ],
+ [
+ "mode",
+ -12.139924049377441
+ ],
+ [
+ "▁lifting",
+ -12.139941215515137
+ ],
+ [
+ "▁Institution",
+ -12.139951705932617
+ ],
+ [
+ "Când",
+ -12.14001750946045
+ ],
+ [
+ "Aus",
+ -12.140048027038574
+ ],
+ [
+ "▁fears",
+ -12.140054702758789
+ ],
+ [
+ "▁appointments",
+ -12.140079498291016
+ ],
+ [
+ "oarele",
+ -12.140162467956543
+ ],
+ [
+ "▁duck",
+ -12.140193939208984
+ ],
+ [
+ "▁stadium",
+ -12.140213012695312
+ ],
+ [
+ "▁vezi",
+ -12.140227317810059
+ ],
+ [
+ "▁lap",
+ -12.140315055847168
+ ],
+ [
+ "▁proceeds",
+ -12.140382766723633
+ ],
+ [
+ "geschlossen",
+ -12.140412330627441
+ ],
+ [
+ "▁tren",
+ -12.140478134155273
+ ],
+ [
+ "VS",
+ -12.140536308288574
+ ],
+ [
+ "▁vais",
+ -12.140800476074219
+ ],
+ [
+ "ținut",
+ -12.140859603881836
+ ],
+ [
+ "▁Concert",
+ -12.140928268432617
+ ],
+ [
+ "▁planting",
+ -12.141008377075195
+ ],
+ [
+ "▁honour",
+ -12.141069412231445
+ ],
+ [
+ "▁gras",
+ -12.141071319580078
+ ],
+ [
+ "woo",
+ -12.141092300415039
+ ],
+ [
+ "▁Hero",
+ -12.141282081604004
+ ],
+ [
+ "▁stimulate",
+ -12.14134407043457
+ ],
+ [
+ "▁überhaupt",
+ -12.141426086425781
+ ],
+ [
+ "▁bounce",
+ -12.14148235321045
+ ],
+ [
+ "oodle",
+ -12.14151382446289
+ ],
+ [
+ "▁packs",
+ -12.141576766967773
+ ],
+ [
+ "▁Poker",
+ -12.14158821105957
+ ],
+ [
+ "▁acea",
+ -12.141684532165527
+ ],
+ [
+ "▁parish",
+ -12.141754150390625
+ ],
+ [
+ "-24",
+ -12.141766548156738
+ ],
+ [
+ "▁iTunes",
+ -12.141874313354492
+ ],
+ [
+ "▁lumière",
+ -12.141948699951172
+ ],
+ [
+ "third",
+ -12.142024993896484
+ ],
+ [
+ "▁dynamics",
+ -12.142038345336914
+ ],
+ [
+ "Unless",
+ -12.142162322998047
+ ],
+ [
+ "▁immense",
+ -12.142416000366211
+ ],
+ [
+ "▁Sec",
+ -12.142781257629395
+ ],
+ [
+ "lois",
+ -12.143009185791016
+ ],
+ [
+ "époque",
+ -12.14302921295166
+ ],
+ [
+ "NB",
+ -12.143139839172363
+ ],
+ [
+ "written",
+ -12.143210411071777
+ ],
+ [
+ "▁logement",
+ -12.143226623535156
+ ],
+ [
+ "submitting",
+ -12.143295288085938
+ ],
+ [
+ "▁Quand",
+ -12.14331340789795
+ ],
+ [
+ "▁foi",
+ -12.143322944641113
+ ],
+ [
+ "▁catalogue",
+ -12.143351554870605
+ ],
+ [
+ "nova",
+ -12.14343547821045
+ ],
+ [
+ "▁prezentat",
+ -12.143527030944824
+ ],
+ [
+ "▁tart",
+ -12.143877983093262
+ ],
+ [
+ "те",
+ -12.143912315368652
+ ],
+ [
+ "hack",
+ -12.143916130065918
+ ],
+ [
+ "▁Politic",
+ -12.144003868103027
+ ],
+ [
+ "▁18,",
+ -12.144048690795898
+ ],
+ [
+ "▁ignored",
+ -12.144145965576172
+ ],
+ [
+ "▁spoon",
+ -12.144245147705078
+ ],
+ [
+ "▁Joy",
+ -12.144280433654785
+ ],
+ [
+ "▁reside",
+ -12.144482612609863
+ ],
+ [
+ ".99",
+ -12.144488334655762
+ ],
+ [
+ "lytic",
+ -12.144625663757324
+ ],
+ [
+ "▁bogat",
+ -12.144643783569336
+ ],
+ [
+ "▁nurses",
+ -12.144845008850098
+ ],
+ [
+ "▁funcţi",
+ -12.145029067993164
+ ],
+ [
+ "▁produselor",
+ -12.145038604736328
+ ],
+ [
+ "▁Associates",
+ -12.145069122314453
+ ],
+ [
+ "Est",
+ -12.14511489868164
+ ],
+ [
+ "▁peanut",
+ -12.145187377929688
+ ],
+ [
+ "▁résultat",
+ -12.145257949829102
+ ],
+ [
+ "08.",
+ -12.145424842834473
+ ],
+ [
+ "▁Astro",
+ -12.145439147949219
+ ],
+ [
+ "▁personnelle",
+ -12.145527839660645
+ ],
+ [
+ "320",
+ -12.145668983459473
+ ],
+ [
+ "▁Grab",
+ -12.145748138427734
+ ],
+ [
+ "éco",
+ -12.145801544189453
+ ],
+ [
+ "▁clasic",
+ -12.145857810974121
+ ],
+ [
+ "offre",
+ -12.14588451385498
+ ],
+ [
+ "▁idee",
+ -12.14589786529541
+ ],
+ [
+ "▁cheat",
+ -12.146259307861328
+ ],
+ [
+ "▁Flug",
+ -12.146286964416504
+ ],
+ [
+ "▁1500",
+ -12.146413803100586
+ ],
+ [
+ "▁kurze",
+ -12.14643383026123
+ ],
+ [
+ "With",
+ -12.146512985229492
+ ],
+ [
+ "▁Half",
+ -12.146575927734375
+ ],
+ [
+ "▁disciplines",
+ -12.146642684936523
+ ],
+ [
+ "sorption",
+ -12.14669132232666
+ ],
+ [
+ "▁greutate",
+ -12.146927833557129
+ ],
+ [
+ "mä",
+ -12.146940231323242
+ ],
+ [
+ "▁Literatur",
+ -12.146956443786621
+ ],
+ [
+ "3/",
+ -12.147016525268555
+ ],
+ [
+ "4.0",
+ -12.147095680236816
+ ],
+ [
+ "▁déco",
+ -12.147119522094727
+ ],
+ [
+ "▁Fuß",
+ -12.147233963012695
+ ],
+ [
+ "▁Deutsche",
+ -12.147289276123047
+ ],
+ [
+ "▁abundance",
+ -12.14746379852295
+ ],
+ [
+ "▁Luther",
+ -12.14750862121582
+ ],
+ [
+ "▁nutritional",
+ -12.147562980651855
+ ],
+ [
+ "▁Jude",
+ -12.147687911987305
+ ],
+ [
+ "AY",
+ -12.14786148071289
+ ],
+ [
+ "▁chore",
+ -12.147916793823242
+ ],
+ [
+ "▁Kro",
+ -12.148006439208984
+ ],
+ [
+ "▁alin",
+ -12.14801025390625
+ ],
+ [
+ "lösung",
+ -12.148030281066895
+ ],
+ [
+ "▁geworden",
+ -12.148238182067871
+ ],
+ [
+ "▁sociaux",
+ -12.148255348205566
+ ],
+ [
+ "▁Spark",
+ -12.1486177444458
+ ],
+ [
+ "▁phenomenon",
+ -12.148624420166016
+ ],
+ [
+ "ICA",
+ -12.148805618286133
+ ],
+ [
+ "▁Ran",
+ -12.148836135864258
+ ],
+ [
+ "▁Schwarz",
+ -12.148959159851074
+ ],
+ [
+ "▁1983",
+ -12.148985862731934
+ ],
+ [
+ "ет",
+ -12.148990631103516
+ ],
+ [
+ "möglich",
+ -12.149084091186523
+ ],
+ [
+ "vocation",
+ -12.149087905883789
+ ],
+ [
+ "▁Organic",
+ -12.14926815032959
+ ],
+ [
+ "Oh",
+ -12.149408340454102
+ ],
+ [
+ "▁blockchain",
+ -12.149422645568848
+ ],
+ [
+ "▁Bă",
+ -12.149515151977539
+ ],
+ [
+ "▁Bass",
+ -12.14953899383545
+ ],
+ [
+ "enie",
+ -12.149687767028809
+ ],
+ [
+ "▁rêve",
+ -12.149807929992676
+ ],
+ [
+ "▁Rap",
+ -12.149986267089844
+ ],
+ [
+ "▁democratic",
+ -12.150044441223145
+ ],
+ [
+ "▁Chart",
+ -12.150167465209961
+ ],
+ [
+ "▁Voi",
+ -12.150189399719238
+ ],
+ [
+ "process",
+ -12.150263786315918
+ ],
+ [
+ "▁preach",
+ -12.150389671325684
+ ],
+ [
+ "tient",
+ -12.150456428527832
+ ],
+ [
+ "▁Train",
+ -12.150468826293945
+ ],
+ [
+ "▁Reihe",
+ -12.150472640991211
+ ],
+ [
+ "help",
+ -12.150514602661133
+ ],
+ [
+ "1.6",
+ -12.150547981262207
+ ],
+ [
+ "▁cazuri",
+ -12.150547981262207
+ ],
+ [
+ "▁chap",
+ -12.150559425354004
+ ],
+ [
+ "aktiv",
+ -12.150632858276367
+ ],
+ [
+ "▁2006.",
+ -12.15079116821289
+ ],
+ [
+ "iene",
+ -12.150849342346191
+ ],
+ [
+ "▁BBQ",
+ -12.150969505310059
+ ],
+ [
+ "dauer",
+ -12.151028633117676
+ ],
+ [
+ "2).",
+ -12.151226997375488
+ ],
+ [
+ "▁Monat",
+ -12.151277542114258
+ ],
+ [
+ "Generally",
+ -12.151285171508789
+ ],
+ [
+ "▁bracelet",
+ -12.151336669921875
+ ],
+ [
+ "▁cartoon",
+ -12.151349067687988
+ ],
+ [
+ "▁pui",
+ -12.151488304138184
+ ],
+ [
+ "temp",
+ -12.151506423950195
+ ],
+ [
+ "▁Particip",
+ -12.151555061340332
+ ],
+ [
+ "▁dumneavoastră",
+ -12.151725769042969
+ ],
+ [
+ "▁Gin",
+ -12.151824951171875
+ ],
+ [
+ "iunile",
+ -12.151829719543457
+ ],
+ [
+ "reise",
+ -12.151849746704102
+ ],
+ [
+ "▁einzige",
+ -12.15189266204834
+ ],
+ [
+ "ANCE",
+ -12.15192985534668
+ ],
+ [
+ "▁humble",
+ -12.151951789855957
+ ],
+ [
+ "claim",
+ -12.152093887329102
+ ],
+ [
+ "LV",
+ -12.152143478393555
+ ],
+ [
+ "▁confiance",
+ -12.152270317077637
+ ],
+ [
+ "▁Trading",
+ -12.152535438537598
+ ],
+ [
+ "▁Fabric",
+ -12.152770042419434
+ ],
+ [
+ "▁Duke",
+ -12.152851104736328
+ ],
+ [
+ "spieler",
+ -12.152937889099121
+ ],
+ [
+ "▁reject",
+ -12.152987480163574
+ ],
+ [
+ "▁crise",
+ -12.153170585632324
+ ],
+ [
+ "▁borders",
+ -12.153196334838867
+ ],
+ [
+ "▁Vehicle",
+ -12.153279304504395
+ ],
+ [
+ "zeiten",
+ -12.153481483459473
+ ],
+ [
+ "enrolled",
+ -12.153514862060547
+ ],
+ [
+ "venue",
+ -12.153555870056152
+ ],
+ [
+ "▁forests",
+ -12.153564453125
+ ],
+ [
+ "vascular",
+ -12.15358829498291
+ ],
+ [
+ "▁phrases",
+ -12.153661727905273
+ ],
+ [
+ "▁receptor",
+ -12.15368366241455
+ ],
+ [
+ "schied",
+ -12.153687477111816
+ ],
+ [
+ "▁soirée",
+ -12.153785705566406
+ ],
+ [
+ "▁partener",
+ -12.153987884521484
+ ],
+ [
+ "▁Jobs",
+ -12.15417194366455
+ ],
+ [
+ "▁segments",
+ -12.154216766357422
+ ],
+ [
+ "▁violate",
+ -12.154438972473145
+ ],
+ [
+ "▁viable",
+ -12.154500007629395
+ ],
+ [
+ "▁encountered",
+ -12.154533386230469
+ ],
+ [
+ "▁travelers",
+ -12.154552459716797
+ ],
+ [
+ "▁împ",
+ -12.154679298400879
+ ],
+ [
+ "▁convince",
+ -12.154693603515625
+ ],
+ [
+ "▁mailing",
+ -12.154693603515625
+ ],
+ [
+ "▁Zahn",
+ -12.154698371887207
+ ],
+ [
+ "attend",
+ -12.15477466583252
+ ],
+ [
+ "▁eBay",
+ -12.154836654663086
+ ],
+ [
+ "▁Emergency",
+ -12.154844284057617
+ ],
+ [
+ "wirtschaft",
+ -12.154882431030273
+ ],
+ [
+ "▁scholars",
+ -12.154947280883789
+ ],
+ [
+ "▁considerably",
+ -12.155118942260742
+ ],
+ [
+ "▁combo",
+ -12.1551513671875
+ ],
+ [
+ "hiver",
+ -12.155198097229004
+ ],
+ [
+ "▁mysterious",
+ -12.15522575378418
+ ],
+ [
+ "▁Degree",
+ -12.155234336853027
+ ],
+ [
+ "▁fate",
+ -12.155242919921875
+ ],
+ [
+ "▁transplant",
+ -12.155281066894531
+ ],
+ [
+ "▁samedi",
+ -12.155400276184082
+ ],
+ [
+ "unit",
+ -12.155519485473633
+ ],
+ [
+ "▁moyenne",
+ -12.155611991882324
+ ],
+ [
+ "▁Liverpool",
+ -12.155614852905273
+ ],
+ [
+ "▁Champions",
+ -12.155728340148926
+ ],
+ [
+ "zzle",
+ -12.155824661254883
+ ],
+ [
+ "▁arena",
+ -12.156228065490723
+ ],
+ [
+ "▁Pipe",
+ -12.15633487701416
+ ],
+ [
+ "▁waterproof",
+ -12.156356811523438
+ ],
+ [
+ "▁eternal",
+ -12.156463623046875
+ ],
+ [
+ "Whenever",
+ -12.156503677368164
+ ],
+ [
+ "▁Hop",
+ -12.156535148620605
+ ],
+ [
+ "▁Betrieb",
+ -12.156816482543945
+ ],
+ [
+ "gne",
+ -12.15692138671875
+ ],
+ [
+ "▁spe",
+ -12.156975746154785
+ ],
+ [
+ "▁Corner",
+ -12.157078742980957
+ ],
+ [
+ "▁devenir",
+ -12.157118797302246
+ ],
+ [
+ "ambiance",
+ -12.157144546508789
+ ],
+ [
+ "▁Graham",
+ -12.157200813293457
+ ],
+ [
+ "▁desires",
+ -12.157289505004883
+ ],
+ [
+ "▁Applications",
+ -12.157291412353516
+ ],
+ [
+ "▁genutzt",
+ -12.157477378845215
+ ],
+ [
+ "tek",
+ -12.157612800598145
+ ],
+ [
+ "▁Career",
+ -12.157641410827637
+ ],
+ [
+ "▁staple",
+ -12.157695770263672
+ ],
+ [
+ "▁Dodge",
+ -12.157817840576172
+ ],
+ [
+ "▁strictly",
+ -12.157889366149902
+ ],
+ [
+ "▁Gruppen",
+ -12.157952308654785
+ ],
+ [
+ "▁Finanz",
+ -12.157981872558594
+ ],
+ [
+ "▁sporting",
+ -12.15809440612793
+ ],
+ [
+ "▁Wieder",
+ -12.158127784729004
+ ],
+ [
+ "anny",
+ -12.158208847045898
+ ],
+ [
+ "▁bucura",
+ -12.158233642578125
+ ],
+ [
+ "▁Pest",
+ -12.15824031829834
+ ],
+ [
+ "▁circles",
+ -12.158246994018555
+ ],
+ [
+ "▁richtige",
+ -12.158309936523438
+ ],
+ [
+ "▁cycles",
+ -12.158379554748535
+ ],
+ [
+ "static",
+ -12.15845012664795
+ ],
+ [
+ "lasting",
+ -12.15847396850586
+ ],
+ [
+ "▁calcium",
+ -12.158549308776855
+ ],
+ [
+ "▁digest",
+ -12.158697128295898
+ ],
+ [
+ "Enfin",
+ -12.158865928649902
+ ],
+ [
+ "▁stressful",
+ -12.158951759338379
+ ],
+ [
+ "▁schemes",
+ -12.158981323242188
+ ],
+ [
+ "▁décision",
+ -12.158987045288086
+ ],
+ [
+ "▁comercial",
+ -12.15907096862793
+ ],
+ [
+ "işti",
+ -12.159098625183105
+ ],
+ [
+ "▁Comic",
+ -12.15910816192627
+ ],
+ [
+ "▁extensions",
+ -12.159140586853027
+ ],
+ [
+ "▁Sieg",
+ -12.159168243408203
+ ],
+ [
+ "▁pine",
+ -12.15919017791748
+ ],
+ [
+ "ieß",
+ -12.159272193908691
+ ],
+ [
+ "▁Images",
+ -12.159427642822266
+ ],
+ [
+ "▁Mensch",
+ -12.159668922424316
+ ],
+ [
+ "Pap",
+ -12.159773826599121
+ ],
+ [
+ "▁crops",
+ -12.15994930267334
+ ],
+ [
+ "▁sheep",
+ -12.159996032714844
+ ],
+ [
+ "▁istoric",
+ -12.160001754760742
+ ],
+ [
+ "▁Assessment",
+ -12.160035133361816
+ ],
+ [
+ "▁mounting",
+ -12.16035270690918
+ ],
+ [
+ "wirken",
+ -12.160469055175781
+ ],
+ [
+ "▁augment",
+ -12.160469055175781
+ ],
+ [
+ "▁picioare",
+ -12.160542488098145
+ ],
+ [
+ "organisme",
+ -12.160590171813965
+ ],
+ [
+ "▁Monitor",
+ -12.16060733795166
+ ],
+ [
+ "▁celles",
+ -12.160642623901367
+ ],
+ [
+ "▁Maison",
+ -12.160709381103516
+ ],
+ [
+ "notified",
+ -12.160783767700195
+ ],
+ [
+ "▁chew",
+ -12.160831451416016
+ ],
+ [
+ "▁bleu",
+ -12.16083812713623
+ ],
+ [
+ "dow",
+ -12.160844802856445
+ ],
+ [
+ "▁Grav",
+ -12.16097354888916
+ ],
+ [
+ "▁curtains",
+ -12.160975456237793
+ ],
+ [
+ "▁Campus",
+ -12.161076545715332
+ ],
+ [
+ "▁controversial",
+ -12.161087036132812
+ ],
+ [
+ "▁soutien",
+ -12.161189079284668
+ ],
+ [
+ "▁Dell",
+ -12.1613187789917
+ ],
+ [
+ "▁instrumental",
+ -12.161431312561035
+ ],
+ [
+ "▁Nan",
+ -12.161514282226562
+ ],
+ [
+ "▁prom",
+ -12.161520957946777
+ ],
+ [
+ "▁spatial",
+ -12.161523818969727
+ ],
+ [
+ "Similarly",
+ -12.161558151245117
+ ],
+ [
+ "▁Gala",
+ -12.161601066589355
+ ],
+ [
+ "ultimul",
+ -12.16162109375
+ ],
+ [
+ "▁Vom",
+ -12.161761283874512
+ ],
+ [
+ "▁Foot",
+ -12.161784172058105
+ ],
+ [
+ "bike",
+ -12.1618013381958
+ ],
+ [
+ "▁acids",
+ -12.161979675292969
+ ],
+ [
+ "entend",
+ -12.162002563476562
+ ],
+ [
+ "ivă",
+ -12.162040710449219
+ ],
+ [
+ "▁Weitere",
+ -12.162124633789062
+ ],
+ [
+ "▁vitamins",
+ -12.162131309509277
+ ],
+ [
+ "▁enhancement",
+ -12.16234016418457
+ ],
+ [
+ "▁Cruise",
+ -12.162367820739746
+ ],
+ [
+ "assemble",
+ -12.162385940551758
+ ],
+ [
+ "▁spécifique",
+ -12.162459373474121
+ ],
+ [
+ "affaires",
+ -12.16261100769043
+ ],
+ [
+ "▁indispensable",
+ -12.1626558303833
+ ],
+ [
+ "▁logistics",
+ -12.16283130645752
+ ],
+ [
+ "▁manche",
+ -12.162919044494629
+ ],
+ [
+ "▁dealt",
+ -12.16297435760498
+ ],
+ [
+ "▁favorable",
+ -12.163036346435547
+ ],
+ [
+ "▁unwanted",
+ -12.163047790527344
+ ],
+ [
+ "▁handmade",
+ -12.163065910339355
+ ],
+ [
+ "▁Regi",
+ -12.163102149963379
+ ],
+ [
+ "safe",
+ -12.163134574890137
+ ],
+ [
+ "persoanele",
+ -12.163202285766602
+ ],
+ [
+ "▁destinat",
+ -12.163252830505371
+ ],
+ [
+ "▁Maxi",
+ -12.163299560546875
+ ],
+ [
+ "▁salmon",
+ -12.163454055786133
+ ],
+ [
+ "wag",
+ -12.163578033447266
+ ],
+ [
+ "210",
+ -12.163769721984863
+ ],
+ [
+ "▁warned",
+ -12.163865089416504
+ ],
+ [
+ "läuft",
+ -12.16386604309082
+ ],
+ [
+ "agging",
+ -12.163931846618652
+ ],
+ [
+ "▁responsabil",
+ -12.16398811340332
+ ],
+ [
+ "▁presse",
+ -12.164271354675293
+ ],
+ [
+ "▁amis",
+ -12.164305686950684
+ ],
+ [
+ "▁rolls",
+ -12.164377212524414
+ ],
+ [
+ "control",
+ -12.164405822753906
+ ],
+ [
+ "▁Manufacturer",
+ -12.164422988891602
+ ],
+ [
+ "hnen",
+ -12.164449691772461
+ ],
+ [
+ "▁buget",
+ -12.164546012878418
+ ],
+ [
+ "OW",
+ -12.16467571258545
+ ],
+ [
+ "etro",
+ -12.164745330810547
+ ],
+ [
+ "▁communauté",
+ -12.164837837219238
+ ],
+ [
+ "unci",
+ -12.164944648742676
+ ],
+ [
+ "▁Chine",
+ -12.164952278137207
+ ],
+ [
+ "combines",
+ -12.16501235961914
+ ],
+ [
+ "▁learners",
+ -12.165046691894531
+ ],
+ [
+ "STE",
+ -12.165055274963379
+ ],
+ [
+ "ckel",
+ -12.16511344909668
+ ],
+ [
+ "Service",
+ -12.165169715881348
+ ],
+ [
+ "▁veröffentlicht",
+ -12.165209770202637
+ ],
+ [
+ "besides",
+ -12.165266036987305
+ ],
+ [
+ "getragen",
+ -12.165349960327148
+ ],
+ [
+ "▁opponent",
+ -12.165521621704102
+ ],
+ [
+ "▁volum",
+ -12.165533065795898
+ ],
+ [
+ "▁confusing",
+ -12.165802001953125
+ ],
+ [
+ "invasive",
+ -12.165813446044922
+ ],
+ [
+ "▁conseils",
+ -12.165881156921387
+ ],
+ [
+ "▁vibe",
+ -12.165928840637207
+ ],
+ [
+ "View",
+ -12.166062355041504
+ ],
+ [
+ "oară",
+ -12.166086196899414
+ ],
+ [
+ "Link",
+ -12.166261672973633
+ ],
+ [
+ "▁holy",
+ -12.166261672973633
+ ],
+ [
+ "▁crema",
+ -12.16629409790039
+ ],
+ [
+ "▁Michelle",
+ -12.166303634643555
+ ],
+ [
+ "▁Wien",
+ -12.166383743286133
+ ],
+ [
+ "▁undertake",
+ -12.166404724121094
+ ],
+ [
+ "▁Photograph",
+ -12.166421890258789
+ ],
+ [
+ "humain",
+ -12.16645336151123
+ ],
+ [
+ "▁Hang",
+ -12.166545867919922
+ ],
+ [
+ "designed",
+ -12.16657829284668
+ ],
+ [
+ "▁analyses",
+ -12.166614532470703
+ ],
+ [
+ "▁compose",
+ -12.166653633117676
+ ],
+ [
+ "▁substantially",
+ -12.166765213012695
+ ],
+ [
+ "▁marking",
+ -12.166772842407227
+ ],
+ [
+ "▁campagne",
+ -12.166826248168945
+ ],
+ [
+ "▁$15",
+ -12.166828155517578
+ ],
+ [
+ "pharma",
+ -12.166972160339355
+ ],
+ [
+ "▁playoff",
+ -12.1669921875
+ ],
+ [
+ "▁momentum",
+ -12.167091369628906
+ ],
+ [
+ "Temp",
+ -12.16714096069336
+ ],
+ [
+ "▁vinegar",
+ -12.167143821716309
+ ],
+ [
+ "▁descriptions",
+ -12.167581558227539
+ ],
+ [
+ "christ",
+ -12.167656898498535
+ ],
+ [
+ "wore",
+ -12.16773509979248
+ ],
+ [
+ "ITY",
+ -12.167768478393555
+ ],
+ [
+ "stehen",
+ -12.167771339416504
+ ],
+ [
+ "▁insulation",
+ -12.1677827835083
+ ],
+ [
+ "grav",
+ -12.167842864990234
+ ],
+ [
+ "2.2",
+ -12.167887687683105
+ ],
+ [
+ "▁Explore",
+ -12.168028831481934
+ ],
+ [
+ "▁dye",
+ -12.168127059936523
+ ],
+ [
+ "stair",
+ -12.168155670166016
+ ],
+ [
+ "artisan",
+ -12.168207168579102
+ ],
+ [
+ "▁zoom",
+ -12.168285369873047
+ ],
+ [
+ "▁turkey",
+ -12.168573379516602
+ ],
+ [
+ "▁locksmith",
+ -12.168577194213867
+ ],
+ [
+ "▁sewing",
+ -12.168610572814941
+ ],
+ [
+ "▁modeling",
+ -12.168627738952637
+ ],
+ [
+ "lied",
+ -12.16870403289795
+ ],
+ [
+ "adel",
+ -12.168773651123047
+ ],
+ [
+ "▁Going",
+ -12.168785095214844
+ ],
+ [
+ "WH",
+ -12.168798446655273
+ ],
+ [
+ "▁deserves",
+ -12.168919563293457
+ ],
+ [
+ "▁arriving",
+ -12.168960571289062
+ ],
+ [
+ "OFF",
+ -12.169039726257324
+ ],
+ [
+ "torului",
+ -12.169109344482422
+ ],
+ [
+ "ucked",
+ -12.16921615600586
+ ],
+ [
+ "▁approached",
+ -12.169351577758789
+ ],
+ [
+ "▁élevé",
+ -12.169354438781738
+ ],
+ [
+ "▁quotidien",
+ -12.169416427612305
+ ],
+ [
+ "▁derzeit",
+ -12.16942024230957
+ ],
+ [
+ "nutzt",
+ -12.169656753540039
+ ],
+ [
+ "science",
+ -12.169729232788086
+ ],
+ [
+ "▁Emma",
+ -12.169841766357422
+ ],
+ [
+ "▁builds",
+ -12.169879913330078
+ ],
+ [
+ "▁Logo",
+ -12.169949531555176
+ ],
+ [
+ "▁clouds",
+ -12.170061111450195
+ ],
+ [
+ "inflammatory",
+ -12.170141220092773
+ ],
+ [
+ "țiuni",
+ -12.170199394226074
+ ],
+ [
+ "▁Cisco",
+ -12.17025089263916
+ ],
+ [
+ "▁würden",
+ -12.170254707336426
+ ],
+ [
+ "▁Shaw",
+ -12.170256614685059
+ ],
+ [
+ "▁Ell",
+ -12.170266151428223
+ ],
+ [
+ "avance",
+ -12.1703519821167
+ ],
+ [
+ "anglais",
+ -12.170365333557129
+ ],
+ [
+ "weil",
+ -12.170368194580078
+ ],
+ [
+ "▁singura",
+ -12.170464515686035
+ ],
+ [
+ "ACK",
+ -12.170489311218262
+ ],
+ [
+ "likewise",
+ -12.170522689819336
+ ],
+ [
+ "ographie",
+ -12.170646667480469
+ ],
+ [
+ "liegen",
+ -12.17088508605957
+ ],
+ [
+ "▁Crow",
+ -12.170964241027832
+ ],
+ [
+ "▁unic",
+ -12.171187400817871
+ ],
+ [
+ "▁Ale",
+ -12.171241760253906
+ ],
+ [
+ "▁păstr",
+ -12.17125129699707
+ ],
+ [
+ "▁informal",
+ -12.171337127685547
+ ],
+ [
+ "650",
+ -12.17136287689209
+ ],
+ [
+ "Benz",
+ -12.171489715576172
+ ],
+ [
+ "▁antenna",
+ -12.171540260314941
+ ],
+ [
+ "▁pagini",
+ -12.171552658081055
+ ],
+ [
+ "▁lansat",
+ -12.171561241149902
+ ],
+ [
+ "▁Fans",
+ -12.171576499938965
+ ],
+ [
+ "taine",
+ -12.171822547912598
+ ],
+ [
+ "JO",
+ -12.171853065490723
+ ],
+ [
+ "▁Tips",
+ -12.172091484069824
+ ],
+ [
+ "cir",
+ -12.172130584716797
+ ],
+ [
+ "nou",
+ -12.172384262084961
+ ],
+ [
+ "▁planted",
+ -12.17241382598877
+ ],
+ [
+ "▁steering",
+ -12.172423362731934
+ ],
+ [
+ "▁Waren",
+ -12.172475814819336
+ ],
+ [
+ "▁clearance",
+ -12.172515869140625
+ ],
+ [
+ "▁Moscow",
+ -12.172516822814941
+ ],
+ [
+ "▁Faith",
+ -12.172534942626953
+ ],
+ [
+ "▁Pizza",
+ -12.172572135925293
+ ],
+ [
+ "▁Tank",
+ -12.17273998260498
+ ],
+ [
+ "QUE",
+ -12.172783851623535
+ ],
+ [
+ "▁studii",
+ -12.172804832458496
+ ],
+ [
+ "éné",
+ -12.172829627990723
+ ],
+ [
+ "▁guerre",
+ -12.1728515625
+ ],
+ [
+ "▁celebr",
+ -12.173083305358887
+ ],
+ [
+ "▁Factory",
+ -12.173111915588379
+ ],
+ [
+ "▁Browse",
+ -12.173198699951172
+ ],
+ [
+ "▁Request",
+ -12.17323112487793
+ ],
+ [
+ "▁taxpayer",
+ -12.173311233520508
+ ],
+ [
+ "▁assert",
+ -12.173562049865723
+ ],
+ [
+ "unternehmen",
+ -12.173588752746582
+ ],
+ [
+ "▁Ergebnis",
+ -12.173687934875488
+ ],
+ [
+ "▁Antwort",
+ -12.173727035522461
+ ],
+ [
+ "▁Photography",
+ -12.173808097839355
+ ],
+ [
+ "▁plă",
+ -12.173866271972656
+ ],
+ [
+ "IME",
+ -12.173982620239258
+ ],
+ [
+ "▁prochaine",
+ -12.174074172973633
+ ],
+ [
+ "ajouter",
+ -12.174103736877441
+ ],
+ [
+ "▁buffet",
+ -12.174227714538574
+ ],
+ [
+ "▁pixels",
+ -12.174239158630371
+ ],
+ [
+ "▁pledge",
+ -12.174250602722168
+ ],
+ [
+ "▁Inhalt",
+ -12.17435359954834
+ ],
+ [
+ "▁chase",
+ -12.174384117126465
+ ],
+ [
+ "Flow",
+ -12.174493789672852
+ ],
+ [
+ "▁melodi",
+ -12.174872398376465
+ ],
+ [
+ "▁Abu",
+ -12.174991607666016
+ ],
+ [
+ "▁1979",
+ -12.175042152404785
+ ],
+ [
+ "▁Photos",
+ -12.175042152404785
+ ],
+ [
+ "▁qualifications",
+ -12.175148963928223
+ ],
+ [
+ "▁zis",
+ -12.175213813781738
+ ],
+ [
+ "IAL",
+ -12.175354957580566
+ ],
+ [
+ "▁lender",
+ -12.175390243530273
+ ],
+ [
+ "▁indiferent",
+ -12.175494194030762
+ ],
+ [
+ "▁behaviors",
+ -12.175506591796875
+ ],
+ [
+ "▁flowing",
+ -12.175531387329102
+ ],
+ [
+ "▁zweite",
+ -12.1756010055542
+ ],
+ [
+ "abl",
+ -12.175765037536621
+ ],
+ [
+ "Schw",
+ -12.176004409790039
+ ],
+ [
+ "opi",
+ -12.176030158996582
+ ],
+ [
+ "ggi",
+ -12.176164627075195
+ ],
+ [
+ "▁depart",
+ -12.176314353942871
+ ],
+ [
+ "▁garde",
+ -12.17640209197998
+ ],
+ [
+ "▁tuition",
+ -12.176490783691406
+ ],
+ [
+ "fälle",
+ -12.17650032043457
+ ],
+ [
+ "▁determina",
+ -12.17652702331543
+ ],
+ [
+ "▁spice",
+ -12.176627159118652
+ ],
+ [
+ "▁petites",
+ -12.176777839660645
+ ],
+ [
+ "kot",
+ -12.176973342895508
+ ],
+ [
+ "▁intersection",
+ -12.177242279052734
+ ],
+ [
+ "hak",
+ -12.177248001098633
+ ],
+ [
+ "▁autumn",
+ -12.177284240722656
+ ],
+ [
+ "▁verbunden",
+ -12.177284240722656
+ ],
+ [
+ "▁ferme",
+ -12.177287101745605
+ ],
+ [
+ "PN",
+ -12.17733097076416
+ ],
+ [
+ "▁insurer",
+ -12.177390098571777
+ ],
+ [
+ "arten",
+ -12.177401542663574
+ ],
+ [
+ "▁Turkish",
+ -12.177715301513672
+ ],
+ [
+ "▁shoulders",
+ -12.177732467651367
+ ],
+ [
+ "=>",
+ -12.177742004394531
+ ],
+ [
+ "▁Nike",
+ -12.177760124206543
+ ],
+ [
+ "uire",
+ -12.177763938903809
+ ],
+ [
+ "▁Chile",
+ -12.177811622619629
+ ],
+ [
+ "jon",
+ -12.177842140197754
+ ],
+ [
+ "▁fragrance",
+ -12.177884101867676
+ ],
+ [
+ "▁bean",
+ -12.177908897399902
+ ],
+ [
+ "ips",
+ -12.178108215332031
+ ],
+ [
+ "assuming",
+ -12.178191184997559
+ ],
+ [
+ "liens",
+ -12.178215026855469
+ ],
+ [
+ "tocmai",
+ -12.178267478942871
+ ],
+ [
+ "▁60%",
+ -12.178301811218262
+ ],
+ [
+ "ipped",
+ -12.178384780883789
+ ],
+ [
+ "DIS",
+ -12.178473472595215
+ ],
+ [
+ "▁predicted",
+ -12.178537368774414
+ ],
+ [
+ "▁Picture",
+ -12.178555488586426
+ ],
+ [
+ "Bahn",
+ -12.178796768188477
+ ],
+ [
+ "104",
+ -12.178854942321777
+ ],
+ [
+ "tended",
+ -12.178958892822266
+ ],
+ [
+ "▁approve",
+ -12.179031372070312
+ ],
+ [
+ "▁magasin",
+ -12.17908000946045
+ ],
+ [
+ "▁mindset",
+ -12.179208755493164
+ ],
+ [
+ "rase",
+ -12.179363250732422
+ ],
+ [
+ "grand",
+ -12.179469108581543
+ ],
+ [
+ "▁Principal",
+ -12.17947769165039
+ ],
+ [
+ "▁informații",
+ -12.17959976196289
+ ],
+ [
+ "▁legătur",
+ -12.179628372192383
+ ],
+ [
+ "▁Farb",
+ -12.179692268371582
+ ],
+ [
+ "▁Dieu",
+ -12.179710388183594
+ ],
+ [
+ "▁alliance",
+ -12.180378913879395
+ ],
+ [
+ "weiligen",
+ -12.180397987365723
+ ],
+ [
+ "▁Câ",
+ -12.18048095703125
+ ],
+ [
+ "▁counseling",
+ -12.180521011352539
+ ],
+ [
+ "▁traveled",
+ -12.180533409118652
+ ],
+ [
+ "▁translated",
+ -12.180558204650879
+ ],
+ [
+ "▁carne",
+ -12.180679321289062
+ ],
+ [
+ "aked",
+ -12.180707931518555
+ ],
+ [
+ "▁LCD",
+ -12.180868148803711
+ ],
+ [
+ "▁Folge",
+ -12.180909156799316
+ ],
+ [
+ "▁Erfahrungen",
+ -12.18093204498291
+ ],
+ [
+ "▁1981",
+ -12.18106460571289
+ ],
+ [
+ "▁răspuns",
+ -12.181075096130371
+ ],
+ [
+ "itori",
+ -12.18117618560791
+ ],
+ [
+ "▁elementary",
+ -12.181200981140137
+ ],
+ [
+ "▁vorbei",
+ -12.18127727508545
+ ],
+ [
+ "▁cargo",
+ -12.181361198425293
+ ],
+ [
+ "disciplinary",
+ -12.18140983581543
+ ],
+ [
+ "WR",
+ -12.181492805480957
+ ],
+ [
+ "▁counterpart",
+ -12.18162727355957
+ ],
+ [
+ "family",
+ -12.181641578674316
+ ],
+ [
+ "▁viață",
+ -12.181644439697266
+ ],
+ [
+ "▁Definition",
+ -12.18167495727539
+ ],
+ [
+ "▁Cow",
+ -12.18171501159668
+ ],
+ [
+ "fällig",
+ -12.182003021240234
+ ],
+ [
+ "▁Sicht",
+ -12.182025909423828
+ ],
+ [
+ "▁mum",
+ -12.182145118713379
+ ],
+ [
+ "▁Mediterranean",
+ -12.182275772094727
+ ],
+ [
+ "nev",
+ -12.182278633117676
+ ],
+ [
+ "bü",
+ -12.182293891906738
+ ],
+ [
+ "▁slave",
+ -12.182293891906738
+ ],
+ [
+ "schnitt",
+ -12.18233871459961
+ ],
+ [
+ "▁firme",
+ -12.182430267333984
+ ],
+ [
+ "▁spill",
+ -12.182454109191895
+ ],
+ [
+ "▁wages",
+ -12.182592391967773
+ ],
+ [
+ "▁refine",
+ -12.182615280151367
+ ],
+ [
+ "▁upgraded",
+ -12.182632446289062
+ ],
+ [
+ "▁gospel",
+ -12.182698249816895
+ ],
+ [
+ "▁quartier",
+ -12.182744979858398
+ ],
+ [
+ "▁#2",
+ -12.182772636413574
+ ],
+ [
+ "▁Situation",
+ -12.18298625946045
+ ],
+ [
+ "▁suggesting",
+ -12.183075904846191
+ ],
+ [
+ "▁acne",
+ -12.183113098144531
+ ],
+ [
+ "▁Murray",
+ -12.183337211608887
+ ],
+ [
+ "▁Ian",
+ -12.183469772338867
+ ],
+ [
+ "hören",
+ -12.183489799499512
+ ],
+ [
+ "bia",
+ -12.183603286743164
+ ],
+ [
+ "▁Bewegung",
+ -12.183684349060059
+ ],
+ [
+ "▁abzu",
+ -12.18379020690918
+ ],
+ [
+ "reveals",
+ -12.183795928955078
+ ],
+ [
+ "friend",
+ -12.184025764465332
+ ],
+ [
+ "▁Connecticut",
+ -12.18407917022705
+ ],
+ [
+ "▁Testament",
+ -12.184151649475098
+ ],
+ [
+ "▁Lit",
+ -12.184199333190918
+ ],
+ [
+ "▁Ship",
+ -12.184209823608398
+ ],
+ [
+ "▁minunat",
+ -12.184344291687012
+ ],
+ [
+ "▁Moving",
+ -12.184346199035645
+ ],
+ [
+ "▁Device",
+ -12.184486389160156
+ ],
+ [
+ "▁Bake",
+ -12.18453598022461
+ ],
+ [
+ "▁qualification",
+ -12.184633255004883
+ ],
+ [
+ "▁challenged",
+ -12.184640884399414
+ ],
+ [
+ "▁Hinweis",
+ -12.184721946716309
+ ],
+ [
+ "▁sechs",
+ -12.184769630432129
+ ],
+ [
+ "та",
+ -12.184903144836426
+ ],
+ [
+ "120",
+ -12.184904098510742
+ ],
+ [
+ "licht",
+ -12.184940338134766
+ ],
+ [
+ "▁supervision",
+ -12.185022354125977
+ ],
+ [
+ "▁milestone",
+ -12.18503189086914
+ ],
+ [
+ "zeig",
+ -12.185050964355469
+ ],
+ [
+ "▁emphasize",
+ -12.185224533081055
+ ],
+ [
+ "▁complain",
+ -12.185232162475586
+ ],
+ [
+ "sack",
+ -12.185341835021973
+ ],
+ [
+ "▁rebuild",
+ -12.185445785522461
+ ],
+ [
+ "projekt",
+ -12.18548583984375
+ ],
+ [
+ "▁saint",
+ -12.185644149780273
+ ],
+ [
+ "lette",
+ -12.185752868652344
+ ],
+ [
+ "rade",
+ -12.18580150604248
+ ],
+ [
+ "▁pacient",
+ -12.185893058776855
+ ],
+ [
+ "signed",
+ -12.186169624328613
+ ],
+ [
+ "▁mil",
+ -12.186261177062988
+ ],
+ [
+ "cali",
+ -12.186266899108887
+ ],
+ [
+ "▁brochure",
+ -12.186487197875977
+ ],
+ [
+ "▁Bulgaria",
+ -12.186488151550293
+ ],
+ [
+ "Har",
+ -12.186623573303223
+ ],
+ [
+ "DH",
+ -12.186697006225586
+ ],
+ [
+ "▁jumping",
+ -12.186712265014648
+ ],
+ [
+ "ären",
+ -12.186732292175293
+ ],
+ [
+ "▁tactics",
+ -12.186911582946777
+ ],
+ [
+ "▁soleil",
+ -12.187030792236328
+ ],
+ [
+ "lessness",
+ -12.18705940246582
+ ],
+ [
+ "steigen",
+ -12.187085151672363
+ ],
+ [
+ "▁Brief",
+ -12.187117576599121
+ ],
+ [
+ "▁Oz",
+ -12.18718433380127
+ ],
+ [
+ "credit",
+ -12.187239646911621
+ ],
+ [
+ "glass",
+ -12.187241554260254
+ ],
+ [
+ "▁Baltimore",
+ -12.187292098999023
+ ],
+ [
+ "varies",
+ -12.187445640563965
+ ],
+ [
+ "sourced",
+ -12.187575340270996
+ ],
+ [
+ "▁documented",
+ -12.187604904174805
+ ],
+ [
+ "▁devine",
+ -12.187664985656738
+ ],
+ [
+ "möglichst",
+ -12.187732696533203
+ ],
+ [
+ "▁früher",
+ -12.187756538391113
+ ],
+ [
+ "outefois",
+ -12.18790054321289
+ ],
+ [
+ "▁Engagement",
+ -12.187934875488281
+ ],
+ [
+ "▁anumit",
+ -12.18806266784668
+ ],
+ [
+ "▁1930",
+ -12.188186645507812
+ ],
+ [
+ "▁Aufgaben",
+ -12.188214302062988
+ ],
+ [
+ "▁lineup",
+ -12.188227653503418
+ ],
+ [
+ "▁Cad",
+ -12.188349723815918
+ ],
+ [
+ "améliorer",
+ -12.188437461853027
+ ],
+ [
+ "▁februarie",
+ -12.188499450683594
+ ],
+ [
+ "▁cancellation",
+ -12.188529968261719
+ ],
+ [
+ "▁locks",
+ -12.188577651977539
+ ],
+ [
+ "▁modèles",
+ -12.188711166381836
+ ],
+ [
+ "▁breakdown",
+ -12.188748359680176
+ ],
+ [
+ "Ticket",
+ -12.188810348510742
+ ],
+ [
+ "▁Chen",
+ -12.188855171203613
+ ],
+ [
+ "▁Competition",
+ -12.188910484313965
+ ],
+ [
+ "▁median",
+ -12.18896770477295
+ ],
+ [
+ "rische",
+ -12.189159393310547
+ ],
+ [
+ "▁multipli",
+ -12.189269065856934
+ ],
+ [
+ "▁Belgium",
+ -12.189305305480957
+ ],
+ [
+ "▁Physical",
+ -12.189308166503906
+ ],
+ [
+ "▁parameter",
+ -12.189432144165039
+ ],
+ [
+ "▁carrot",
+ -12.189435005187988
+ ],
+ [
+ "▁mandat",
+ -12.189617156982422
+ ],
+ [
+ "▁towel",
+ -12.189697265625
+ ],
+ [
+ "▁insured",
+ -12.189825057983398
+ ],
+ [
+ "PRI",
+ -12.189868927001953
+ ],
+ [
+ "etter",
+ -12.189915657043457
+ ],
+ [
+ "▁Oder",
+ -12.190083503723145
+ ],
+ [
+ "argued",
+ -12.190171241760254
+ ],
+ [
+ "FB",
+ -12.190196990966797
+ ],
+ [
+ "versicherung",
+ -12.190197944641113
+ ],
+ [
+ "abila",
+ -12.190251350402832
+ ],
+ [
+ "▁Coin",
+ -12.190324783325195
+ ],
+ [
+ "around",
+ -12.19050121307373
+ ],
+ [
+ "▁Lorsqu",
+ -12.190773963928223
+ ],
+ [
+ "valent",
+ -12.190918922424316
+ ],
+ [
+ "▁weltweit",
+ -12.19092082977295
+ ],
+ [
+ "Mod",
+ -12.191039085388184
+ ],
+ [
+ "▁defect",
+ -12.191044807434082
+ ],
+ [
+ "ibly",
+ -12.191136360168457
+ ],
+ [
+ "▁Juan",
+ -12.191153526306152
+ ],
+ [
+ "▁Jur",
+ -12.191171646118164
+ ],
+ [
+ "large",
+ -12.191307067871094
+ ],
+ [
+ "▁indicators",
+ -12.191461563110352
+ ],
+ [
+ "invest",
+ -12.19168472290039
+ ],
+ [
+ "▁rehabilitation",
+ -12.191705703735352
+ ],
+ [
+ "nag",
+ -12.191823959350586
+ ],
+ [
+ "▁Grundlage",
+ -12.191829681396484
+ ],
+ [
+ "▁Strategy",
+ -12.192131042480469
+ ],
+ [
+ "▁supérieur",
+ -12.192173957824707
+ ],
+ [
+ "▁orbit",
+ -12.192281723022461
+ ],
+ [
+ "▁Auftrag",
+ -12.192360877990723
+ ],
+ [
+ "▁Verb",
+ -12.192441940307617
+ ],
+ [
+ "ANA",
+ -12.19256591796875
+ ],
+ [
+ "▁trimis",
+ -12.192611694335938
+ ],
+ [
+ "▁Rub",
+ -12.192704200744629
+ ],
+ [
+ "institu",
+ -12.192732810974121
+ ],
+ [
+ "▁inspect",
+ -12.1927490234375
+ ],
+ [
+ "▁Princess",
+ -12.192757606506348
+ ],
+ [
+ "especially",
+ -12.192777633666992
+ ],
+ [
+ "▁combinations",
+ -12.192793846130371
+ ],
+ [
+ "▁gaze",
+ -12.192842483520508
+ ],
+ [
+ "elemente",
+ -12.192970275878906
+ ],
+ [
+ "deal",
+ -12.192980766296387
+ ],
+ [
+ "polis",
+ -12.193157196044922
+ ],
+ [
+ "shaw",
+ -12.193168640136719
+ ],
+ [
+ "▁Republicans",
+ -12.193203926086426
+ ],
+ [
+ "aded",
+ -12.193244934082031
+ ],
+ [
+ "▁Louisiana",
+ -12.193364143371582
+ ],
+ [
+ "▁Ville",
+ -12.193368911743164
+ ],
+ [
+ "▁afterwards",
+ -12.193389892578125
+ ],
+ [
+ "ONG",
+ -12.193608283996582
+ ],
+ [
+ "▁dryer",
+ -12.193636894226074
+ ],
+ [
+ "▁Manhattan",
+ -12.19374942779541
+ ],
+ [
+ "▁recomanda",
+ -12.19412612915039
+ ],
+ [
+ "▁juca",
+ -12.194253921508789
+ ],
+ [
+ "▁Crown",
+ -12.194260597229004
+ ],
+ [
+ "▁flesh",
+ -12.194347381591797
+ ],
+ [
+ "sichtig",
+ -12.194358825683594
+ ],
+ [
+ "▁rempli",
+ -12.19437026977539
+ ],
+ [
+ "▁deposits",
+ -12.19438362121582
+ ],
+ [
+ "▁Voll",
+ -12.194599151611328
+ ],
+ [
+ "▁analysts",
+ -12.194672584533691
+ ],
+ [
+ "▁Krieg",
+ -12.19484806060791
+ ],
+ [
+ "▁Rosa",
+ -12.19495964050293
+ ],
+ [
+ "▁Supply",
+ -12.194964408874512
+ ],
+ [
+ "GF",
+ -12.19497013092041
+ ],
+ [
+ "idad",
+ -12.195098876953125
+ ],
+ [
+ "▁flush",
+ -12.195103645324707
+ ],
+ [
+ "▁circular",
+ -12.195355415344238
+ ],
+ [
+ "▁național",
+ -12.195379257202148
+ ],
+ [
+ "▁lorsqu",
+ -12.195441246032715
+ ],
+ [
+ "▁analyst",
+ -12.195459365844727
+ ],
+ [
+ "▁Jahrhundert",
+ -12.195586204528809
+ ],
+ [
+ "▁biology",
+ -12.195713996887207
+ ],
+ [
+ "copy",
+ -12.195733070373535
+ ],
+ [
+ "▁bringt",
+ -12.195765495300293
+ ],
+ [
+ "▁Gospel",
+ -12.195780754089355
+ ],
+ [
+ "▁sorgen",
+ -12.195842742919922
+ ],
+ [
+ "zeichnung",
+ -12.196181297302246
+ ],
+ [
+ "chair",
+ -12.196197509765625
+ ],
+ [
+ "EB",
+ -12.19636344909668
+ ],
+ [
+ "▁Beth",
+ -12.1964111328125
+ ],
+ [
+ "115",
+ -12.196416854858398
+ ],
+ [
+ "▁Neue",
+ -12.196479797363281
+ ],
+ [
+ "▁faible",
+ -12.196599960327148
+ ],
+ [
+ "▁methodology",
+ -12.196603775024414
+ ],
+ [
+ "spiele",
+ -12.196647644042969
+ ],
+ [
+ "▁cherry",
+ -12.196727752685547
+ ],
+ [
+ "▁Mak",
+ -12.196802139282227
+ ],
+ [
+ "▁volet",
+ -12.196982383728027
+ ],
+ [
+ "funk",
+ -12.197196006774902
+ ],
+ [
+ "▁aktuelle",
+ -12.197372436523438
+ ],
+ [
+ "▁Yahoo",
+ -12.197408676147461
+ ],
+ [
+ "▁Zusammenarbeit",
+ -12.197669982910156
+ ],
+ [
+ "▁Serve",
+ -12.197754859924316
+ ],
+ [
+ "▁simpler",
+ -12.197978019714355
+ ],
+ [
+ "intégr",
+ -12.197990417480469
+ ],
+ [
+ "ndlich",
+ -12.198083877563477
+ ],
+ [
+ "▁actress",
+ -12.198320388793945
+ ],
+ [
+ "▁reuse",
+ -12.198332786560059
+ ],
+ [
+ "▁reviewing",
+ -12.198405265808105
+ ],
+ [
+ "statt",
+ -12.198457717895508
+ ],
+ [
+ "▁diving",
+ -12.198469161987305
+ ],
+ [
+ "▁Național",
+ -12.198677062988281
+ ],
+ [
+ "voi",
+ -12.19873332977295
+ ],
+ [
+ "Disc",
+ -12.198812484741211
+ ],
+ [
+ "▁Mineral",
+ -12.19886302947998
+ ],
+ [
+ "▁emit",
+ -12.199007034301758
+ ],
+ [
+ "witz",
+ -12.199078559875488
+ ],
+ [
+ "▁forgot",
+ -12.19909954071045
+ ],
+ [
+ "▁dim",
+ -12.199115753173828
+ ],
+ [
+ "upper",
+ -12.19947624206543
+ ],
+ [
+ "sichtlich",
+ -12.19949722290039
+ ],
+ [
+ "▁parcours",
+ -12.199670791625977
+ ],
+ [
+ "8:00",
+ -12.199697494506836
+ ],
+ [
+ "▁keyword",
+ -12.199701309204102
+ ],
+ [
+ "▁upgrades",
+ -12.199763298034668
+ ],
+ [
+ "kunden",
+ -12.200177192687988
+ ],
+ [
+ "▁Seg",
+ -12.200257301330566
+ ],
+ [
+ "▁Circle",
+ -12.200289726257324
+ ],
+ [
+ "▁ginger",
+ -12.200336456298828
+ ],
+ [
+ "mment",
+ -12.200516700744629
+ ],
+ [
+ "▁expenditure",
+ -12.200655937194824
+ ],
+ [
+ "▁parle",
+ -12.200693130493164
+ ],
+ [
+ "▁Counsel",
+ -12.200722694396973
+ ],
+ [
+ "▁Gui",
+ -12.200722694396973
+ ],
+ [
+ "resident",
+ -12.20103645324707
+ ],
+ [
+ "▁benchmark",
+ -12.20103931427002
+ ],
+ [
+ "▁Elektro",
+ -12.201064109802246
+ ],
+ [
+ "▁réalité",
+ -12.201064109802246
+ ],
+ [
+ "▁ridiculous",
+ -12.201067924499512
+ ],
+ [
+ "▁necklace",
+ -12.20108699798584
+ ],
+ [
+ "nian",
+ -12.201117515563965
+ ],
+ [
+ "▁Move",
+ -12.20113468170166
+ ],
+ [
+ "▁elevated",
+ -12.201204299926758
+ ],
+ [
+ "WE",
+ -12.201281547546387
+ ],
+ [
+ "▁Drum",
+ -12.20132064819336
+ ],
+ [
+ "▁Delivery",
+ -12.201350212097168
+ ],
+ [
+ "indicating",
+ -12.201452255249023
+ ],
+ [
+ "▁Benjamin",
+ -12.201472282409668
+ ],
+ [
+ "▁Samuel",
+ -12.2014741897583
+ ],
+ [
+ "bene",
+ -12.201666831970215
+ ],
+ [
+ "▁experienta",
+ -12.201676368713379
+ ],
+ [
+ "▁rocket",
+ -12.201839447021484
+ ],
+ [
+ "▁fossil",
+ -12.201883316040039
+ ],
+ [
+ "▁festive",
+ -12.20193099975586
+ ],
+ [
+ "▁conscience",
+ -12.201964378356934
+ ],
+ [
+ "▁bacon",
+ -12.202136993408203
+ ],
+ [
+ "▁aero",
+ -12.202159881591797
+ ],
+ [
+ "public",
+ -12.202187538146973
+ ],
+ [
+ "▁zic",
+ -12.202218055725098
+ ],
+ [
+ "ombre",
+ -12.202356338500977
+ ],
+ [
+ "▁Drain",
+ -12.202550888061523
+ ],
+ [
+ "7.5",
+ -12.202672004699707
+ ],
+ [
+ "▁Deutschen",
+ -12.202703475952148
+ ],
+ [
+ "reportedly",
+ -12.202754974365234
+ ],
+ [
+ "▁Français",
+ -12.203105926513672
+ ],
+ [
+ "▁enzyme",
+ -12.203106880187988
+ ],
+ [
+ "▁inquiry",
+ -12.203117370605469
+ ],
+ [
+ "▁presque",
+ -12.203193664550781
+ ],
+ [
+ "▁Airlines",
+ -12.203228950500488
+ ],
+ [
+ "▁Salon",
+ -12.203237533569336
+ ],
+ [
+ "▁Volunteer",
+ -12.203310012817383
+ ],
+ [
+ "▁modular",
+ -12.203349113464355
+ ],
+ [
+ "ón",
+ -12.203364372253418
+ ],
+ [
+ "NH",
+ -12.203449249267578
+ ],
+ [
+ "▁souhaite",
+ -12.203516960144043
+ ],
+ [
+ "social",
+ -12.203659057617188
+ ],
+ [
+ "▁Include",
+ -12.203729629516602
+ ],
+ [
+ "▁Decor",
+ -12.2037992477417
+ ],
+ [
+ "dded",
+ -12.203965187072754
+ ],
+ [
+ "▁Außen",
+ -12.203969955444336
+ ],
+ [
+ "rendu",
+ -12.20412540435791
+ ],
+ [
+ "▁MBA",
+ -12.204150199890137
+ ],
+ [
+ "▁columns",
+ -12.204155921936035
+ ],
+ [
+ "▁Wing",
+ -12.204436302185059
+ ],
+ [
+ "▁landmark",
+ -12.204442977905273
+ ],
+ [
+ "schritt",
+ -12.204594612121582
+ ],
+ [
+ "▁désir",
+ -12.204630851745605
+ ],
+ [
+ "(5)",
+ -12.204680442810059
+ ],
+ [
+ "▁réseaux",
+ -12.204693794250488
+ ],
+ [
+ "income",
+ -12.204710960388184
+ ],
+ [
+ "▁revised",
+ -12.204819679260254
+ ],
+ [
+ "HY",
+ -12.204863548278809
+ ],
+ [
+ "▁Explorer",
+ -12.204873085021973
+ ],
+ [
+ "▁Lam",
+ -12.204877853393555
+ ],
+ [
+ "▁almond",
+ -12.204910278320312
+ ],
+ [
+ "▁faux",
+ -12.204910278320312
+ ],
+ [
+ "opt",
+ -12.204923629760742
+ ],
+ [
+ "Out",
+ -12.204939842224121
+ ],
+ [
+ "▁virtue",
+ -12.205025672912598
+ ],
+ [
+ "▁Chocolate",
+ -12.205151557922363
+ ],
+ [
+ "▁spannend",
+ -12.205305099487305
+ ],
+ [
+ "▁spices",
+ -12.205327033996582
+ ],
+ [
+ "▁Climate",
+ -12.205560684204102
+ ],
+ [
+ "▁Residential",
+ -12.205560684204102
+ ],
+ [
+ "gung",
+ -12.205700874328613
+ ],
+ [
+ "▁filtr",
+ -12.20606803894043
+ ],
+ [
+ "circ",
+ -12.206123352050781
+ ],
+ [
+ "sisted",
+ -12.206172943115234
+ ],
+ [
+ "▁dedicat",
+ -12.206243515014648
+ ],
+ [
+ "▁foil",
+ -12.206387519836426
+ ],
+ [
+ "▁uita",
+ -12.206392288208008
+ ],
+ [
+ "▁lié",
+ -12.206402778625488
+ ],
+ [
+ "▁Demo",
+ -12.206409454345703
+ ],
+ [
+ "▁spoil",
+ -12.2064208984375
+ ],
+ [
+ "Cu",
+ -12.206448554992676
+ ],
+ [
+ "naut",
+ -12.206525802612305
+ ],
+ [
+ "▁configured",
+ -12.206535339355469
+ ],
+ [
+ "UK",
+ -12.206543922424316
+ ],
+ [
+ "▁disagree",
+ -12.20656967163086
+ ],
+ [
+ "Medic",
+ -12.206767082214355
+ ],
+ [
+ "cosm",
+ -12.207074165344238
+ ],
+ [
+ "Toute",
+ -12.207109451293945
+ ],
+ [
+ "▁beneficia",
+ -12.207170486450195
+ ],
+ [
+ "fassen",
+ -12.207327842712402
+ ],
+ [
+ "▁bail",
+ -12.207337379455566
+ ],
+ [
+ "igue",
+ -12.207439422607422
+ ],
+ [
+ "▁Mă",
+ -12.20744800567627
+ ],
+ [
+ "▁strips",
+ -12.20748519897461
+ ],
+ [
+ "▁Dritte",
+ -12.207537651062012
+ ],
+ [
+ "▁putere",
+ -12.207597732543945
+ ],
+ [
+ "Play",
+ -12.20763111114502
+ ],
+ [
+ "▁Samstag",
+ -12.207632064819336
+ ],
+ [
+ "▁households",
+ -12.207791328430176
+ ],
+ [
+ "▁persistent",
+ -12.207914352416992
+ ],
+ [
+ "uben",
+ -12.207942962646484
+ ],
+ [
+ "Web",
+ -12.20809555053711
+ ],
+ [
+ "▁scenery",
+ -12.20820140838623
+ ],
+ [
+ "▁défini",
+ -12.208257675170898
+ ],
+ [
+ "news",
+ -12.208337783813477
+ ],
+ [
+ "eira",
+ -12.208428382873535
+ ],
+ [
+ "▁Mumbai",
+ -12.208438873291016
+ ],
+ [
+ "▁Ward",
+ -12.208558082580566
+ ],
+ [
+ "▁ladder",
+ -12.2086181640625
+ ],
+ [
+ "▁plaque",
+ -12.208623886108398
+ ],
+ [
+ "nés",
+ -12.208639144897461
+ ],
+ [
+ "▁condamn",
+ -12.20864486694336
+ ],
+ [
+ "▁attribute",
+ -12.208687782287598
+ ],
+ [
+ "atti",
+ -12.20873737335205
+ ],
+ [
+ "▁Emily",
+ -12.208953857421875
+ ],
+ [
+ "▁pleine",
+ -12.20896053314209
+ ],
+ [
+ "▁automatisch",
+ -12.209004402160645
+ ],
+ [
+ "ifies",
+ -12.209052085876465
+ ],
+ [
+ "onna",
+ -12.209104537963867
+ ],
+ [
+ "▁inject",
+ -12.209157943725586
+ ],
+ [
+ "▁evolve",
+ -12.209297180175781
+ ],
+ [
+ "▁breeze",
+ -12.209299087524414
+ ],
+ [
+ "▁montre",
+ -12.209415435791016
+ ],
+ [
+ "▁memorial",
+ -12.209425926208496
+ ],
+ [
+ "ämlich",
+ -12.209465026855469
+ ],
+ [
+ "NBC",
+ -12.209589958190918
+ ],
+ [
+ "▁1940",
+ -12.209836959838867
+ ],
+ [
+ "▁trouvé",
+ -12.209892272949219
+ ],
+ [
+ "when",
+ -12.209914207458496
+ ],
+ [
+ "▁Büro",
+ -12.209959983825684
+ ],
+ [
+ "▁probability",
+ -12.209978103637695
+ ],
+ [
+ "cute",
+ -12.21006965637207
+ ],
+ [
+ "▁sturdy",
+ -12.210078239440918
+ ],
+ [
+ "AMP",
+ -12.210165023803711
+ ],
+ [
+ "▁Constantin",
+ -12.210283279418945
+ ],
+ [
+ "▁batter",
+ -12.21037483215332
+ ],
+ [
+ "▁bist",
+ -12.210470199584961
+ ],
+ [
+ "▁streams",
+ -12.210528373718262
+ ],
+ [
+ "rushing",
+ -12.21057415008545
+ ],
+ [
+ "▁shaft",
+ -12.21065902709961
+ ],
+ [
+ "▁proprii",
+ -12.210722923278809
+ ],
+ [
+ "émi",
+ -12.21074390411377
+ ],
+ [
+ "online",
+ -12.210817337036133
+ ],
+ [
+ "▁vanity",
+ -12.210870742797852
+ ],
+ [
+ "▁mural",
+ -12.210878372192383
+ ],
+ [
+ "▁distinguish",
+ -12.210905075073242
+ ],
+ [
+ "▁niciun",
+ -12.211191177368164
+ ],
+ [
+ "▁européenne",
+ -12.211252212524414
+ ],
+ [
+ "▁secretary",
+ -12.211289405822754
+ ],
+ [
+ "▁gaps",
+ -12.211492538452148
+ ],
+ [
+ "▁realm",
+ -12.211499214172363
+ ],
+ [
+ "▁elastic",
+ -12.211504936218262
+ ],
+ [
+ "▁Avoid",
+ -12.211519241333008
+ ],
+ [
+ "▁mauvais",
+ -12.211931228637695
+ ],
+ [
+ "▁innovations",
+ -12.212663650512695
+ ],
+ [
+ "▁suprem",
+ -12.212776184082031
+ ],
+ [
+ "▁vederea",
+ -12.212817192077637
+ ],
+ [
+ "wenden",
+ -12.212892532348633
+ ],
+ [
+ "-40",
+ -12.213075637817383
+ ],
+ [
+ "prenant",
+ -12.213155746459961
+ ],
+ [
+ "utilisateur",
+ -12.213210105895996
+ ],
+ [
+ "▁Oliver",
+ -12.213228225708008
+ ],
+ [
+ "111",
+ -12.21326732635498
+ ],
+ [
+ "▁manifestation",
+ -12.213382720947266
+ ],
+ [
+ "▁Rachel",
+ -12.213458061218262
+ ],
+ [
+ "agog",
+ -12.21348762512207
+ ],
+ [
+ "▁seamless",
+ -12.213534355163574
+ ],
+ [
+ "▁Employee",
+ -12.213576316833496
+ ],
+ [
+ "▁dimanche",
+ -12.213582038879395
+ ],
+ [
+ "▁banii",
+ -12.213631629943848
+ ],
+ [
+ "▁Ruth",
+ -12.213781356811523
+ ],
+ [
+ "▁Roy",
+ -12.21385383605957
+ ],
+ [
+ "▁homeless",
+ -12.2139253616333
+ ],
+ [
+ "▁Lower",
+ -12.213932037353516
+ ],
+ [
+ "health",
+ -12.21393871307373
+ ],
+ [
+ "▁atenti",
+ -12.2140474319458
+ ],
+ [
+ "▁touched",
+ -12.214183807373047
+ ],
+ [
+ "May",
+ -12.214195251464844
+ ],
+ [
+ "▁Buc",
+ -12.214225769042969
+ ],
+ [
+ "▁explored",
+ -12.214393615722656
+ ],
+ [
+ "▁declare",
+ -12.214461326599121
+ ],
+ [
+ "▁garment",
+ -12.214469909667969
+ ],
+ [
+ "▁buzz",
+ -12.214483261108398
+ ],
+ [
+ "▁rappel",
+ -12.214662551879883
+ ],
+ [
+ "▁uscat",
+ -12.214903831481934
+ ],
+ [
+ "▁Hyper",
+ -12.214914321899414
+ ],
+ [
+ "Etat",
+ -12.215007781982422
+ ],
+ [
+ "▁Titel",
+ -12.215035438537598
+ ],
+ [
+ "product",
+ -12.215191841125488
+ ],
+ [
+ "woman",
+ -12.215280532836914
+ ],
+ [
+ "▁Gab",
+ -12.215450286865234
+ ],
+ [
+ "▁advances",
+ -12.215615272521973
+ ],
+ [
+ "2/",
+ -12.215753555297852
+ ],
+ [
+ "prone",
+ -12.215770721435547
+ ],
+ [
+ "kö",
+ -12.215986251831055
+ ],
+ [
+ "▁counting",
+ -12.21599292755127
+ ],
+ [
+ "Sollte",
+ -12.216043472290039
+ ],
+ [
+ "▁Konzept",
+ -12.216063499450684
+ ],
+ [
+ "▁backgrounds",
+ -12.216153144836426
+ ],
+ [
+ "jährige",
+ -12.216154098510742
+ ],
+ [
+ "▁Alltag",
+ -12.216187477111816
+ ],
+ [
+ "▁metrics",
+ -12.21619701385498
+ ],
+ [
+ "▁illustrated",
+ -12.216222763061523
+ ],
+ [
+ "▁Charge",
+ -12.21631908416748
+ ],
+ [
+ "▁thoughtful",
+ -12.216423034667969
+ ],
+ [
+ "gesetz",
+ -12.216527938842773
+ ],
+ [
+ "pfen",
+ -12.216611862182617
+ ],
+ [
+ "▁déroul",
+ -12.216713905334473
+ ],
+ [
+ "▁checkout",
+ -12.216876029968262
+ ],
+ [
+ "quette",
+ -12.216936111450195
+ ],
+ [
+ "▁pierdut",
+ -12.2170991897583
+ ],
+ [
+ "▁Seat",
+ -12.217140197753906
+ ],
+ [
+ "▁linen",
+ -12.217193603515625
+ ],
+ [
+ "archiv",
+ -12.217245101928711
+ ],
+ [
+ "arna",
+ -12.217254638671875
+ ],
+ [
+ "importe",
+ -12.21742057800293
+ ],
+ [
+ "▁PHP",
+ -12.217496871948242
+ ],
+ [
+ "▁Parents",
+ -12.217503547668457
+ ],
+ [
+ "▁Birmingham",
+ -12.217513084411621
+ ],
+ [
+ "▁Integr",
+ -12.217588424682617
+ ],
+ [
+ "▁Mason",
+ -12.217607498168945
+ ],
+ [
+ "zieht",
+ -12.217781066894531
+ ],
+ [
+ "▁camps",
+ -12.217803001403809
+ ],
+ [
+ "OG",
+ -12.21786117553711
+ ],
+ [
+ "▁syrup",
+ -12.217927932739258
+ ],
+ [
+ "▁Cookies",
+ -12.217928886413574
+ ],
+ [
+ "▁Comfort",
+ -12.217955589294434
+ ],
+ [
+ "ută",
+ -12.217976570129395
+ ],
+ [
+ "abia",
+ -12.217979431152344
+ ],
+ [
+ "zeci",
+ -12.218003273010254
+ ],
+ [
+ "▁Gardens",
+ -12.218009948730469
+ ],
+ [
+ "▁incidents",
+ -12.218149185180664
+ ],
+ [
+ "▁participat",
+ -12.218235969543457
+ ],
+ [
+ "▁glimpse",
+ -12.218342781066895
+ ],
+ [
+ "5.5",
+ -12.218437194824219
+ ],
+ [
+ "▁dealers",
+ -12.218469619750977
+ ],
+ [
+ "▁Grande",
+ -12.218565940856934
+ ],
+ [
+ "▁raid",
+ -12.218944549560547
+ ],
+ [
+ "owing",
+ -12.21903133392334
+ ],
+ [
+ "▁contrary",
+ -12.219109535217285
+ ],
+ [
+ "Earlier",
+ -12.219138145446777
+ ],
+ [
+ "tien",
+ -12.21916389465332
+ ],
+ [
+ "drop",
+ -12.219169616699219
+ ],
+ [
+ "▁angajat",
+ -12.219359397888184
+ ],
+ [
+ "▁procesul",
+ -12.219515800476074
+ ],
+ [
+ "▁focal",
+ -12.219564437866211
+ ],
+ [
+ "▁impart",
+ -12.219703674316406
+ ],
+ [
+ "▁Abschluss",
+ -12.219749450683594
+ ],
+ [
+ "carui",
+ -12.219830513000488
+ ],
+ [
+ "insul",
+ -12.220277786254883
+ ],
+ [
+ "▁creamy",
+ -12.220283508300781
+ ],
+ [
+ "eille",
+ -12.22032356262207
+ ],
+ [
+ "suppl",
+ -12.220335960388184
+ ],
+ [
+ "▁Heaven",
+ -12.220471382141113
+ ],
+ [
+ "éna",
+ -12.220667839050293
+ ],
+ [
+ "▁swap",
+ -12.220739364624023
+ ],
+ [
+ "▁vreau",
+ -12.220762252807617
+ ],
+ [
+ "▁Bryan",
+ -12.220809936523438
+ ],
+ [
+ "▁Zug",
+ -12.220815658569336
+ ],
+ [
+ "▁glance",
+ -12.220848083496094
+ ],
+ [
+ "▁elimin",
+ -12.220900535583496
+ ],
+ [
+ "▁yeux",
+ -12.221084594726562
+ ],
+ [
+ "wehr",
+ -12.221238136291504
+ ],
+ [
+ "2.5",
+ -12.221287727355957
+ ],
+ [
+ "▁poses",
+ -12.221364974975586
+ ],
+ [
+ "▁parcel",
+ -12.221585273742676
+ ],
+ [
+ "▁Apartment",
+ -12.221749305725098
+ ],
+ [
+ "▁NASA",
+ -12.221768379211426
+ ],
+ [
+ "▁bénéfici",
+ -12.22187614440918
+ ],
+ [
+ "▁Umgebung",
+ -12.221890449523926
+ ],
+ [
+ "asia",
+ -12.221946716308594
+ ],
+ [
+ "abi",
+ -12.221967697143555
+ ],
+ [
+ "coup",
+ -12.222002983093262
+ ],
+ [
+ "synchron",
+ -12.222017288208008
+ ],
+ [
+ "▁Sicherheits",
+ -12.222029685974121
+ ],
+ [
+ "bic",
+ -12.222076416015625
+ ],
+ [
+ "▁distract",
+ -12.222148895263672
+ ],
+ [
+ "▁rentals",
+ -12.222163200378418
+ ],
+ [
+ "constru",
+ -12.222290992736816
+ ],
+ [
+ "curs",
+ -12.222345352172852
+ ],
+ [
+ "genannten",
+ -12.222386360168457
+ ],
+ [
+ "▁Shanghai",
+ -12.222501754760742
+ ],
+ [
+ "▁vague",
+ -12.222504615783691
+ ],
+ [
+ "▁Leather",
+ -12.22250747680664
+ ],
+ [
+ "▁Vintage",
+ -12.222532272338867
+ ],
+ [
+ "pointing",
+ -12.22259521484375
+ ],
+ [
+ "avant",
+ -12.22268295288086
+ ],
+ [
+ "gues",
+ -12.222949028015137
+ ],
+ [
+ "sweise",
+ -12.22302532196045
+ ],
+ [
+ "▁Greater",
+ -12.223065376281738
+ ],
+ [
+ "fig",
+ -12.22310733795166
+ ],
+ [
+ "▁Blut",
+ -12.223217964172363
+ ],
+ [
+ "▁Stellen",
+ -12.22326946258545
+ ],
+ [
+ "▁isolation",
+ -12.22337818145752
+ ],
+ [
+ "▁overhead",
+ -12.22338581085205
+ ],
+ [
+ "▁wondered",
+ -12.223508834838867
+ ],
+ [
+ "essai",
+ -12.223609924316406
+ ],
+ [
+ "aves",
+ -12.2236328125
+ ],
+ [
+ "▁Shore",
+ -12.223637580871582
+ ],
+ [
+ "▁INC",
+ -12.223709106445312
+ ],
+ [
+ "rufen",
+ -12.223980903625488
+ ],
+ [
+ "▁magnifique",
+ -12.224069595336914
+ ],
+ [
+ "▁intéressant",
+ -12.224072456359863
+ ],
+ [
+ "▁tanks",
+ -12.224075317382812
+ ],
+ [
+ "▁Tun",
+ -12.224367141723633
+ ],
+ [
+ "▁approaching",
+ -12.224390029907227
+ ],
+ [
+ "▁relay",
+ -12.224479675292969
+ ],
+ [
+ "▁Küche",
+ -12.224529266357422
+ ],
+ [
+ "describing",
+ -12.224587440490723
+ ],
+ [
+ "▁Certification",
+ -12.224588394165039
+ ],
+ [
+ "▁Breakfast",
+ -12.224597930908203
+ ],
+ [
+ "▁Frame",
+ -12.224891662597656
+ ],
+ [
+ "▁Stoff",
+ -12.224909782409668
+ ],
+ [
+ "▁victime",
+ -12.224924087524414
+ ],
+ [
+ "Observ",
+ -12.224943161010742
+ ],
+ [
+ "▁gutter",
+ -12.224989891052246
+ ],
+ [
+ "standard",
+ -12.225220680236816
+ ],
+ [
+ "▁Sci",
+ -12.225244522094727
+ ],
+ [
+ "▁sept",
+ -12.225377082824707
+ ],
+ [
+ "▁Potter",
+ -12.225423812866211
+ ],
+ [
+ "letter",
+ -12.22577953338623
+ ],
+ [
+ "▁tobacco",
+ -12.225852012634277
+ ],
+ [
+ "▁threatened",
+ -12.22591781616211
+ ],
+ [
+ "MW",
+ -12.225936889648438
+ ],
+ [
+ "▁Cher",
+ -12.225944519042969
+ ],
+ [
+ "0.1",
+ -12.225957870483398
+ ],
+ [
+ "mitted",
+ -12.22596263885498
+ ],
+ [
+ "zustellen",
+ -12.225967407226562
+ ],
+ [
+ "dominated",
+ -12.226165771484375
+ ],
+ [
+ "/16",
+ -12.22623348236084
+ ],
+ [
+ "POS",
+ -12.226317405700684
+ ],
+ [
+ "▁Zin",
+ -12.226373672485352
+ ],
+ [
+ "▁Okay",
+ -12.226381301879883
+ ],
+ [
+ "▁projected",
+ -12.226405143737793
+ ],
+ [
+ "▁selber",
+ -12.226548194885254
+ ],
+ [
+ "▁proiectului",
+ -12.2266206741333
+ ],
+ [
+ "▁Shell",
+ -12.226683616638184
+ ],
+ [
+ "▁cartridge",
+ -12.226706504821777
+ ],
+ [
+ "Message",
+ -12.2267484664917
+ ],
+ [
+ "haben",
+ -12.226799964904785
+ ],
+ [
+ "▁slides",
+ -12.226829528808594
+ ],
+ [
+ "▁gleichzeitig",
+ -12.226886749267578
+ ],
+ [
+ "▁Racing",
+ -12.227051734924316
+ ],
+ [
+ "▁20,",
+ -12.227070808410645
+ ],
+ [
+ "▁separat",
+ -12.227094650268555
+ ],
+ [
+ "▁repeatedly",
+ -12.227110862731934
+ ],
+ [
+ "▁casting",
+ -12.22728157043457
+ ],
+ [
+ "▁sacred",
+ -12.227283477783203
+ ],
+ [
+ "verfahren",
+ -12.227387428283691
+ ],
+ [
+ "▁echilibr",
+ -12.227514266967773
+ ],
+ [
+ "▁rebel",
+ -12.2277250289917
+ ],
+ [
+ "säu",
+ -12.227794647216797
+ ],
+ [
+ "ummy",
+ -12.227815628051758
+ ],
+ [
+ "▁backing",
+ -12.227889060974121
+ ],
+ [
+ "▁sponsors",
+ -12.227912902832031
+ ],
+ [
+ "▁Stress",
+ -12.22802448272705
+ ],
+ [
+ "▁Rules",
+ -12.228083610534668
+ ],
+ [
+ "▁render",
+ -12.228241920471191
+ ],
+ [
+ "▁funktioniert",
+ -12.228384971618652
+ ],
+ [
+ "▁Pearl",
+ -12.228472709655762
+ ],
+ [
+ "▁Scho",
+ -12.228527069091797
+ ],
+ [
+ "schwer",
+ -12.228595733642578
+ ],
+ [
+ "▁descoperit",
+ -12.228702545166016
+ ],
+ [
+ "holen",
+ -12.228720664978027
+ ],
+ [
+ "imposed",
+ -12.228960990905762
+ ],
+ [
+ "▁appearing",
+ -12.228968620300293
+ ],
+ [
+ "▁höher",
+ -12.229082107543945
+ ],
+ [
+ "▁Victorian",
+ -12.229111671447754
+ ],
+ [
+ "▁founding",
+ -12.229155540466309
+ ],
+ [
+ "▁Polish",
+ -12.229239463806152
+ ],
+ [
+ "▁anume",
+ -12.229248046875
+ ],
+ [
+ "Box",
+ -12.229488372802734
+ ],
+ [
+ "▁intrat",
+ -12.229598999023438
+ ],
+ [
+ "▁Inspiration",
+ -12.229610443115234
+ ],
+ [
+ "▁Canyon",
+ -12.229625701904297
+ ],
+ [
+ "▁Franklin",
+ -12.22974681854248
+ ],
+ [
+ "▁susceptible",
+ -12.22982120513916
+ ],
+ [
+ "trap",
+ -12.229839324951172
+ ],
+ [
+ "▁Roma",
+ -12.23000717163086
+ ],
+ [
+ "▁ethics",
+ -12.230009078979492
+ ],
+ [
+ "▁Privat",
+ -12.230027198791504
+ ],
+ [
+ "▁journalists",
+ -12.230090141296387
+ ],
+ [
+ "▁Universität",
+ -12.230246543884277
+ ],
+ [
+ "▁conditioner",
+ -12.230308532714844
+ ],
+ [
+ "folge",
+ -12.230327606201172
+ ],
+ [
+ "kirche",
+ -12.230416297912598
+ ],
+ [
+ "gehalten",
+ -12.230530738830566
+ ],
+ [
+ "midi",
+ -12.230570793151855
+ ],
+ [
+ "▁radar",
+ -12.230619430541992
+ ],
+ [
+ "▁Yard",
+ -12.230775833129883
+ ],
+ [
+ "▁professionnelle",
+ -12.230863571166992
+ ],
+ [
+ "▁Orchestra",
+ -12.230870246887207
+ ],
+ [
+ "▁immigrants",
+ -12.230870246887207
+ ],
+ [
+ "▁refined",
+ -12.230929374694824
+ ],
+ [
+ "▁Bishop",
+ -12.231036186218262
+ ],
+ [
+ "string",
+ -12.231095314025879
+ ],
+ [
+ "▁majoritatea",
+ -12.231231689453125
+ ],
+ [
+ "▁workflow",
+ -12.23123836517334
+ ],
+ [
+ "▁întreg",
+ -12.231306076049805
+ ],
+ [
+ "went",
+ -12.231563568115234
+ ],
+ [
+ "▁trat",
+ -12.231689453125
+ ],
+ [
+ "felul",
+ -12.23176383972168
+ ],
+ [
+ "▁hardwood",
+ -12.231821060180664
+ ],
+ [
+ "▁Task",
+ -12.231867790222168
+ ],
+ [
+ "branded",
+ -12.231921195983887
+ ],
+ [
+ "▁cinq",
+ -12.231966018676758
+ ],
+ [
+ "▁curb",
+ -12.232041358947754
+ ],
+ [
+ "▁Discount",
+ -12.232043266296387
+ ],
+ [
+ "▁Episode",
+ -12.232131958007812
+ ],
+ [
+ "▁Knowledge",
+ -12.232144355773926
+ ],
+ [
+ "▁tricky",
+ -12.232173919677734
+ ],
+ [
+ "▁characteristic",
+ -12.232233047485352
+ ],
+ [
+ "▁plata",
+ -12.23226261138916
+ ],
+ [
+ "▁Labour",
+ -12.23232650756836
+ ],
+ [
+ "▁Tha",
+ -12.232372283935547
+ ],
+ [
+ "▁Liefer",
+ -12.232430458068848
+ ],
+ [
+ "▁Reader",
+ -12.232471466064453
+ ],
+ [
+ "▁Linda",
+ -12.232521057128906
+ ],
+ [
+ "ittlerweile",
+ -12.232552528381348
+ ],
+ [
+ "defining",
+ -12.232564926147461
+ ],
+ [
+ "▁delayed",
+ -12.232635498046875
+ ],
+ [
+ "▁Bewertung",
+ -12.232674598693848
+ ],
+ [
+ "▁Unique",
+ -12.232791900634766
+ ],
+ [
+ "▁Champion",
+ -12.232866287231445
+ ],
+ [
+ "2008",
+ -12.232897758483887
+ ],
+ [
+ "▁conclu",
+ -12.232934951782227
+ ],
+ [
+ "▁câștig",
+ -12.2329740524292
+ ],
+ [
+ "▁scheduling",
+ -12.2329740524292
+ ],
+ [
+ "▁sailing",
+ -12.233116149902344
+ ],
+ [
+ "▁Storm",
+ -12.23318862915039
+ ],
+ [
+ "▁Stil",
+ -12.23320198059082
+ ],
+ [
+ "▁Album",
+ -12.233211517333984
+ ],
+ [
+ "▁ultime",
+ -12.233343124389648
+ ],
+ [
+ "url",
+ -12.233369827270508
+ ],
+ [
+ "▁terrific",
+ -12.23339557647705
+ ],
+ [
+ "▁remedy",
+ -12.233396530151367
+ ],
+ [
+ "▁Around",
+ -12.233592987060547
+ ],
+ [
+ "▁Kni",
+ -12.233756065368652
+ ],
+ [
+ "etty",
+ -12.23376750946045
+ ],
+ [
+ "Managing",
+ -12.233809471130371
+ ],
+ [
+ "▁Bedeutung",
+ -12.233816146850586
+ ],
+ [
+ "▁earthquake",
+ -12.233817100524902
+ ],
+ [
+ "▁Telefon",
+ -12.233818054199219
+ ],
+ [
+ "▁Upper",
+ -12.233869552612305
+ ],
+ [
+ "▁validation",
+ -12.233892440795898
+ ],
+ [
+ "-22",
+ -12.233997344970703
+ ],
+ [
+ "▁queue",
+ -12.23401165008545
+ ],
+ [
+ "tinde",
+ -12.234025001525879
+ ],
+ [
+ "built",
+ -12.234047889709473
+ ],
+ [
+ "▁voix",
+ -12.234125137329102
+ ],
+ [
+ "▁Resource",
+ -12.234126091003418
+ ],
+ [
+ "ţiuni",
+ -12.234143257141113
+ ],
+ [
+ "▁satisfying",
+ -12.234299659729004
+ ],
+ [
+ "▁Kohl",
+ -12.234441757202148
+ ],
+ [
+ "▁Materials",
+ -12.234618186950684
+ ],
+ [
+ "▁esp",
+ -12.234732627868652
+ ],
+ [
+ "enseignement",
+ -12.234773635864258
+ ],
+ [
+ "danach",
+ -12.234883308410645
+ ],
+ [
+ "peux",
+ -12.234932899475098
+ ],
+ [
+ "▁deployed",
+ -12.235113143920898
+ ],
+ [
+ "▁1976",
+ -12.235126495361328
+ ],
+ [
+ "ușor",
+ -12.235334396362305
+ ],
+ [
+ "élection",
+ -12.235380172729492
+ ],
+ [
+ "ettes",
+ -12.235437393188477
+ ],
+ [
+ "▁Madison",
+ -12.235506057739258
+ ],
+ [
+ "108",
+ -12.235685348510742
+ ],
+ [
+ "berger",
+ -12.235696792602539
+ ],
+ [
+ "▁pedal",
+ -12.235702514648438
+ ],
+ [
+ "▁quasi",
+ -12.235820770263672
+ ],
+ [
+ "▁lend",
+ -12.235843658447266
+ ],
+ [
+ "VER",
+ -12.235940933227539
+ ],
+ [
+ "▁chapters",
+ -12.236002922058105
+ ],
+ [
+ "▁idei",
+ -12.23600959777832
+ ],
+ [
+ "Deine",
+ -12.236034393310547
+ ],
+ [
+ "▁endure",
+ -12.236092567443848
+ ],
+ [
+ "▁Studios",
+ -12.236259460449219
+ ],
+ [
+ "structure",
+ -12.236274719238281
+ ],
+ [
+ "▁puiss",
+ -12.236370086669922
+ ],
+ [
+ "▁Morning",
+ -12.236443519592285
+ ],
+ [
+ "guide",
+ -12.236462593078613
+ ],
+ [
+ "▁Wave",
+ -12.236617088317871
+ ],
+ [
+ "▁banque",
+ -12.236879348754883
+ ],
+ [
+ "änd",
+ -12.236912727355957
+ ],
+ [
+ "oubli",
+ -12.237070083618164
+ ],
+ [
+ "▁mixer",
+ -12.237125396728516
+ ],
+ [
+ "▁remedi",
+ -12.237210273742676
+ ],
+ [
+ "▁scop",
+ -12.237421989440918
+ ],
+ [
+ "▁Rosen",
+ -12.237561225891113
+ ],
+ [
+ "▁spital",
+ -12.23773193359375
+ ],
+ [
+ "blau",
+ -12.237811088562012
+ ],
+ [
+ "▁financiar",
+ -12.237865447998047
+ ],
+ [
+ "avour",
+ -12.237871170043945
+ ],
+ [
+ "Def",
+ -12.238025665283203
+ ],
+ [
+ "▁socket",
+ -12.238076210021973
+ ],
+ [
+ "▁occurring",
+ -12.238360404968262
+ ],
+ [
+ "▁munci",
+ -12.238368034362793
+ ],
+ [
+ "▁realiza",
+ -12.238426208496094
+ ],
+ [
+ "▁beating",
+ -12.2384614944458
+ ],
+ [
+ "▁Phillip",
+ -12.238490104675293
+ ],
+ [
+ "▁courant",
+ -12.238509178161621
+ ],
+ [
+ "Auto",
+ -12.238608360290527
+ ],
+ [
+ "▁Lager",
+ -12.238685607910156
+ ],
+ [
+ "▁folos",
+ -12.238696098327637
+ ],
+ [
+ "▁moyens",
+ -12.238770484924316
+ ],
+ [
+ "▁Ec",
+ -12.238780975341797
+ ],
+ [
+ "▁Strip",
+ -12.238788604736328
+ ],
+ [
+ "sparen",
+ -12.238848686218262
+ ],
+ [
+ "▁Nintendo",
+ -12.238886833190918
+ ],
+ [
+ "▁Murphy",
+ -12.238912582397461
+ ],
+ [
+ "▁flux",
+ -12.239034652709961
+ ],
+ [
+ "▁mots",
+ -12.239034652709961
+ ],
+ [
+ "▁rechts",
+ -12.239045143127441
+ ],
+ [
+ "▁cardio",
+ -12.239142417907715
+ ],
+ [
+ "avoiding",
+ -12.239343643188477
+ ],
+ [
+ "érer",
+ -12.239453315734863
+ ],
+ [
+ "hiel",
+ -12.239461898803711
+ ],
+ [
+ "▁rezistent",
+ -12.239521980285645
+ ],
+ [
+ "close",
+ -12.23954963684082
+ ],
+ [
+ "hésitez",
+ -12.239596366882324
+ ],
+ [
+ "Hz",
+ -12.239631652832031
+ ],
+ [
+ "▁elaborate",
+ -12.239689826965332
+ ],
+ [
+ "▁permanently",
+ -12.239709854125977
+ ],
+ [
+ "▁Pittsburgh",
+ -12.239734649658203
+ ],
+ [
+ "▁counties",
+ -12.239819526672363
+ ],
+ [
+ "▁bookmark",
+ -12.239919662475586
+ ],
+ [
+ "▁Label",
+ -12.239965438842773
+ ],
+ [
+ "▁Freude",
+ -12.239974021911621
+ ],
+ [
+ "▁preferat",
+ -12.239986419677734
+ ],
+ [
+ "▁Mein",
+ -12.239995002746582
+ ],
+ [
+ "▁Crew",
+ -12.240218162536621
+ ],
+ [
+ "▁clips",
+ -12.240253448486328
+ ],
+ [
+ "8,000",
+ -12.240263938903809
+ ],
+ [
+ "▁recognise",
+ -12.240311622619629
+ ],
+ [
+ "ință",
+ -12.240365028381348
+ ],
+ [
+ "▁prieteni",
+ -12.240447044372559
+ ],
+ [
+ "Heute",
+ -12.240522384643555
+ ],
+ [
+ "ancienne",
+ -12.240534782409668
+ ],
+ [
+ "▁annoying",
+ -12.240583419799805
+ ],
+ [
+ "▁awful",
+ -12.240704536437988
+ ],
+ [
+ "▁Comments",
+ -12.240774154663086
+ ],
+ [
+ "▁musician",
+ -12.240830421447754
+ ],
+ [
+ "▁Elite",
+ -12.241023063659668
+ ],
+ [
+ "▁patri",
+ -12.241024017333984
+ ],
+ [
+ "▁Coupon",
+ -12.241037368774414
+ ],
+ [
+ "▁Farbe",
+ -12.241097450256348
+ ],
+ [
+ "▁contribui",
+ -12.241110801696777
+ ],
+ [
+ "hari",
+ -12.241294860839844
+ ],
+ [
+ "▁activitati",
+ -12.24161148071289
+ ],
+ [
+ "▁Traum",
+ -12.2416410446167
+ ],
+ [
+ "1.8",
+ -12.24170207977295
+ ],
+ [
+ "▁Healthcare",
+ -12.24172306060791
+ ],
+ [
+ "▁refresh",
+ -12.241943359375
+ ],
+ [
+ "▁Maha",
+ -12.242060661315918
+ ],
+ [
+ "▁dép",
+ -12.242082595825195
+ ],
+ [
+ "▁Studien",
+ -12.242314338684082
+ ],
+ [
+ "▁spectacol",
+ -12.242378234863281
+ ],
+ [
+ "impro",
+ -12.24254035949707
+ ],
+ [
+ "▁commentaire",
+ -12.242544174194336
+ ],
+ [
+ "ported",
+ -12.242570877075195
+ ],
+ [
+ "▁reclam",
+ -12.242612838745117
+ ],
+ [
+ "▁Verkauf",
+ -12.242634773254395
+ ],
+ [
+ "▁newspapers",
+ -12.242661476135254
+ ],
+ [
+ "▁iubit",
+ -12.242838859558105
+ ],
+ [
+ "▁Kenne",
+ -12.242844581604004
+ ],
+ [
+ "▁Consultant",
+ -12.242958068847656
+ ],
+ [
+ "▁stau",
+ -12.242986679077148
+ ],
+ [
+ "TON",
+ -12.243057250976562
+ ],
+ [
+ "▁Fehler",
+ -12.243070602416992
+ ],
+ [
+ "▁lettre",
+ -12.243167877197266
+ ],
+ [
+ "▁investigator",
+ -12.243172645568848
+ ],
+ [
+ "▁quantities",
+ -12.243184089660645
+ ],
+ [
+ "ogram",
+ -12.243208885192871
+ ],
+ [
+ "avaient",
+ -12.24323844909668
+ ],
+ [
+ "▁reducere",
+ -12.243265151977539
+ ],
+ [
+ "Lite",
+ -12.243402481079102
+ ],
+ [
+ "kurs",
+ -12.243443489074707
+ ],
+ [
+ "pré",
+ -12.24383544921875
+ ],
+ [
+ "pap",
+ -12.243898391723633
+ ],
+ [
+ "▁Männer",
+ -12.243983268737793
+ ],
+ [
+ "▁gauche",
+ -12.244022369384766
+ ],
+ [
+ "▁ähnlich",
+ -12.244027137756348
+ ],
+ [
+ "▁sunlight",
+ -12.244063377380371
+ ],
+ [
+ "▁rester",
+ -12.24422550201416
+ ],
+ [
+ "jumped",
+ -12.244586944580078
+ ],
+ [
+ "▁exclusiv",
+ -12.24463176727295
+ ],
+ [
+ "▁electoral",
+ -12.244640350341797
+ ],
+ [
+ "▁Portal",
+ -12.244650840759277
+ ],
+ [
+ "ulent",
+ -12.244688987731934
+ ],
+ [
+ "▁sonst",
+ -12.24474048614502
+ ],
+ [
+ "entraîne",
+ -12.24483585357666
+ ],
+ [
+ "▁repas",
+ -12.244837760925293
+ ],
+ [
+ "▁redus",
+ -12.244858741760254
+ ],
+ [
+ "aku",
+ -12.244866371154785
+ ],
+ [
+ "▁Graphic",
+ -12.245251655578613
+ ],
+ [
+ "▁geringe",
+ -12.24539566040039
+ ],
+ [
+ "plätze",
+ -12.245474815368652
+ ],
+ [
+ "Trebuie",
+ -12.245479583740234
+ ],
+ [
+ "▁rezultate",
+ -12.245479583740234
+ ],
+ [
+ "▁configure",
+ -12.245683670043945
+ ],
+ [
+ "▁PV",
+ -12.245834350585938
+ ],
+ [
+ "▁insect",
+ -12.246109962463379
+ ],
+ [
+ "▁Reviews",
+ -12.246129035949707
+ ],
+ [
+ "releasing",
+ -12.246186256408691
+ ],
+ [
+ "▁appliance",
+ -12.246246337890625
+ ],
+ [
+ "▁oferte",
+ -12.246482849121094
+ ],
+ [
+ "▁WILL",
+ -12.246484756469727
+ ],
+ [
+ "rion",
+ -12.246499061584473
+ ],
+ [
+ "▁Cole",
+ -12.246582984924316
+ ],
+ [
+ "▁1975",
+ -12.246650695800781
+ ],
+ [
+ "Admin",
+ -12.24677848815918
+ ],
+ [
+ "▁parade",
+ -12.246800422668457
+ ],
+ [
+ "▁mélange",
+ -12.24692153930664
+ ],
+ [
+ "▁shortage",
+ -12.247007369995117
+ ],
+ [
+ "▁Measure",
+ -12.247400283813477
+ ],
+ [
+ "anchmal",
+ -12.24742603302002
+ ],
+ [
+ "▁transfers",
+ -12.247432708740234
+ ],
+ [
+ "▁sistemului",
+ -12.247573852539062
+ ],
+ [
+ "▁deschide",
+ -12.247819900512695
+ ],
+ [
+ "▁Künstler",
+ -12.247821807861328
+ ],
+ [
+ "▁Plain",
+ -12.247848510742188
+ ],
+ [
+ "▁messaging",
+ -12.247855186462402
+ ],
+ [
+ "▁metabolism",
+ -12.247879981994629
+ ],
+ [
+ "fill",
+ -12.248031616210938
+ ],
+ [
+ "▁Bomb",
+ -12.24814224243164
+ ],
+ [
+ "usine",
+ -12.248208045959473
+ ],
+ [
+ "▁restart",
+ -12.248233795166016
+ ],
+ [
+ "▁Discussion",
+ -12.248336791992188
+ ],
+ [
+ "smith",
+ -12.248472213745117
+ ],
+ [
+ "▁Bh",
+ -12.248607635498047
+ ],
+ [
+ "▁sap",
+ -12.248689651489258
+ ],
+ [
+ "Moo",
+ -12.248714447021484
+ ],
+ [
+ "▁indirect",
+ -12.248785972595215
+ ],
+ [
+ "▁eingesetzt",
+ -12.248863220214844
+ ],
+ [
+ "▁Hip",
+ -12.248870849609375
+ ],
+ [
+ "▁iulie",
+ -12.249113082885742
+ ],
+ [
+ "▁atac",
+ -12.249201774597168
+ ],
+ [
+ "▁passport",
+ -12.2492036819458
+ ],
+ [
+ "▁Egyptian",
+ -12.249290466308594
+ ],
+ [
+ "▁soluți",
+ -12.249349594116211
+ ],
+ [
+ "▁cakes",
+ -12.249356269836426
+ ],
+ [
+ "▁Fellow",
+ -12.24949836730957
+ ],
+ [
+ "▁collision",
+ -12.249533653259277
+ ],
+ [
+ "▁abundant",
+ -12.249961853027344
+ ],
+ [
+ "▁Wonder",
+ -12.24997329711914
+ ],
+ [
+ "▁theories",
+ -12.249991416931152
+ ],
+ [
+ "landed",
+ -12.250046730041504
+ ],
+ [
+ "▁meantime",
+ -12.2500638961792
+ ],
+ [
+ "schlüsse",
+ -12.25022029876709
+ ],
+ [
+ "▁helicopter",
+ -12.25039005279541
+ ],
+ [
+ "Voici",
+ -12.250479698181152
+ ],
+ [
+ "▁Honey",
+ -12.25049877166748
+ ],
+ [
+ "▁deleted",
+ -12.250511169433594
+ ],
+ [
+ "▁Projekte",
+ -12.250523567199707
+ ],
+ [
+ "▁gasi",
+ -12.2506742477417
+ ],
+ [
+ "applique",
+ -12.25068473815918
+ ],
+ [
+ "TAL",
+ -12.250699043273926
+ ],
+ [
+ "notch",
+ -12.250699996948242
+ ],
+ [
+ "▁Response",
+ -12.250818252563477
+ ],
+ [
+ "▁deveni",
+ -12.250818252563477
+ ],
+ [
+ "▁regulate",
+ -12.250829696655273
+ ],
+ [
+ "▁vegetarian",
+ -12.25083065032959
+ ],
+ [
+ "▁Pastor",
+ -12.250880241394043
+ ],
+ [
+ "▁Strong",
+ -12.250940322875977
+ ],
+ [
+ "▁élèves",
+ -12.251055717468262
+ ],
+ [
+ "▁alimente",
+ -12.25113582611084
+ ],
+ [
+ "graphy",
+ -12.251181602478027
+ ],
+ [
+ "▁spirits",
+ -12.251266479492188
+ ],
+ [
+ "▁Cau",
+ -12.251282691955566
+ ],
+ [
+ "determin",
+ -12.251304626464844
+ ],
+ [
+ "arilor",
+ -12.251382827758789
+ ],
+ [
+ "▁masura",
+ -12.251470565795898
+ ],
+ [
+ "RAN",
+ -12.251500129699707
+ ],
+ [
+ "marked",
+ -12.251564979553223
+ ],
+ [
+ "cuba",
+ -12.251602172851562
+ ],
+ [
+ "omni",
+ -12.251609802246094
+ ],
+ [
+ "▁detox",
+ -12.251662254333496
+ ],
+ [
+ "▁quartz",
+ -12.251741409301758
+ ],
+ [
+ "▁Bug",
+ -12.25177001953125
+ ],
+ [
+ "▁Sugar",
+ -12.25185775756836
+ ],
+ [
+ "▁opponents",
+ -12.25197982788086
+ ],
+ [
+ "▁solved",
+ -12.25207805633545
+ ],
+ [
+ "semn",
+ -12.252257347106934
+ ],
+ [
+ "▁Prepare",
+ -12.252558708190918
+ ],
+ [
+ "ffel",
+ -12.252586364746094
+ ],
+ [
+ "▁Highlight",
+ -12.252608299255371
+ ],
+ [
+ "▁curent",
+ -12.252618789672852
+ ],
+ [
+ "▁praktisch",
+ -12.252626419067383
+ ],
+ [
+ "▁lending",
+ -12.252676963806152
+ ],
+ [
+ "▁minority",
+ -12.252752304077148
+ ],
+ [
+ "Free",
+ -12.252970695495605
+ ],
+ [
+ "business",
+ -12.252997398376465
+ ],
+ [
+ "▁outlook",
+ -12.253097534179688
+ ],
+ [
+ "▁assessments",
+ -12.253168106079102
+ ],
+ [
+ "▁Brother",
+ -12.253266334533691
+ ],
+ [
+ "▁partager",
+ -12.25326919555664
+ ],
+ [
+ "▁Brun",
+ -12.25329303741455
+ ],
+ [
+ "▁pedestrian",
+ -12.25339412689209
+ ],
+ [
+ "anța",
+ -12.253413200378418
+ ],
+ [
+ "▁recycled",
+ -12.253457069396973
+ ],
+ [
+ "▁quicker",
+ -12.253626823425293
+ ],
+ [
+ "▁lamps",
+ -12.253683090209961
+ ],
+ [
+ "▁nationally",
+ -12.253813743591309
+ ],
+ [
+ "▁Supplier",
+ -12.253823280334473
+ ],
+ [
+ "ograph",
+ -12.253936767578125
+ ],
+ [
+ "engage",
+ -12.253981590270996
+ ],
+ [
+ "▁Marg",
+ -12.254131317138672
+ ],
+ [
+ "▁aplicare",
+ -12.254181861877441
+ ],
+ [
+ "▁scared",
+ -12.254194259643555
+ ],
+ [
+ "▁accredited",
+ -12.254255294799805
+ ],
+ [
+ "▁outils",
+ -12.25436019897461
+ ],
+ [
+ "▁bâtiment",
+ -12.254446029663086
+ ],
+ [
+ "▁existed",
+ -12.254586219787598
+ ],
+ [
+ "gegangen",
+ -12.254619598388672
+ ],
+ [
+ "▁elevation",
+ -12.25463581085205
+ ],
+ [
+ "▁Tradition",
+ -12.254670143127441
+ ],
+ [
+ "▁Gericht",
+ -12.254677772521973
+ ],
+ [
+ "hub",
+ -12.254680633544922
+ ],
+ [
+ "strahl",
+ -12.25473690032959
+ ],
+ [
+ "build",
+ -12.254796981811523
+ ],
+ [
+ "▁Customers",
+ -12.25487232208252
+ ],
+ [
+ "klasse",
+ -12.254890441894531
+ ],
+ [
+ "▁pierre",
+ -12.254895210266113
+ ],
+ [
+ "(2)",
+ -12.255006790161133
+ ],
+ [
+ "Life",
+ -12.255125999450684
+ ],
+ [
+ "▁bachelor",
+ -12.25513744354248
+ ],
+ [
+ "▁quad",
+ -12.255195617675781
+ ],
+ [
+ "▁dispozitiv",
+ -12.25523567199707
+ ],
+ [
+ "106",
+ -12.255266189575195
+ ],
+ [
+ "▁suburb",
+ -12.255495071411133
+ ],
+ [
+ "▁1977",
+ -12.255586624145508
+ ],
+ [
+ "▁Alzheimer",
+ -12.255973815917969
+ ],
+ [
+ "▁spicy",
+ -12.255988121032715
+ ],
+ [
+ "▁spreading",
+ -12.256002426147461
+ ],
+ [
+ "nötigen",
+ -12.256078720092773
+ ],
+ [
+ "▁novels",
+ -12.256104469299316
+ ],
+ [
+ "▁responsabilité",
+ -12.256141662597656
+ ],
+ [
+ "▁Bud",
+ -12.256332397460938
+ ],
+ [
+ "▁desirable",
+ -12.256407737731934
+ ],
+ [
+ "TOR",
+ -12.256444931030273
+ ],
+ [
+ "five",
+ -12.256547927856445
+ ],
+ [
+ "▁Firmen",
+ -12.256860733032227
+ ],
+ [
+ "oeuvre",
+ -12.257075309753418
+ ],
+ [
+ "grass",
+ -12.257233619689941
+ ],
+ [
+ "▁practically",
+ -12.257277488708496
+ ],
+ [
+ "▁runners",
+ -12.257281303405762
+ ],
+ [
+ "▁mothers",
+ -12.257341384887695
+ ],
+ [
+ "Shop",
+ -12.257345199584961
+ ],
+ [
+ "▁Chicken",
+ -12.257408142089844
+ ],
+ [
+ "▁License",
+ -12.257593154907227
+ ],
+ [
+ "▁Bach",
+ -12.25765323638916
+ ],
+ [
+ "earliest",
+ -12.257729530334473
+ ],
+ [
+ "▁replica",
+ -12.25774097442627
+ ],
+ [
+ "▁haunt",
+ -12.257833480834961
+ ],
+ [
+ "▁materi",
+ -12.257854461669922
+ ],
+ [
+ "▁Finland",
+ -12.257893562316895
+ ],
+ [
+ "▁europene",
+ -12.257919311523438
+ ],
+ [
+ "abilă",
+ -12.257944107055664
+ ],
+ [
+ "cati",
+ -12.258007049560547
+ ],
+ [
+ "▁cholesterol",
+ -12.258132934570312
+ ],
+ [
+ "...).",
+ -12.258151054382324
+ ],
+ [
+ "cardi",
+ -12.25838565826416
+ ],
+ [
+ "▁(12",
+ -12.258387565612793
+ ],
+ [
+ "analyzed",
+ -12.258506774902344
+ ],
+ [
+ "▁respondents",
+ -12.258591651916504
+ ],
+ [
+ "▁höchste",
+ -12.258646011352539
+ ],
+ [
+ "▁Kern",
+ -12.258647918701172
+ ],
+ [
+ "▁knapp",
+ -12.258781433105469
+ ],
+ [
+ "▁Someone",
+ -12.258955001831055
+ ],
+ [
+ "▁équipé",
+ -12.258997917175293
+ ],
+ [
+ "credited",
+ -12.259106636047363
+ ],
+ [
+ "▁numar",
+ -12.259163856506348
+ ],
+ [
+ "▁Ace",
+ -12.259185791015625
+ ],
+ [
+ "zentrum",
+ -12.2592191696167
+ ],
+ [
+ "nehmer",
+ -12.259270668029785
+ ],
+ [
+ "arrivée",
+ -12.259282112121582
+ ],
+ [
+ "ELE",
+ -12.259291648864746
+ ],
+ [
+ "clean",
+ -12.259418487548828
+ ],
+ [
+ "Boost",
+ -12.259538650512695
+ ],
+ [
+ "call",
+ -12.259575843811035
+ ],
+ [
+ "▁Polizei",
+ -12.259659767150879
+ ],
+ [
+ "▁Januar",
+ -12.259663581848145
+ ],
+ [
+ "▁Tile",
+ -12.259681701660156
+ ],
+ [
+ "▁traduc",
+ -12.259744644165039
+ ],
+ [
+ "▁promptly",
+ -12.259773254394531
+ ],
+ [
+ "limit",
+ -12.259809494018555
+ ],
+ [
+ "▁recharge",
+ -12.2598237991333
+ ],
+ [
+ "▁wipe",
+ -12.259862899780273
+ ],
+ [
+ "▁Norway",
+ -12.26001262664795
+ ],
+ [
+ "▁Municipal",
+ -12.260077476501465
+ ],
+ [
+ "▁medieval",
+ -12.260117530822754
+ ],
+ [
+ "▁Treat",
+ -12.26021671295166
+ ],
+ [
+ "Orient",
+ -12.260283470153809
+ ],
+ [
+ "▁Stewart",
+ -12.260294914245605
+ ],
+ [
+ "▁lol",
+ -12.26039981842041
+ ],
+ [
+ "appartement",
+ -12.260522842407227
+ ],
+ [
+ "▁payer",
+ -12.260655403137207
+ ],
+ [
+ "▁splash",
+ -12.260723114013672
+ ],
+ [
+ "doubtedly",
+ -12.260726928710938
+ ],
+ [
+ "dry",
+ -12.260846138000488
+ ],
+ [
+ "▁Forex",
+ -12.260939598083496
+ ],
+ [
+ "▁Edinburgh",
+ -12.260943412780762
+ ],
+ [
+ "▁Traditional",
+ -12.261032104492188
+ ],
+ [
+ "▁1968",
+ -12.261134147644043
+ ],
+ [
+ "▁glow",
+ -12.261248588562012
+ ],
+ [
+ "Alternatively",
+ -12.261265754699707
+ ],
+ [
+ "▁partly",
+ -12.261354446411133
+ ],
+ [
+ "égi",
+ -12.261401176452637
+ ],
+ [
+ "▁Prices",
+ -12.261640548706055
+ ],
+ [
+ "haupt",
+ -12.261651992797852
+ ],
+ [
+ "▁sentences",
+ -12.261711120605469
+ ],
+ [
+ "ouvre",
+ -12.261735916137695
+ ],
+ [
+ "▁Liter",
+ -12.261746406555176
+ ],
+ [
+ "▁Important",
+ -12.2620267868042
+ ],
+ [
+ "▁Collins",
+ -12.262077331542969
+ ],
+ [
+ "▁reproduce",
+ -12.262106895446777
+ ],
+ [
+ "▁selten",
+ -12.262124061584473
+ ],
+ [
+ "▁Mitte",
+ -12.262170791625977
+ ],
+ [
+ "OA",
+ -12.262174606323242
+ ],
+ [
+ "▁Sister",
+ -12.262358665466309
+ ],
+ [
+ "▁responding",
+ -12.262385368347168
+ ],
+ [
+ "▁ballot",
+ -12.262455940246582
+ ],
+ [
+ "▁Nutrition",
+ -12.262460708618164
+ ],
+ [
+ "occurrence",
+ -12.26246452331543
+ ],
+ [
+ "Atunci",
+ -12.262604713439941
+ ],
+ [
+ "▁hockey",
+ -12.262680053710938
+ ],
+ [
+ "▁undertaking",
+ -12.262697219848633
+ ],
+ [
+ "▁educators",
+ -12.262885093688965
+ ],
+ [
+ "▁Swedish",
+ -12.262893676757812
+ ],
+ [
+ "▁Recovery",
+ -12.262894630432129
+ ],
+ [
+ "▁circum",
+ -12.262910842895508
+ ],
+ [
+ "▁chains",
+ -12.263084411621094
+ ],
+ [
+ "▁genug",
+ -12.263113021850586
+ ],
+ [
+ "▁Pil",
+ -12.263227462768555
+ ],
+ [
+ "▁farms",
+ -12.263265609741211
+ ],
+ [
+ "▁simplicity",
+ -12.263336181640625
+ ],
+ [
+ "-21",
+ -12.263399124145508
+ ],
+ [
+ "▁partition",
+ -12.263493537902832
+ ],
+ [
+ "▁Relations",
+ -12.26360034942627
+ ],
+ [
+ "zentrale",
+ -12.263794898986816
+ ],
+ [
+ "lapse",
+ -12.263855934143066
+ ],
+ [
+ "▁toast",
+ -12.263862609863281
+ ],
+ [
+ "▁citi",
+ -12.263946533203125
+ ],
+ [
+ "▁longtemps",
+ -12.263984680175781
+ ],
+ [
+ "maj",
+ -12.264448165893555
+ ],
+ [
+ "▁Cin",
+ -12.264483451843262
+ ],
+ [
+ "zeichen",
+ -12.264504432678223
+ ],
+ [
+ "▁Zoo",
+ -12.264567375183105
+ ],
+ [
+ "▁frisch",
+ -12.264570236206055
+ ],
+ [
+ "▁permettra",
+ -12.264595031738281
+ ],
+ [
+ "▁Liberty",
+ -12.264642715454102
+ ],
+ [
+ "▁playground",
+ -12.264873504638672
+ ],
+ [
+ "▁Mate",
+ -12.265031814575195
+ ],
+ [
+ "▁evolving",
+ -12.265066146850586
+ ],
+ [
+ "national",
+ -12.265207290649414
+ ],
+ [
+ "▁signifie",
+ -12.265279769897461
+ ],
+ [
+ "▁Related",
+ -12.265292167663574
+ ],
+ [
+ "NES",
+ -12.265337944030762
+ ],
+ [
+ "euil",
+ -12.265473365783691
+ ],
+ [
+ "▁struggles",
+ -12.265542030334473
+ ],
+ [
+ "▁instinct",
+ -12.265628814697266
+ ],
+ [
+ "arbre",
+ -12.26608943939209
+ ],
+ [
+ "▁commands",
+ -12.266222953796387
+ ],
+ [
+ "▁frumoase",
+ -12.26637077331543
+ ],
+ [
+ "▁watches",
+ -12.266779899597168
+ ],
+ [
+ "NM",
+ -12.266804695129395
+ ],
+ [
+ "▁influential",
+ -12.266807556152344
+ ],
+ [
+ "▁gewesen",
+ -12.266901969909668
+ ],
+ [
+ "▁Pictures",
+ -12.267224311828613
+ ],
+ [
+ "▁HVAC",
+ -12.267242431640625
+ ],
+ [
+ "▁skate",
+ -12.26732063293457
+ ],
+ [
+ "▁Robot",
+ -12.267327308654785
+ ],
+ [
+ "▁Boys",
+ -12.267404556274414
+ ],
+ [
+ "▁Mutter",
+ -12.267425537109375
+ ],
+ [
+ "▁marques",
+ -12.267539024353027
+ ],
+ [
+ "utiliser",
+ -12.267793655395508
+ ],
+ [
+ "▁amazed",
+ -12.267799377441406
+ ],
+ [
+ "ächtig",
+ -12.26783275604248
+ ],
+ [
+ "▁Success",
+ -12.267870903015137
+ ],
+ [
+ "gramm",
+ -12.267956733703613
+ ],
+ [
+ "▁1972",
+ -12.267956733703613
+ ],
+ [
+ "▁marina",
+ -12.268269538879395
+ ],
+ [
+ "▁lou",
+ -12.268321990966797
+ ],
+ [
+ "▁précis",
+ -12.268380165100098
+ ],
+ [
+ "ographic",
+ -12.268482208251953
+ ],
+ [
+ "people",
+ -12.26848316192627
+ ],
+ [
+ "fahr",
+ -12.268547058105469
+ ],
+ [
+ "▁Contemporary",
+ -12.268550872802734
+ ],
+ [
+ "▁frustrating",
+ -12.26858139038086
+ ],
+ [
+ "chide",
+ -12.268704414367676
+ ],
+ [
+ "1.5",
+ -12.268807411193848
+ ],
+ [
+ "▁ankle",
+ -12.268850326538086
+ ],
+ [
+ "▁proximity",
+ -12.268986701965332
+ ],
+ [
+ "▁Leute",
+ -12.269006729125977
+ ],
+ [
+ "UA",
+ -12.269031524658203
+ ],
+ [
+ "union",
+ -12.269131660461426
+ ],
+ [
+ "▁recovered",
+ -12.269133567810059
+ ],
+ [
+ "▁sword",
+ -12.269216537475586
+ ],
+ [
+ "▁Mut",
+ -12.26923942565918
+ ],
+ [
+ "▁Rin",
+ -12.269360542297363
+ ],
+ [
+ "▁lectures",
+ -12.26942253112793
+ ],
+ [
+ "▁licensing",
+ -12.269423484802246
+ ],
+ [
+ "MAC",
+ -12.269498825073242
+ ],
+ [
+ "▁commute",
+ -12.269776344299316
+ ],
+ [
+ "Acesta",
+ -12.269858360290527
+ ],
+ [
+ "▁Koch",
+ -12.270088195800781
+ ],
+ [
+ "▁depozit",
+ -12.270119667053223
+ ],
+ [
+ "▁erstmal",
+ -12.270163536071777
+ ],
+ [
+ "arhi",
+ -12.270271301269531
+ ],
+ [
+ "▁Normal",
+ -12.270462036132812
+ ],
+ [
+ "EZ",
+ -12.270464897155762
+ ],
+ [
+ "ărilor",
+ -12.270986557006836
+ ],
+ [
+ "▁favoris",
+ -12.271041870117188
+ ],
+ [
+ "▁$9",
+ -12.271050453186035
+ ],
+ [
+ "▁Lawrence",
+ -12.271172523498535
+ ],
+ [
+ "▁fixing",
+ -12.271200180053711
+ ],
+ [
+ "▁researching",
+ -12.271288871765137
+ ],
+ [
+ "▁Pant",
+ -12.271467208862305
+ ],
+ [
+ "▁candid",
+ -12.271490097045898
+ ],
+ [
+ "▁Arkansas",
+ -12.27160930633545
+ ],
+ [
+ "▁bitcoin",
+ -12.271612167358398
+ ],
+ [
+ "ва",
+ -12.271645545959473
+ ],
+ [
+ "▁Finger",
+ -12.271692276000977
+ ],
+ [
+ "▁SRL",
+ -12.271718978881836
+ ],
+ [
+ "Arg",
+ -12.271797180175781
+ ],
+ [
+ "trade",
+ -12.271903991699219
+ ],
+ [
+ "▁extraction",
+ -12.271941184997559
+ ],
+ [
+ "▁footprint",
+ -12.2720308303833
+ ],
+ [
+ "▁folosite",
+ -12.272085189819336
+ ],
+ [
+ "▁Flex",
+ -12.272184371948242
+ ],
+ [
+ "▁dys",
+ -12.272294998168945
+ ],
+ [
+ "▁Wright",
+ -12.272343635559082
+ ],
+ [
+ "▁multitude",
+ -12.272378921508789
+ ],
+ [
+ "▁Chu",
+ -12.272494316101074
+ ],
+ [
+ "▁Jerry",
+ -12.27249526977539
+ ],
+ [
+ "▁notebook",
+ -12.272722244262695
+ ],
+ [
+ "▁SIM",
+ -12.272932052612305
+ ],
+ [
+ "dietary",
+ -12.272963523864746
+ ],
+ [
+ "▁polished",
+ -12.272984504699707
+ ],
+ [
+ "▁carriers",
+ -12.272993087768555
+ ],
+ [
+ "▁cardiac",
+ -12.27299976348877
+ ],
+ [
+ "▁burned",
+ -12.273038864135742
+ ],
+ [
+ "▁sealed",
+ -12.273062705993652
+ ],
+ [
+ "▁pumps",
+ -12.273224830627441
+ ],
+ [
+ "▁consumed",
+ -12.273233413696289
+ ],
+ [
+ "▁Teaching",
+ -12.273446083068848
+ ],
+ [
+ "▁daughters",
+ -12.27348518371582
+ ],
+ [
+ "serviciile",
+ -12.273600578308105
+ ],
+ [
+ "▁Teams",
+ -12.273690223693848
+ ],
+ [
+ "▁avoided",
+ -12.273903846740723
+ ],
+ [
+ "▁compagnie",
+ -12.274019241333008
+ ],
+ [
+ "▁mașin",
+ -12.274024963378906
+ ],
+ [
+ "▁Sean",
+ -12.27418041229248
+ ],
+ [
+ "▁arunc",
+ -12.274208068847656
+ ],
+ [
+ "kräfte",
+ -12.274238586425781
+ ],
+ [
+ "vani",
+ -12.274255752563477
+ ],
+ [
+ "Metall",
+ -12.27437973022461
+ ],
+ [
+ "2009",
+ -12.274449348449707
+ ],
+ [
+ "moi",
+ -12.274688720703125
+ ],
+ [
+ "▁THAT",
+ -12.274700164794922
+ ],
+ [
+ "▁Ny",
+ -12.274809837341309
+ ],
+ [
+ "▁countertops",
+ -12.274860382080078
+ ],
+ [
+ "Pod",
+ -12.274938583374023
+ ],
+ [
+ "amente",
+ -12.274943351745605
+ ],
+ [
+ "▁offshore",
+ -12.275001525878906
+ ],
+ [
+ "luti",
+ -12.275087356567383
+ ],
+ [
+ "parked",
+ -12.275160789489746
+ ],
+ [
+ "ajout",
+ -12.275247573852539
+ ],
+ [
+ "Shirt",
+ -12.275328636169434
+ ],
+ [
+ "▁3/4",
+ -12.275389671325684
+ ],
+ [
+ "▁gratuite",
+ -12.27543830871582
+ ],
+ [
+ "mètres",
+ -12.27557373046875
+ ],
+ [
+ "▁Wish",
+ -12.2755765914917
+ ],
+ [
+ "▁holistic",
+ -12.27558422088623
+ ],
+ [
+ "gren",
+ -12.275607109069824
+ ],
+ [
+ "compiled",
+ -12.275660514831543
+ ],
+ [
+ "▁innocent",
+ -12.275779724121094
+ ],
+ [
+ "▁sorte",
+ -12.275787353515625
+ ],
+ [
+ "▁insulin",
+ -12.275792121887207
+ ],
+ [
+ "▁Academic",
+ -12.275996208190918
+ ],
+ [
+ "▁acrylic",
+ -12.27600383758545
+ ],
+ [
+ "▁hinzu",
+ -12.27616024017334
+ ],
+ [
+ "▁compression",
+ -12.27619457244873
+ ],
+ [
+ "▁viral",
+ -12.276220321655273
+ ],
+ [
+ "▁stereo",
+ -12.2764892578125
+ ],
+ [
+ "▁Concept",
+ -12.276542663574219
+ ],
+ [
+ "▁Margaret",
+ -12.276659965515137
+ ],
+ [
+ "▁consolidation",
+ -12.276875495910645
+ ],
+ [
+ "Figure",
+ -12.277058601379395
+ ],
+ [
+ "zzo",
+ -12.277061462402344
+ ],
+ [
+ "▁Egg",
+ -12.277098655700684
+ ],
+ [
+ "weiterhin",
+ -12.277213096618652
+ ],
+ [
+ "▁Vista",
+ -12.277252197265625
+ ],
+ [
+ "▁necessity",
+ -12.277316093444824
+ ],
+ [
+ "▁kayak",
+ -12.277490615844727
+ ],
+ [
+ "▁consensus",
+ -12.277535438537598
+ ],
+ [
+ "▁Katz",
+ -12.277602195739746
+ ],
+ [
+ "▁Warren",
+ -12.277640342712402
+ ],
+ [
+ "▁custody",
+ -12.277755737304688
+ ],
+ [
+ "++",
+ -12.277759552001953
+ ],
+ [
+ "▁paiement",
+ -12.277782440185547
+ ],
+ [
+ "▁foul",
+ -12.277878761291504
+ ],
+ [
+ "Chaque",
+ -12.277934074401855
+ ],
+ [
+ "▁Syrian",
+ -12.277998924255371
+ ],
+ [
+ "▁photographers",
+ -12.278056144714355
+ ],
+ [
+ "▁dismiss",
+ -12.278270721435547
+ ],
+ [
+ "▁Gaz",
+ -12.278526306152344
+ ],
+ [
+ "▁développer",
+ -12.278529167175293
+ ],
+ [
+ "▁Dakota",
+ -12.27863883972168
+ ],
+ [
+ "▁cardiovascular",
+ -12.278642654418945
+ ],
+ [
+ "▁tattoo",
+ -12.278858184814453
+ ],
+ [
+ "▁Lighting",
+ -12.278918266296387
+ ],
+ [
+ "▁nowhere",
+ -12.278940200805664
+ ],
+ [
+ "vada",
+ -12.27895450592041
+ ],
+ [
+ "▁Favor",
+ -12.279084205627441
+ ],
+ [
+ "ruled",
+ -12.2791748046875
+ ],
+ [
+ "▁Dating",
+ -12.2793550491333
+ ],
+ [
+ "gain",
+ -12.279963493347168
+ ],
+ [
+ "rism",
+ -12.28016471862793
+ ],
+ [
+ "coloured",
+ -12.280169486999512
+ ],
+ [
+ "▁refugees",
+ -12.280184745788574
+ ],
+ [
+ "▁Schm",
+ -12.2803955078125
+ ],
+ [
+ "▁happily",
+ -12.280402183532715
+ ],
+ [
+ "▁specification",
+ -12.280607223510742
+ ],
+ [
+ "WM",
+ -12.280736923217773
+ ],
+ [
+ "▁intro",
+ -12.280823707580566
+ ],
+ [
+ "rack",
+ -12.28097915649414
+ ],
+ [
+ "characterized",
+ -12.28107738494873
+ ],
+ [
+ "▁externe",
+ -12.281136512756348
+ ],
+ [
+ "▁arrives",
+ -12.28114128112793
+ ],
+ [
+ "WO",
+ -12.281181335449219
+ ],
+ [
+ "bericht",
+ -12.281233787536621
+ ],
+ [
+ "▁delays",
+ -12.281242370605469
+ ],
+ [
+ "▁Flight",
+ -12.281256675720215
+ ],
+ [
+ "1-3",
+ -12.281524658203125
+ ],
+ [
+ "▁Singh",
+ -12.281548500061035
+ ],
+ [
+ "▁shifting",
+ -12.281651496887207
+ ],
+ [
+ "▁dashboard",
+ -12.281729698181152
+ ],
+ [
+ "▁lieux",
+ -12.281781196594238
+ ],
+ [
+ "▁validate",
+ -12.281901359558105
+ ],
+ [
+ "▁uniquement",
+ -12.281963348388672
+ ],
+ [
+ "clip",
+ -12.28199291229248
+ ],
+ [
+ "cov",
+ -12.282132148742676
+ ],
+ [
+ "▁tendance",
+ -12.282215118408203
+ ],
+ [
+ "èle",
+ -12.282258033752441
+ ],
+ [
+ "▁incepe",
+ -12.282261848449707
+ ],
+ [
+ "▁chunk",
+ -12.282585144042969
+ ],
+ [
+ "▁Nr",
+ -12.28266716003418
+ ],
+ [
+ "▁Montana",
+ -12.282674789428711
+ ],
+ [
+ "▁sticks",
+ -12.28277587890625
+ ],
+ [
+ "▁caps",
+ -12.28309154510498
+ ],
+ [
+ "▁Jimmy",
+ -12.283167839050293
+ ],
+ [
+ "▁Levi",
+ -12.283285140991211
+ ],
+ [
+ "▁cables",
+ -12.28345012664795
+ ],
+ [
+ "▁SB",
+ -12.283550262451172
+ ],
+ [
+ "▁thème",
+ -12.2836275100708
+ ],
+ [
+ "ADA",
+ -12.283672332763672
+ ],
+ [
+ "▁garant",
+ -12.283686637878418
+ ],
+ [
+ "▁Joint",
+ -12.283820152282715
+ ],
+ [
+ "▁partage",
+ -12.28398323059082
+ ],
+ [
+ "schreib",
+ -12.284119606018066
+ ],
+ [
+ "ether",
+ -12.28420352935791
+ ],
+ [
+ "▁Klima",
+ -12.284303665161133
+ ],
+ [
+ "▁medicines",
+ -12.284317016601562
+ ],
+ [
+ "▁pH",
+ -12.284320831298828
+ ],
+ [
+ "Architect",
+ -12.284378051757812
+ ],
+ [
+ "știi",
+ -12.284396171569824
+ ],
+ [
+ "▁retrouve",
+ -12.284700393676758
+ ],
+ [
+ "▁posture",
+ -12.284753799438477
+ ],
+ [
+ "Feature",
+ -12.284773826599121
+ ],
+ [
+ "▁drying",
+ -12.284884452819824
+ ],
+ [
+ "trifft",
+ -12.28488826751709
+ ],
+ [
+ "ibi",
+ -12.285079002380371
+ ],
+ [
+ "▁rezerv",
+ -12.285116195678711
+ ],
+ [
+ "▁Vă",
+ -12.28518009185791
+ ],
+ [
+ "▁Speaker",
+ -12.285282135009766
+ ],
+ [
+ "▁illustration",
+ -12.285319328308105
+ ],
+ [
+ "oooo",
+ -12.285419464111328
+ ],
+ [
+ "▁initiated",
+ -12.285518646240234
+ ],
+ [
+ "PK",
+ -12.285545349121094
+ ],
+ [
+ "▁algorithms",
+ -12.285630226135254
+ ],
+ [
+ "▁zice",
+ -12.285757064819336
+ ],
+ [
+ "WI",
+ -12.28581428527832
+ ],
+ [
+ "urgence",
+ -12.285823822021484
+ ],
+ [
+ "▁bloggers",
+ -12.285887718200684
+ ],
+ [
+ "▁realitate",
+ -12.285894393920898
+ ],
+ [
+ "eks",
+ -12.28598690032959
+ ],
+ [
+ "▁cushions",
+ -12.286149024963379
+ ],
+ [
+ "▁Kri",
+ -12.286224365234375
+ ],
+ [
+ "▁réalisation",
+ -12.286396026611328
+ ],
+ [
+ "▁Photoshop",
+ -12.286407470703125
+ ],
+ [
+ "cret",
+ -12.286462783813477
+ ],
+ [
+ "faire",
+ -12.286613464355469
+ ],
+ [
+ "▁Cei",
+ -12.286782264709473
+ ],
+ [
+ "ICO",
+ -12.286789894104004
+ ],
+ [
+ "Contin",
+ -12.28681755065918
+ ],
+ [
+ "▁Builder",
+ -12.286916732788086
+ ],
+ [
+ "look",
+ -12.28698444366455
+ ],
+ [
+ "▁tenants",
+ -12.287023544311523
+ ],
+ [
+ "▁gloves",
+ -12.287113189697266
+ ],
+ [
+ "Day",
+ -12.287169456481934
+ ],
+ [
+ "firmly",
+ -12.28725814819336
+ ],
+ [
+ "CIA",
+ -12.287352561950684
+ ],
+ [
+ "▁TVA",
+ -12.28741455078125
+ ],
+ [
+ "▁notifications",
+ -12.287446975708008
+ ],
+ [
+ "▁Higher",
+ -12.287459373474121
+ ],
+ [
+ "▁Weihnachts",
+ -12.287491798400879
+ ],
+ [
+ "▁blur",
+ -12.287755012512207
+ ],
+ [
+ "ов",
+ -12.288087844848633
+ ],
+ [
+ "feder",
+ -12.288159370422363
+ ],
+ [
+ "▁explosion",
+ -12.288171768188477
+ ],
+ [
+ "▁Fenster",
+ -12.288189888000488
+ ],
+ [
+ "▁junge",
+ -12.288225173950195
+ ],
+ [
+ "▁Highland",
+ -12.288230895996094
+ ],
+ [
+ "▁Lü",
+ -12.288290023803711
+ ],
+ [
+ "▁Alba",
+ -12.28832721710205
+ ],
+ [
+ "▁Dort",
+ -12.288338661193848
+ ],
+ [
+ "▁recruiting",
+ -12.28835391998291
+ ],
+ [
+ "▁Multiple",
+ -12.288549423217773
+ ],
+ [
+ "▁animated",
+ -12.288604736328125
+ ],
+ [
+ "▁Virgin",
+ -12.288637161254883
+ ],
+ [
+ "1000",
+ -12.288676261901855
+ ],
+ [
+ "▁resin",
+ -12.288700103759766
+ ],
+ [
+ "▁matrix",
+ -12.288826942443848
+ ],
+ [
+ "irri",
+ -12.289011001586914
+ ],
+ [
+ "▁chiffre",
+ -12.28904914855957
+ ],
+ [
+ "▁Corps",
+ -12.289252281188965
+ ],
+ [
+ "▁advocacy",
+ -12.28927230834961
+ ],
+ [
+ "▁pozitiv",
+ -12.289274215698242
+ ],
+ [
+ "▁pouss",
+ -12.289451599121094
+ ],
+ [
+ "événement",
+ -12.28950309753418
+ ],
+ [
+ "▁pielii",
+ -12.289717674255371
+ ],
+ [
+ "onnais",
+ -12.289750099182129
+ ],
+ [
+ "▁Statement",
+ -12.289754867553711
+ ],
+ [
+ "crimin",
+ -12.289868354797363
+ ],
+ [
+ "hidrat",
+ -12.289942741394043
+ ],
+ [
+ "▁Jugendliche",
+ -12.290057182312012
+ ],
+ [
+ "TRI",
+ -12.290223121643066
+ ],
+ [
+ "erra",
+ -12.290240287780762
+ ],
+ [
+ "chat",
+ -12.290321350097656
+ ],
+ [
+ "▁traits",
+ -12.290359497070312
+ ],
+ [
+ "▁incentives",
+ -12.29038143157959
+ ],
+ [
+ "▁accelerate",
+ -12.290568351745605
+ ],
+ [
+ "woven",
+ -12.290633201599121
+ ],
+ [
+ "UST",
+ -12.290688514709473
+ ],
+ [
+ "▁premiers",
+ -12.290717124938965
+ ],
+ [
+ "▁Ferien",
+ -12.290755271911621
+ ],
+ [
+ "▁mariage",
+ -12.290796279907227
+ ],
+ [
+ "▁financially",
+ -12.290801048278809
+ ],
+ [
+ "gesellschaft",
+ -12.290863037109375
+ ],
+ [
+ "▁situaţi",
+ -12.290865898132324
+ ],
+ [
+ "▁quoted",
+ -12.291373252868652
+ ],
+ [
+ "▁periodic",
+ -12.291421890258789
+ ],
+ [
+ "▁chaos",
+ -12.291543960571289
+ ],
+ [
+ "▁remodel",
+ -12.29159927368164
+ ],
+ [
+ "▁Contractor",
+ -12.291641235351562
+ ],
+ [
+ "▁recuper",
+ -12.291729927062988
+ ],
+ [
+ "▁driveway",
+ -12.291755676269531
+ ],
+ [
+ "▁entertain",
+ -12.291765213012695
+ ],
+ [
+ "▁condus",
+ -12.291769027709961
+ ],
+ [
+ "▁chefs",
+ -12.29184341430664
+ ],
+ [
+ "pak",
+ -12.291866302490234
+ ],
+ [
+ "▁possède",
+ -12.291948318481445
+ ],
+ [
+ "▁outreach",
+ -12.291984558105469
+ ],
+ [
+ "▁navig",
+ -12.292036056518555
+ ],
+ [
+ "▁renewal",
+ -12.292071342468262
+ ],
+ [
+ "▁Rice",
+ -12.292309761047363
+ ],
+ [
+ "▁Czech",
+ -12.292398452758789
+ ],
+ [
+ "▁entstehen",
+ -12.292445182800293
+ ],
+ [
+ "▁droite",
+ -12.292448997497559
+ ],
+ [
+ "▁Investor",
+ -12.292497634887695
+ ],
+ [
+ "▁Soci",
+ -12.29250431060791
+ ],
+ [
+ "▁scalp",
+ -12.292622566223145
+ ],
+ [
+ "▁politiques",
+ -12.292815208435059
+ ],
+ [
+ "▁plaintiff",
+ -12.292841911315918
+ ],
+ [
+ "extending",
+ -12.29287052154541
+ ],
+ [
+ "▁paperwork",
+ -12.29300594329834
+ ],
+ [
+ "vizi",
+ -12.293142318725586
+ ],
+ [
+ "assisting",
+ -12.29317569732666
+ ],
+ [
+ "local",
+ -12.293272972106934
+ ],
+ [
+ "▁Wear",
+ -12.293323516845703
+ ],
+ [
+ "▁descend",
+ -12.293340682983398
+ ],
+ [
+ "▁Wikipedia",
+ -12.293513298034668
+ ],
+ [
+ "▁Consiliului",
+ -12.293516159057617
+ ],
+ [
+ "▁Nokia",
+ -12.293540000915527
+ ],
+ [
+ "▁facult",
+ -12.293560028076172
+ ],
+ [
+ "▁altogether",
+ -12.293851852416992
+ ],
+ [
+ "▁rankings",
+ -12.29391860961914
+ ],
+ [
+ "▁downloading",
+ -12.293953895568848
+ ],
+ [
+ "QU",
+ -12.294007301330566
+ ],
+ [
+ "▁Olive",
+ -12.294041633605957
+ ],
+ [
+ "▁backdrop",
+ -12.294110298156738
+ ],
+ [
+ "▁recomandat",
+ -12.294116020202637
+ ],
+ [
+ "▁Faculty",
+ -12.294184684753418
+ ],
+ [
+ "ANS",
+ -12.294220924377441
+ ],
+ [
+ "▁fracture",
+ -12.294225692749023
+ ],
+ [
+ "job",
+ -12.29448127746582
+ ],
+ [
+ "▁anticipate",
+ -12.294525146484375
+ ],
+ [
+ "▁drift",
+ -12.294543266296387
+ ],
+ [
+ "▁Marco",
+ -12.294632911682129
+ ],
+ [
+ "▁witnessed",
+ -12.294700622558594
+ ],
+ [
+ "▁comprend",
+ -12.294974327087402
+ ],
+ [
+ "▁bulb",
+ -12.29504680633545
+ ],
+ [
+ "▁shallow",
+ -12.295059204101562
+ ],
+ [
+ "stärke",
+ -12.295063972473145
+ ],
+ [
+ "▁Jessica",
+ -12.295080184936523
+ ],
+ [
+ "▁démarche",
+ -12.29508113861084
+ ],
+ [
+ "▁traditionally",
+ -12.29508113861084
+ ],
+ [
+ "Deputy",
+ -12.295093536376953
+ ],
+ [
+ "▁rivers",
+ -12.295260429382324
+ ],
+ [
+ "▁livraison",
+ -12.29531192779541
+ ],
+ [
+ "▁lacking",
+ -12.295421600341797
+ ],
+ [
+ "▁remodeling",
+ -12.295426368713379
+ ],
+ [
+ "▁acesteia",
+ -12.295514106750488
+ ],
+ [
+ "▁grosse",
+ -12.295669555664062
+ ],
+ [
+ "▁propus",
+ -12.295833587646484
+ ],
+ [
+ "lessly",
+ -12.29587459564209
+ ],
+ [
+ "▁Kredit",
+ -12.295931816101074
+ ],
+ [
+ "reputable",
+ -12.295981407165527
+ ],
+ [
+ "▁Sell",
+ -12.2960205078125
+ ],
+ [
+ "▁Crime",
+ -12.296111106872559
+ ],
+ [
+ "Ent",
+ -12.296310424804688
+ ],
+ [
+ "finity",
+ -12.296422004699707
+ ],
+ [
+ "▁Complex",
+ -12.296500205993652
+ ],
+ [
+ "easing",
+ -12.296638488769531
+ ],
+ [
+ "dynamic",
+ -12.296670913696289
+ ],
+ [
+ "▁eaten",
+ -12.296727180480957
+ ],
+ [
+ "gezogen",
+ -12.296734809875488
+ ],
+ [
+ "▁2004,",
+ -12.296774864196777
+ ],
+ [
+ "▁Muslims",
+ -12.296822547912598
+ ],
+ [
+ "▁Sprache",
+ -12.296883583068848
+ ],
+ [
+ "▁Truth",
+ -12.296927452087402
+ ],
+ [
+ "▁guarantees",
+ -12.296928405761719
+ ],
+ [
+ "/5",
+ -12.29712963104248
+ ],
+ [
+ "”).",
+ -12.297135353088379
+ ],
+ [
+ "▁Medium",
+ -12.2972993850708
+ ],
+ [
+ "▁décidé",
+ -12.297445297241211
+ ],
+ [
+ "▁balcony",
+ -12.29747200012207
+ ],
+ [
+ "leuchte",
+ -12.297502517700195
+ ],
+ [
+ "hik",
+ -12.297849655151367
+ ],
+ [
+ "▁Agriculture",
+ -12.298221588134766
+ ],
+ [
+ "▁securities",
+ -12.298221588134766
+ ],
+ [
+ "Probably",
+ -12.298224449157715
+ ],
+ [
+ "▁macar",
+ -12.29824161529541
+ ],
+ [
+ "▁Signal",
+ -12.298399925231934
+ ],
+ [
+ "lake",
+ -12.298677444458008
+ ],
+ [
+ "▁compétences",
+ -12.298726081848145
+ ],
+ [
+ "▁proprietary",
+ -12.298812866210938
+ ],
+ [
+ "allons",
+ -12.298850059509277
+ ],
+ [
+ "▁belongs",
+ -12.298916816711426
+ ],
+ [
+ "▁missile",
+ -12.298958778381348
+ ],
+ [
+ "țiune",
+ -12.298999786376953
+ ],
+ [
+ "▁Integration",
+ -12.299116134643555
+ ],
+ [
+ "▁testimony",
+ -12.299120903015137
+ ],
+ [
+ "▁wesentlich",
+ -12.299142837524414
+ ],
+ [
+ "▁donors",
+ -12.299152374267578
+ ],
+ [
+ "▁pivot",
+ -12.299202919006348
+ ],
+ [
+ "▁Uber",
+ -12.299219131469727
+ ],
+ [
+ "▁databases",
+ -12.299281120300293
+ ],
+ [
+ "▁studi",
+ -12.299317359924316
+ ],
+ [
+ "totdeauna",
+ -12.299351692199707
+ ],
+ [
+ "▁briefly",
+ -12.299449920654297
+ ],
+ [
+ "▁livr",
+ -12.29952335357666
+ ],
+ [
+ "▁CRM",
+ -12.299581527709961
+ ],
+ [
+ "gone",
+ -12.299697875976562
+ ],
+ [
+ "10)",
+ -12.299761772155762
+ ],
+ [
+ "▁zilele",
+ -12.299920082092285
+ ],
+ [
+ "Basically",
+ -12.300008773803711
+ ],
+ [
+ "▁medie",
+ -12.300041198730469
+ ],
+ [
+ "spotted",
+ -12.30006217956543
+ ],
+ [
+ "▁troubles",
+ -12.30009937286377
+ ],
+ [
+ "▁acknowledged",
+ -12.300176620483398
+ ],
+ [
+ "350",
+ -12.300185203552246
+ ],
+ [
+ "LB",
+ -12.300273895263672
+ ],
+ [
+ "Phy",
+ -12.30038833618164
+ ],
+ [
+ "natal",
+ -12.300397872924805
+ ],
+ [
+ "illé",
+ -12.300445556640625
+ ],
+ [
+ "bilder",
+ -12.300625801086426
+ ],
+ [
+ "▁apples",
+ -12.300636291503906
+ ],
+ [
+ "graphical",
+ -12.300889015197754
+ ],
+ [
+ "organiser",
+ -12.301024436950684
+ ],
+ [
+ "▁ochii",
+ -12.301040649414062
+ ],
+ [
+ "glas",
+ -12.301178932189941
+ ],
+ [
+ "CAP",
+ -12.301180839538574
+ ],
+ [
+ "▁Doors",
+ -12.301331520080566
+ ],
+ [
+ "▁Eis",
+ -12.30156135559082
+ ],
+ [
+ "tipuri",
+ -12.301590919494629
+ ],
+ [
+ "▁Worth",
+ -12.301684379577637
+ ],
+ [
+ "izează",
+ -12.301719665527344
+ ],
+ [
+ "nunț",
+ -12.30180549621582
+ ],
+ [
+ "▁Trip",
+ -12.30186653137207
+ ],
+ [
+ "ISS",
+ -12.301976203918457
+ ],
+ [
+ "efficient",
+ -12.30201530456543
+ ],
+ [
+ "Luckily",
+ -12.302099227905273
+ ],
+ [
+ "▁vase",
+ -12.302133560180664
+ ],
+ [
+ "▁gay",
+ -12.302343368530273
+ ],
+ [
+ "▁certificates",
+ -12.302434921264648
+ ],
+ [
+ "riad",
+ -12.302549362182617
+ ],
+ [
+ "stab",
+ -12.302570343017578
+ ],
+ [
+ "affiche",
+ -12.302604675292969
+ ],
+ [
+ "▁iPod",
+ -12.302645683288574
+ ],
+ [
+ "▁aștept",
+ -12.302726745605469
+ ],
+ [
+ "▁$500",
+ -12.302751541137695
+ ],
+ [
+ "▁Catherine",
+ -12.302952766418457
+ ],
+ [
+ "▁Circuit",
+ -12.302957534790039
+ ],
+ [
+ "▁ranch",
+ -12.303045272827148
+ ],
+ [
+ "▁consequence",
+ -12.303118705749512
+ ],
+ [
+ "listened",
+ -12.303131103515625
+ ],
+ [
+ "▁Options",
+ -12.303187370300293
+ ],
+ [
+ "feed",
+ -12.30318832397461
+ ],
+ [
+ "▁adviser",
+ -12.303248405456543
+ ],
+ [
+ "▁présenter",
+ -12.30333423614502
+ ],
+ [
+ "substant",
+ -12.30337905883789
+ ],
+ [
+ "▁Flag",
+ -12.303604125976562
+ ],
+ [
+ "▁Keith",
+ -12.30366325378418
+ ],
+ [
+ "▁inima",
+ -12.303709983825684
+ ],
+ [
+ "▁substrate",
+ -12.30373764038086
+ ],
+ [
+ "▁charger",
+ -12.303803443908691
+ ],
+ [
+ "▁reporter",
+ -12.303844451904297
+ ],
+ [
+ "ütz",
+ -12.304068565368652
+ ],
+ [
+ "▁unten",
+ -12.30417537689209
+ ],
+ [
+ "▁sympa",
+ -12.304542541503906
+ ],
+ [
+ "▁defeated",
+ -12.304600715637207
+ ],
+ [
+ "ändig",
+ -12.304644584655762
+ ],
+ [
+ "individu",
+ -12.304747581481934
+ ],
+ [
+ "▁Straßen",
+ -12.304774284362793
+ ],
+ [
+ "▁Nepal",
+ -12.304791450500488
+ ],
+ [
+ "million",
+ -12.304803848266602
+ ],
+ [
+ "▁Cake",
+ -12.30499267578125
+ ],
+ [
+ "▁investigations",
+ -12.30526065826416
+ ],
+ [
+ "▁inspector",
+ -12.3054780960083
+ ],
+ [
+ "▁Campbell",
+ -12.305486679077148
+ ],
+ [
+ "▁consommation",
+ -12.305489540100098
+ ],
+ [
+ "▁Ministerul",
+ -12.305628776550293
+ ],
+ [
+ "Advisory",
+ -12.305749893188477
+ ],
+ [
+ "▁Leistungs",
+ -12.305939674377441
+ ],
+ [
+ "▁Pull",
+ -12.306157112121582
+ ],
+ [
+ "▁lover",
+ -12.306194305419922
+ ],
+ [
+ "▁trunk",
+ -12.306380271911621
+ ],
+ [
+ "▁folosesc",
+ -12.30639934539795
+ ],
+ [
+ "pom",
+ -12.306558609008789
+ ],
+ [
+ "wunder",
+ -12.306794166564941
+ ],
+ [
+ "▁happier",
+ -12.306801795959473
+ ],
+ [
+ "▁embark",
+ -12.30689525604248
+ ],
+ [
+ "▁mediul",
+ -12.3069486618042
+ ],
+ [
+ "riff",
+ -12.306973457336426
+ ],
+ [
+ "▁copilul",
+ -12.307039260864258
+ ],
+ [
+ "ommage",
+ -12.307126998901367
+ ],
+ [
+ "rechnung",
+ -12.307218551635742
+ ],
+ [
+ "NU",
+ -12.307220458984375
+ ],
+ [
+ "▁fellowship",
+ -12.307395935058594
+ ],
+ [
+ "▁Mental",
+ -12.307403564453125
+ ],
+ [
+ "▁fever",
+ -12.3074312210083
+ ],
+ [
+ "▁silly",
+ -12.307547569274902
+ ],
+ [
+ "Object",
+ -12.30756664276123
+ ],
+ [
+ "NV",
+ -12.307591438293457
+ ],
+ [
+ "от",
+ -12.30774974822998
+ ],
+ [
+ "▁Strand",
+ -12.307762145996094
+ ],
+ [
+ "▁Exist",
+ -12.30777359008789
+ ],
+ [
+ "warum",
+ -12.307832717895508
+ ],
+ [
+ "CY",
+ -12.307848930358887
+ ],
+ [
+ "kä",
+ -12.307856559753418
+ ],
+ [
+ "!!!!!",
+ -12.307869911193848
+ ],
+ [
+ "▁moarte",
+ -12.30793571472168
+ ],
+ [
+ "▁waterfall",
+ -12.308024406433105
+ ],
+ [
+ "left",
+ -12.30815601348877
+ ],
+ [
+ "▁Nursing",
+ -12.308225631713867
+ ],
+ [
+ "▁invalid",
+ -12.30826187133789
+ ],
+ [
+ "struktur",
+ -12.308385848999023
+ ],
+ [
+ "Allerdings",
+ -12.30838680267334
+ ],
+ [
+ "étranger",
+ -12.30838680267334
+ ],
+ [
+ "▁prost",
+ -12.308517456054688
+ ],
+ [
+ "▁Parent",
+ -12.308562278747559
+ ],
+ [
+ "▁întreag",
+ -12.308611869812012
+ ],
+ [
+ "▁compensate",
+ -12.308871269226074
+ ],
+ [
+ "▁sometime",
+ -12.308955192565918
+ ],
+ [
+ "graduate",
+ -12.308968544006348
+ ],
+ [
+ "▁Carter",
+ -12.30898380279541
+ ],
+ [
+ "▁crap",
+ -12.308998107910156
+ ],
+ [
+ "▁mathematics",
+ -12.309067726135254
+ ],
+ [
+ "resemble",
+ -12.309069633483887
+ ],
+ [
+ "Dame",
+ -12.309152603149414
+ ],
+ [
+ "▁Swa",
+ -12.309198379516602
+ ],
+ [
+ "▁celebrity",
+ -12.309239387512207
+ ],
+ [
+ "▁verified",
+ -12.309338569641113
+ ],
+ [
+ "▁Behind",
+ -12.309349060058594
+ ],
+ [
+ "carbon",
+ -12.309432983398438
+ ],
+ [
+ "▁gateway",
+ -12.309490203857422
+ ],
+ [
+ "▁ambitious",
+ -12.30952262878418
+ ],
+ [
+ "▁Wellness",
+ -12.30966567993164
+ ],
+ [
+ "30,000",
+ -12.30968189239502
+ ],
+ [
+ "defined",
+ -12.309929847717285
+ ],
+ [
+ "specializes",
+ -12.310121536254883
+ ],
+ [
+ "▁Chase",
+ -12.310199737548828
+ ],
+ [
+ "HF",
+ -12.310233116149902
+ ],
+ [
+ "ABLE",
+ -12.310348510742188
+ ],
+ [
+ "▁Ehr",
+ -12.310467720031738
+ ],
+ [
+ "▁régime",
+ -12.310480117797852
+ ],
+ [
+ "▁awake",
+ -12.310487747192383
+ ],
+ [
+ "▁seafood",
+ -12.310487747192383
+ ],
+ [
+ "leading",
+ -12.310554504394531
+ ],
+ [
+ "▁Rule",
+ -12.310602188110352
+ ],
+ [
+ "verkehr",
+ -12.310726165771484
+ ],
+ [
+ "erem",
+ -12.310737609863281
+ ],
+ [
+ "▁1973",
+ -12.310795783996582
+ ],
+ [
+ "personal",
+ -12.311171531677246
+ ],
+ [
+ "ența",
+ -12.311330795288086
+ ],
+ [
+ "apprend",
+ -12.311396598815918
+ ],
+ [
+ "faisant",
+ -12.311420440673828
+ ],
+ [
+ "▁Sounds",
+ -12.31151008605957
+ ],
+ [
+ "▁Launch",
+ -12.31151294708252
+ ],
+ [
+ "half",
+ -12.311636924743652
+ ],
+ [
+ "▁verre",
+ -12.311859130859375
+ ],
+ [
+ "▁Regular",
+ -12.31207275390625
+ ],
+ [
+ "▁Nancy",
+ -12.312142372131348
+ ],
+ [
+ "quelles",
+ -12.312161445617676
+ ],
+ [
+ "▁erhält",
+ -12.312169075012207
+ ],
+ [
+ "▁socks",
+ -12.3121919631958
+ ],
+ [
+ "lamp",
+ -12.312387466430664
+ ],
+ [
+ "▁durchgeführt",
+ -12.312472343444824
+ ],
+ [
+ "▁advertise",
+ -12.31260871887207
+ ],
+ [
+ "powered",
+ -12.312653541564941
+ ],
+ [
+ "▁concur",
+ -12.312699317932129
+ ],
+ [
+ "▁ressources",
+ -12.31293773651123
+ ],
+ [
+ "▁allocation",
+ -12.312986373901367
+ ],
+ [
+ "chon",
+ -12.313041687011719
+ ],
+ [
+ "▁Larry",
+ -12.313177108764648
+ ],
+ [
+ "lässig",
+ -12.313254356384277
+ ],
+ [
+ "OLD",
+ -12.313493728637695
+ ],
+ [
+ "itty",
+ -12.313599586486816
+ ],
+ [
+ "▁immuno",
+ -12.313645362854004
+ ],
+ [
+ "▁(+",
+ -12.313651084899902
+ ],
+ [
+ "▁Essential",
+ -12.313674926757812
+ ],
+ [
+ "▁semaines",
+ -12.313719749450684
+ ],
+ [
+ "Ru",
+ -12.31375503540039
+ ],
+ [
+ "▁Gear",
+ -12.313764572143555
+ ],
+ [
+ "völlig",
+ -12.313850402832031
+ ],
+ [
+ "liga",
+ -12.31391716003418
+ ],
+ [
+ "▁Neg",
+ -12.314082145690918
+ ],
+ [
+ "▁gratitude",
+ -12.31408977508545
+ ],
+ [
+ "aventure",
+ -12.314108848571777
+ ],
+ [
+ "▁frustrated",
+ -12.314115524291992
+ ],
+ [
+ "▁retrait",
+ -12.31422233581543
+ ],
+ [
+ "▁statut",
+ -12.314231872558594
+ ],
+ [
+ "550",
+ -12.31434440612793
+ ],
+ [
+ "ла",
+ -12.314428329467773
+ ],
+ [
+ "risto",
+ -12.314448356628418
+ ],
+ [
+ "WAY",
+ -12.314607620239258
+ ],
+ [
+ "▁pigment",
+ -12.314652442932129
+ ],
+ [
+ "Selon",
+ -12.314715385437012
+ ],
+ [
+ "stil",
+ -12.3148775100708
+ ],
+ [
+ "▁Marin",
+ -12.315055847167969
+ ],
+ [
+ "ashi",
+ -12.315085411071777
+ ],
+ [
+ "▁contine",
+ -12.31519889831543
+ ],
+ [
+ "▁Economics",
+ -12.315200805664062
+ ],
+ [
+ "both",
+ -12.3152437210083
+ ],
+ [
+ "▁Dou",
+ -12.31527328491211
+ ],
+ [
+ "Fel",
+ -12.315373420715332
+ ],
+ [
+ "UNT",
+ -12.315434455871582
+ ],
+ [
+ "▁grandmother",
+ -12.31548023223877
+ ],
+ [
+ "▁domicile",
+ -12.315678596496582
+ ],
+ [
+ "▁buffer",
+ -12.31574535369873
+ ],
+ [
+ "▁fuse",
+ -12.315815925598145
+ ],
+ [
+ "▁dosage",
+ -12.315821647644043
+ ],
+ [
+ "▁Nici",
+ -12.315839767456055
+ ],
+ [
+ "▁worries",
+ -12.315908432006836
+ ],
+ [
+ "▁Rail",
+ -12.3159818649292
+ ],
+ [
+ "uneori",
+ -12.315990447998047
+ ],
+ [
+ "▁Sierra",
+ -12.316030502319336
+ ],
+ [
+ "▁porni",
+ -12.316032409667969
+ ],
+ [
+ "▁NOTE",
+ -12.316056251525879
+ ],
+ [
+ "▁tendency",
+ -12.316065788269043
+ ],
+ [
+ "Set",
+ -12.316256523132324
+ ],
+ [
+ "▁Hof",
+ -12.31629753112793
+ ],
+ [
+ "▁Ruhe",
+ -12.316300392150879
+ ],
+ [
+ "harm",
+ -12.316360473632812
+ ],
+ [
+ "▁Developer",
+ -12.316367149353027
+ ],
+ [
+ "suing",
+ -12.316400527954102
+ ],
+ [
+ "persönlichen",
+ -12.31658935546875
+ ],
+ [
+ "▁agréable",
+ -12.316596031188965
+ ],
+ [
+ "commissioned",
+ -12.316696166992188
+ ],
+ [
+ "▁1974",
+ -12.31672191619873
+ ],
+ [
+ "▁1969",
+ -12.316758155822754
+ ],
+ [
+ "▁regl",
+ -12.316996574401855
+ ],
+ [
+ "▁terror",
+ -12.317042350769043
+ ],
+ [
+ "▁température",
+ -12.317051887512207
+ ],
+ [
+ "▁Archiv",
+ -12.31706714630127
+ ],
+ [
+ "▁Military",
+ -12.317140579223633
+ ],
+ [
+ "▁König",
+ -12.317290306091309
+ ],
+ [
+ "▁forex",
+ -12.31737232208252
+ ],
+ [
+ "wiki",
+ -12.31745719909668
+ ],
+ [
+ "thetic",
+ -12.317506790161133
+ ],
+ [
+ "alaturi",
+ -12.317974090576172
+ ],
+ [
+ "▁montant",
+ -12.3179931640625
+ ],
+ [
+ "▁maladie",
+ -12.318044662475586
+ ],
+ [
+ "gust",
+ -12.318151473999023
+ ],
+ [
+ "▁demander",
+ -12.318164825439453
+ ],
+ [
+ "avocat",
+ -12.318191528320312
+ ],
+ [
+ "▁sci",
+ -12.318192481994629
+ ],
+ [
+ "▁Wireless",
+ -12.318214416503906
+ ],
+ [
+ "▁Dein",
+ -12.318220138549805
+ ],
+ [
+ "▁trio",
+ -12.3183012008667
+ ],
+ [
+ "▁Same",
+ -12.318395614624023
+ ],
+ [
+ "Datei",
+ -12.318464279174805
+ ],
+ [
+ "▁alerg",
+ -12.318578720092773
+ ],
+ [
+ "crowded",
+ -12.318657875061035
+ ],
+ [
+ "▁Punkt",
+ -12.318853378295898
+ ],
+ [
+ "▁sanctions",
+ -12.318864822387695
+ ],
+ [
+ "stating",
+ -12.318922996520996
+ ],
+ [
+ "▁discusse",
+ -12.318949699401855
+ ],
+ [
+ "▁Eigen",
+ -12.319068908691406
+ ],
+ [
+ "▁sănătate",
+ -12.31911563873291
+ ],
+ [
+ "▁correspondence",
+ -12.319211959838867
+ ],
+ [
+ "cred",
+ -12.319331169128418
+ ],
+ [
+ "VG",
+ -12.319347381591797
+ ],
+ [
+ "▁différence",
+ -12.319347381591797
+ ],
+ [
+ "▁Montreal",
+ -12.319391250610352
+ ],
+ [
+ "▁masini",
+ -12.319398880004883
+ ],
+ [
+ "iata",
+ -12.319487571716309
+ ],
+ [
+ "▁sampling",
+ -12.319574356079102
+ ],
+ [
+ "▁Gib",
+ -12.319831848144531
+ ],
+ [
+ "▁sheer",
+ -12.319944381713867
+ ],
+ [
+ "330",
+ -12.319947242736816
+ ],
+ [
+ "CHI",
+ -12.319990158081055
+ ],
+ [
+ "▁damn",
+ -12.320030212402344
+ ],
+ [
+ "▁Advisor",
+ -12.320201873779297
+ ],
+ [
+ "Typically",
+ -12.320302963256836
+ ],
+ [
+ "ssé",
+ -12.320352554321289
+ ],
+ [
+ "quart",
+ -12.320361137390137
+ ],
+ [
+ "chete",
+ -12.320385932922363
+ ],
+ [
+ "▁Puerto",
+ -12.32049560546875
+ ],
+ [
+ "2-1",
+ -12.32050609588623
+ ],
+ [
+ "NN",
+ -12.320674896240234
+ ],
+ [
+ "▁styling",
+ -12.320707321166992
+ ],
+ [
+ "rud",
+ -12.320777893066406
+ ],
+ [
+ "од",
+ -12.320856094360352
+ ],
+ [
+ "▁Hydro",
+ -12.320941925048828
+ ],
+ [
+ "▁Cable",
+ -12.320961952209473
+ ],
+ [
+ "video",
+ -12.320974349975586
+ ],
+ [
+ "▁Wirkung",
+ -12.321194648742676
+ ],
+ [
+ "▁noble",
+ -12.321270942687988
+ ],
+ [
+ "▁Sonder",
+ -12.32129192352295
+ ],
+ [
+ "mati",
+ -12.321317672729492
+ ],
+ [
+ "850",
+ -12.321395874023438
+ ],
+ [
+ "▁Richmond",
+ -12.32143497467041
+ ],
+ [
+ "▁niciodată",
+ -12.321442604064941
+ ],
+ [
+ "AO",
+ -12.321527481079102
+ ],
+ [
+ "▁altered",
+ -12.321648597717285
+ ],
+ [
+ "▁(15",
+ -12.32168960571289
+ ],
+ [
+ "▁Motiv",
+ -12.322052001953125
+ ],
+ [
+ "AKE",
+ -12.322089195251465
+ ],
+ [
+ "▁bestimmte",
+ -12.322172164916992
+ ],
+ [
+ "6.5",
+ -12.322176933288574
+ ],
+ [
+ "hectare",
+ -12.322333335876465
+ ],
+ [
+ "atorită",
+ -12.322335243225098
+ ],
+ [
+ "▁phases",
+ -12.322447776794434
+ ],
+ [
+ "▁Nova",
+ -12.322566032409668
+ ],
+ [
+ "ordinateur",
+ -12.322579383850098
+ ],
+ [
+ "▁corrupt",
+ -12.322813034057617
+ ],
+ [
+ "error",
+ -12.322895050048828
+ ],
+ [
+ "▁attacked",
+ -12.323005676269531
+ ],
+ [
+ "▁Kirche",
+ -12.323019981384277
+ ],
+ [
+ "heir",
+ -12.323040962219238
+ ],
+ [
+ "Das",
+ -12.323254585266113
+ ],
+ [
+ "▁anxious",
+ -12.323258399963379
+ ],
+ [
+ "▁Doc",
+ -12.323386192321777
+ ],
+ [
+ "▁Roth",
+ -12.323415756225586
+ ],
+ [
+ "▁Cine",
+ -12.32388687133789
+ ],
+ [
+ "▁auditor",
+ -12.324418067932129
+ ],
+ [
+ "▁beverage",
+ -12.324586868286133
+ ],
+ [
+ "▁précédent",
+ -12.324637413024902
+ ],
+ [
+ "▁deploy",
+ -12.324837684631348
+ ],
+ [
+ "▁accessibility",
+ -12.324843406677246
+ ],
+ [
+ "▁cage",
+ -12.324885368347168
+ ],
+ [
+ "▁Contra",
+ -12.324934005737305
+ ],
+ [
+ "Best",
+ -12.324952125549316
+ ],
+ [
+ "iji",
+ -12.324972152709961
+ ],
+ [
+ "▁père",
+ -12.325060844421387
+ ],
+ [
+ "▁scenic",
+ -12.32511043548584
+ ],
+ [
+ "synthesis",
+ -12.325165748596191
+ ],
+ [
+ "ßen",
+ -12.32534408569336
+ ],
+ [
+ "▁Videos",
+ -12.325482368469238
+ ],
+ [
+ "▁refus",
+ -12.325484275817871
+ ],
+ [
+ "stimmen",
+ -12.3255615234375
+ ],
+ [
+ "▁sleek",
+ -12.325577735900879
+ ],
+ [
+ "artige",
+ -12.32563591003418
+ ],
+ [
+ "mari",
+ -12.32568359375
+ ],
+ [
+ "▁excelent",
+ -12.325740814208984
+ ],
+ [
+ "▁negativ",
+ -12.325806617736816
+ ],
+ [
+ "▁blocking",
+ -12.32590103149414
+ ],
+ [
+ "spricht",
+ -12.326001167297363
+ ],
+ [
+ "▁discomfort",
+ -12.32602310180664
+ ],
+ [
+ "▁stratégie",
+ -12.32602310180664
+ ],
+ [
+ "▁Datenschutz",
+ -12.326078414916992
+ ],
+ [
+ "curg",
+ -12.326128005981445
+ ],
+ [
+ "▁lapte",
+ -12.326432228088379
+ ],
+ [
+ "▁acasă",
+ -12.326491355895996
+ ],
+ [
+ "▁ausschließlich",
+ -12.32653522491455
+ ],
+ [
+ "▁unbedingt",
+ -12.326802253723145
+ ],
+ [
+ "▁Linie",
+ -12.32689380645752
+ ],
+ [
+ "▁subscribers",
+ -12.327019691467285
+ ],
+ [
+ "109",
+ -12.32702350616455
+ ],
+ [
+ "▁Waste",
+ -12.32712173461914
+ ],
+ [
+ "▁Planung",
+ -12.327231407165527
+ ],
+ [
+ "▁visually",
+ -12.32734489440918
+ ],
+ [
+ "utilizarea",
+ -12.327370643615723
+ ],
+ [
+ "uba",
+ -12.327381134033203
+ ],
+ [
+ "▁fifteen",
+ -12.327411651611328
+ ],
+ [
+ "▁légère",
+ -12.327411651611328
+ ],
+ [
+ "ința",
+ -12.327446937561035
+ ],
+ [
+ "▁tolerance",
+ -12.327460289001465
+ ],
+ [
+ "▁piscine",
+ -12.327536582946777
+ ],
+ [
+ "▁nails",
+ -12.327569007873535
+ ],
+ [
+ "▁accus",
+ -12.327693939208984
+ ],
+ [
+ "▁coeur",
+ -12.327773094177246
+ ],
+ [
+ "freie",
+ -12.327849388122559
+ ],
+ [
+ "enţă",
+ -12.32812213897705
+ ],
+ [
+ "▁glucose",
+ -12.328336715698242
+ ],
+ [
+ "▁Jar",
+ -12.32838249206543
+ ],
+ [
+ "▁commencer",
+ -12.328387260437012
+ ],
+ [
+ "▁eliminating",
+ -12.328414916992188
+ ],
+ [
+ "▁mutation",
+ -12.32844352722168
+ ],
+ [
+ "▁afirma",
+ -12.328444480895996
+ ],
+ [
+ "▁Consulting",
+ -12.328454971313477
+ ],
+ [
+ "adia",
+ -12.328543663024902
+ ],
+ [
+ "zog",
+ -12.328604698181152
+ ],
+ [
+ "▁pielea",
+ -12.328658103942871
+ ],
+ [
+ "rton",
+ -12.328706741333008
+ ],
+ [
+ "exercice",
+ -12.3287935256958
+ ],
+ [
+ "namely",
+ -12.328847885131836
+ ],
+ [
+ "▁ajutor",
+ -12.3289155960083
+ ],
+ [
+ "▁markers",
+ -12.328917503356934
+ ],
+ [
+ "▁gardening",
+ -12.328932762145996
+ ],
+ [
+ "Karte",
+ -12.329038619995117
+ ],
+ [
+ "▁Pump",
+ -12.329142570495605
+ ],
+ [
+ "▁Dual",
+ -12.329169273376465
+ ],
+ [
+ "▁pratiques",
+ -12.329349517822266
+ ],
+ [
+ "▁behavioral",
+ -12.329358100891113
+ ],
+ [
+ "▁construire",
+ -12.329511642456055
+ ],
+ [
+ "▁Leonard",
+ -12.329596519470215
+ ],
+ [
+ "ediglich",
+ -12.329630851745605
+ ],
+ [
+ "ubbed",
+ -12.3297758102417
+ ],
+ [
+ "NK",
+ -12.329792022705078
+ ],
+ [
+ "shell",
+ -12.329912185668945
+ ],
+ [
+ "▁persönliche",
+ -12.329996109008789
+ ],
+ [
+ "ecuring",
+ -12.329998970031738
+ ],
+ [
+ "beaten",
+ -12.33000373840332
+ ],
+ [
+ "ALE",
+ -12.330053329467773
+ ],
+ [
+ "▁puppy",
+ -12.33023452758789
+ ],
+ [
+ "▁capac",
+ -12.33027458190918
+ ],
+ [
+ "▁seventh",
+ -12.330394744873047
+ ],
+ [
+ "▁nursery",
+ -12.330400466918945
+ ],
+ [
+ "▁Rum",
+ -12.330419540405273
+ ],
+ [
+ "▁exquisite",
+ -12.330423355102539
+ ],
+ [
+ "▁Legi",
+ -12.330483436584473
+ ],
+ [
+ "▁persist",
+ -12.330497741699219
+ ],
+ [
+ "bacterial",
+ -12.330548286437988
+ ],
+ [
+ "▁cereal",
+ -12.330572128295898
+ ],
+ [
+ "▁principe",
+ -12.330693244934082
+ ],
+ [
+ "chip",
+ -12.330766677856445
+ ],
+ [
+ "rush",
+ -12.330832481384277
+ ],
+ [
+ "▁funnel",
+ -12.330904006958008
+ ],
+ [
+ "▁calitatea",
+ -12.331024169921875
+ ],
+ [
+ "ibă",
+ -12.33104419708252
+ ],
+ [
+ "▁reign",
+ -12.331086158752441
+ ],
+ [
+ "▁congregation",
+ -12.331120491027832
+ ],
+ [
+ "▁obtine",
+ -12.331270217895508
+ ],
+ [
+ "▁découverte",
+ -12.331286430358887
+ ],
+ [
+ "▁gama",
+ -12.331315040588379
+ ],
+ [
+ "▁judec",
+ -12.33132553100586
+ ],
+ [
+ "Plan",
+ -12.331351280212402
+ ],
+ [
+ "▁gesture",
+ -12.331539154052734
+ ],
+ [
+ "öffentlichen",
+ -12.331644058227539
+ ],
+ [
+ "▁imported",
+ -12.331693649291992
+ ],
+ [
+ "▁rotate",
+ -12.331747055053711
+ ],
+ [
+ "blown",
+ -12.331756591796875
+ ],
+ [
+ "▁Protein",
+ -12.331827163696289
+ ],
+ [
+ "parfaitement",
+ -12.331832885742188
+ ],
+ [
+ "ondo",
+ -12.331868171691895
+ ],
+ [
+ "ologists",
+ -12.331890106201172
+ ],
+ [
+ "▁neighborhoods",
+ -12.331989288330078
+ ],
+ [
+ "▁Pope",
+ -12.33202075958252
+ ],
+ [
+ "▁museums",
+ -12.332194328308105
+ ],
+ [
+ "▁porter",
+ -12.332330703735352
+ ],
+ [
+ "▁kiss",
+ -12.332335472106934
+ ],
+ [
+ "pdf",
+ -12.332354545593262
+ ],
+ [
+ "sided",
+ -12.332359313964844
+ ],
+ [
+ "▁gern",
+ -12.332395553588867
+ ],
+ [
+ "bedingungen",
+ -12.332496643066406
+ ],
+ [
+ "▁Ride",
+ -12.332582473754883
+ ],
+ [
+ "Apoi",
+ -12.332584381103516
+ ],
+ [
+ "▁bestehen",
+ -12.332603454589844
+ ],
+ [
+ "5\"",
+ -12.33285903930664
+ ],
+ [
+ "bob",
+ -12.332862854003906
+ ],
+ [
+ "ficient",
+ -12.33303165435791
+ ],
+ [
+ "premise",
+ -12.333086967468262
+ ],
+ [
+ "▁Clip",
+ -12.333112716674805
+ ],
+ [
+ "▁concours",
+ -12.333213806152344
+ ],
+ [
+ "olar",
+ -12.333281517028809
+ ],
+ [
+ "▁Centr",
+ -12.333356857299805
+ ],
+ [
+ "outlined",
+ -12.333429336547852
+ ],
+ [
+ "▁observa",
+ -12.333511352539062
+ ],
+ [
+ "▁negotiate",
+ -12.333537101745605
+ ],
+ [
+ "▁Partnership",
+ -12.33358383178711
+ ],
+ [
+ "clock",
+ -12.333662033081055
+ ],
+ [
+ "roasted",
+ -12.333755493164062
+ ],
+ [
+ "Pourquoi",
+ -12.33391284942627
+ ],
+ [
+ "▁Marshall",
+ -12.334005355834961
+ ],
+ [
+ "▁Gerade",
+ -12.334052085876465
+ ],
+ [
+ "▁pachet",
+ -12.334160804748535
+ ],
+ [
+ "▁preliminary",
+ -12.334162712097168
+ ],
+ [
+ "▁tragic",
+ -12.334200859069824
+ ],
+ [
+ "author",
+ -12.334268569946289
+ ],
+ [
+ "▁Gov",
+ -12.334309577941895
+ ],
+ [
+ "▁comunic",
+ -12.334403991699219
+ ],
+ [
+ "▁coordinator",
+ -12.334410667419434
+ ],
+ [
+ "YA",
+ -12.33445930480957
+ ],
+ [
+ "▁Steam",
+ -12.33476734161377
+ ],
+ [
+ "▁Nag",
+ -12.334796905517578
+ ],
+ [
+ "▁Kara",
+ -12.334851264953613
+ ],
+ [
+ "▁Gang",
+ -12.334858894348145
+ ],
+ [
+ "aurez",
+ -12.334868431091309
+ ],
+ [
+ "▁horrible",
+ -12.334869384765625
+ ],
+ [
+ "▁Luxury",
+ -12.335076332092285
+ ],
+ [
+ "▁encouragement",
+ -12.335169792175293
+ ],
+ [
+ "▁conceptual",
+ -12.335250854492188
+ ],
+ [
+ "▁constituent",
+ -12.335431098937988
+ ],
+ [
+ "nvelop",
+ -12.335494041442871
+ ],
+ [
+ "ucc",
+ -12.335500717163086
+ ],
+ [
+ "▁conçu",
+ -12.335542678833008
+ ],
+ [
+ "pfel",
+ -12.33559513092041
+ ],
+ [
+ "special",
+ -12.335700988769531
+ ],
+ [
+ "▁Growth",
+ -12.335834503173828
+ ],
+ [
+ "cada",
+ -12.335916519165039
+ ],
+ [
+ "▁oamenilor",
+ -12.335976600646973
+ ],
+ [
+ "▁vendredi",
+ -12.336021423339844
+ ],
+ [
+ "▁coupe",
+ -12.336055755615234
+ ],
+ [
+ "▁Danke",
+ -12.336134910583496
+ ],
+ [
+ "reflects",
+ -12.336181640625
+ ],
+ [
+ "▁girlfriend",
+ -12.336273193359375
+ ],
+ [
+ "▁diffuse",
+ -12.336325645446777
+ ],
+ [
+ "HER",
+ -12.336328506469727
+ ],
+ [
+ "storing",
+ -12.336464881896973
+ ],
+ [
+ "ailing",
+ -12.336591720581055
+ ],
+ [
+ "▁Desi",
+ -12.336601257324219
+ ],
+ [
+ "stitution",
+ -12.336832046508789
+ ],
+ [
+ "▁adun",
+ -12.336844444274902
+ ],
+ [
+ "▁Partie",
+ -12.336869239807129
+ ],
+ [
+ "▁tissues",
+ -12.336958885192871
+ ],
+ [
+ "▁discovering",
+ -12.337154388427734
+ ],
+ [
+ "Jacques",
+ -12.337178230285645
+ ],
+ [
+ "lungs",
+ -12.33724594116211
+ ],
+ [
+ "▁Handy",
+ -12.337261199951172
+ ],
+ [
+ "centric",
+ -12.337285995483398
+ ],
+ [
+ "slav",
+ -12.337442398071289
+ ],
+ [
+ "▁sights",
+ -12.337560653686523
+ ],
+ [
+ "▁Category",
+ -12.337644577026367
+ ],
+ [
+ "▁Einrichtung",
+ -12.337957382202148
+ ],
+ [
+ "▁Robinson",
+ -12.33804702758789
+ ],
+ [
+ "▁Terra",
+ -12.338150978088379
+ ],
+ [
+ "▁creep",
+ -12.338167190551758
+ ],
+ [
+ "▁Lob",
+ -12.338184356689453
+ ],
+ [
+ "001",
+ -12.33820629119873
+ ],
+ [
+ "kop",
+ -12.338208198547363
+ ],
+ [
+ "Emb",
+ -12.338292121887207
+ ],
+ [
+ "▁forgive",
+ -12.338391304016113
+ ],
+ [
+ "▁icons",
+ -12.33847427368164
+ ],
+ [
+ "electric",
+ -12.3385009765625
+ ],
+ [
+ "▁faucet",
+ -12.338516235351562
+ ],
+ [
+ "▁invisible",
+ -12.3386812210083
+ ],
+ [
+ "sprach",
+ -12.338801383972168
+ ],
+ [
+ "▁beachten",
+ -12.33881664276123
+ ],
+ [
+ "rahm",
+ -12.338833808898926
+ ],
+ [
+ "▁Teacher",
+ -12.338919639587402
+ ],
+ [
+ "Fab",
+ -12.339070320129395
+ ],
+ [
+ "▁joue",
+ -12.339101791381836
+ ],
+ [
+ "▁Popular",
+ -12.339120864868164
+ ],
+ [
+ "▁Februar",
+ -12.339171409606934
+ ],
+ [
+ "sound",
+ -12.339251518249512
+ ],
+ [
+ "▁(0",
+ -12.339317321777344
+ ],
+ [
+ "▁Compare",
+ -12.33938980102539
+ ],
+ [
+ "▁pads",
+ -12.339455604553223
+ ],
+ [
+ "270",
+ -12.339498519897461
+ ],
+ [
+ "ousse",
+ -12.339548110961914
+ ],
+ [
+ "▁UAE",
+ -12.339786529541016
+ ],
+ [
+ "izări",
+ -12.339787483215332
+ ],
+ [
+ "▁bonuses",
+ -12.33993911743164
+ ],
+ [
+ "▁switches",
+ -12.3400239944458
+ ],
+ [
+ "▁Brothers",
+ -12.340166091918945
+ ],
+ [
+ "▁environmentally",
+ -12.340171813964844
+ ],
+ [
+ "vista",
+ -12.340264320373535
+ ],
+ [
+ "▁intentions",
+ -12.3402738571167
+ ],
+ [
+ "▁Terri",
+ -12.340301513671875
+ ],
+ [
+ "▁diabet",
+ -12.34030532836914
+ ],
+ [
+ "▁prese",
+ -12.340333938598633
+ ],
+ [
+ "▁parcurs",
+ -12.340389251708984
+ ],
+ [
+ "Warum",
+ -12.340449333190918
+ ],
+ [
+ "▁credentials",
+ -12.340455055236816
+ ],
+ [
+ "▁PLA",
+ -12.34046459197998
+ ],
+ [
+ "▁instruct",
+ -12.340470314025879
+ ],
+ [
+ "▁benefic",
+ -12.340633392333984
+ ],
+ [
+ "write",
+ -12.340675354003906
+ ],
+ [
+ "▁poids",
+ -12.340773582458496
+ ],
+ [
+ "▁Anspruch",
+ -12.340923309326172
+ ],
+ [
+ "▁avocado",
+ -12.340923309326172
+ ],
+ [
+ "▁inevitable",
+ -12.340923309326172
+ ],
+ [
+ "▁poorly",
+ -12.340950965881348
+ ],
+ [
+ "karte",
+ -12.340994834899902
+ ],
+ [
+ "▁Publishing",
+ -12.340999603271484
+ ],
+ [
+ "odată",
+ -12.341140747070312
+ ],
+ [
+ "▁scientifique",
+ -12.341157913208008
+ ],
+ [
+ "▁lăsa",
+ -12.341262817382812
+ ],
+ [
+ "▁secol",
+ -12.34131908416748
+ ],
+ [
+ "▁nevertheless",
+ -12.341392517089844
+ ],
+ [
+ "SAT",
+ -12.341597557067871
+ ],
+ [
+ "280",
+ -12.341651916503906
+ ],
+ [
+ "▁prevederi",
+ -12.341670989990234
+ ],
+ [
+ "▁chrome",
+ -12.342002868652344
+ ],
+ [
+ "institut",
+ -12.342267036437988
+ ],
+ [
+ "richtigen",
+ -12.34228515625
+ ],
+ [
+ "▁grief",
+ -12.342338562011719
+ ],
+ [
+ "▁penalties",
+ -12.342373847961426
+ ],
+ [
+ "▁Bayern",
+ -12.34238052368164
+ ],
+ [
+ "▁caramel",
+ -12.342473983764648
+ ],
+ [
+ "Now",
+ -12.342495918273926
+ ],
+ [
+ "Stiftung",
+ -12.342576026916504
+ ],
+ [
+ "country",
+ -12.342737197875977
+ ],
+ [
+ "dication",
+ -12.34278678894043
+ ],
+ [
+ "▁Chor",
+ -12.342801094055176
+ ],
+ [
+ "▁rămâne",
+ -12.342936515808105
+ ],
+ [
+ "▁TOP",
+ -12.34300708770752
+ ],
+ [
+ "▁complète",
+ -12.34301471710205
+ ],
+ [
+ "▁Marian",
+ -12.34302806854248
+ ],
+ [
+ "▁Avant",
+ -12.343121528625488
+ ],
+ [
+ "▁Shower",
+ -12.343156814575195
+ ],
+ [
+ "treu",
+ -12.34316349029541
+ ],
+ [
+ "▁chop",
+ -12.34321403503418
+ ],
+ [
+ "▁comfortably",
+ -12.343220710754395
+ ],
+ [
+ "▁autism",
+ -12.34323787689209
+ ],
+ [
+ "▁Sind",
+ -12.34328556060791
+ ],
+ [
+ "▁(20",
+ -12.343340873718262
+ ],
+ [
+ "▁Cinema",
+ -12.343414306640625
+ ],
+ [
+ "compania",
+ -12.343606948852539
+ ],
+ [
+ "▁Lex",
+ -12.343622207641602
+ ],
+ [
+ "▁Sofa",
+ -12.343716621398926
+ ],
+ [
+ "dru",
+ -12.343753814697266
+ ],
+ [
+ "▁verification",
+ -12.343770027160645
+ ],
+ [
+ "▁Immer",
+ -12.343825340270996
+ ],
+ [
+ "lomb",
+ -12.343829154968262
+ ],
+ [
+ "meric",
+ -12.34385871887207
+ ],
+ [
+ "▁slower",
+ -12.34398365020752
+ ],
+ [
+ "▁propag",
+ -12.344090461730957
+ ],
+ [
+ "Inter",
+ -12.344097137451172
+ ],
+ [
+ "selling",
+ -12.34418773651123
+ ],
+ [
+ "▁Bright",
+ -12.344269752502441
+ ],
+ [
+ "condition",
+ -12.344280242919922
+ ],
+ [
+ "PDF",
+ -12.344291687011719
+ ],
+ [
+ "oyez",
+ -12.344391822814941
+ ],
+ [
+ "▁Fried",
+ -12.344420433044434
+ ],
+ [
+ "▁Nazi",
+ -12.34443187713623
+ ],
+ [
+ "▁Buffalo",
+ -12.344447135925293
+ ],
+ [
+ "▁Sue",
+ -12.344449043273926
+ ],
+ [
+ "▁Rhein",
+ -12.34468936920166
+ ],
+ [
+ "▁Klaus",
+ -12.344889640808105
+ ],
+ [
+ "▁indiqu",
+ -12.344963073730469
+ ],
+ [
+ "echte",
+ -12.344996452331543
+ ],
+ [
+ "▁frecvent",
+ -12.345165252685547
+ ],
+ [
+ "▁conveniently",
+ -12.345187187194824
+ ],
+ [
+ "▁Moi",
+ -12.345197677612305
+ ],
+ [
+ "▁greenhouse",
+ -12.345220565795898
+ ],
+ [
+ "▁rédui",
+ -12.34524154663086
+ ],
+ [
+ "▁lengthy",
+ -12.34542179107666
+ ],
+ [
+ "verband",
+ -12.345534324645996
+ ],
+ [
+ "inţă",
+ -12.345622062683105
+ ],
+ [
+ "▁rigorous",
+ -12.345625877380371
+ ],
+ [
+ "▁Finish",
+ -12.34580135345459
+ ],
+ [
+ "▁FBI",
+ -12.346052169799805
+ ],
+ [
+ "cultura",
+ -12.346083641052246
+ ],
+ [
+ "▁compartment",
+ -12.346110343933105
+ ],
+ [
+ "▁pretend",
+ -12.346117973327637
+ ],
+ [
+ "▁assembled",
+ -12.346212387084961
+ ],
+ [
+ "▁Nie",
+ -12.34639835357666
+ ],
+ [
+ "fession",
+ -12.34640884399414
+ ],
+ [
+ "▁£2",
+ -12.34642219543457
+ ],
+ [
+ "algré",
+ -12.3468017578125
+ ],
+ [
+ "▁anterior",
+ -12.346817970275879
+ ],
+ [
+ "▁Wissenschaft",
+ -12.34683609008789
+ ],
+ [
+ "▁Harbor",
+ -12.346923828125
+ ],
+ [
+ "lix",
+ -12.346985816955566
+ ],
+ [
+ "=\"",
+ -12.347049713134766
+ ],
+ [
+ "▁breathtaking",
+ -12.34705638885498
+ ],
+ [
+ "▁Stern",
+ -12.34708309173584
+ ],
+ [
+ "▁Internetseite",
+ -12.347132682800293
+ ],
+ [
+ "▁locker",
+ -12.347216606140137
+ ],
+ [
+ "▁feather",
+ -12.34726619720459
+ ],
+ [
+ "Serv",
+ -12.347297668457031
+ ],
+ [
+ "▁snake",
+ -12.347332000732422
+ ],
+ [
+ "▁Border",
+ -12.347396850585938
+ ],
+ [
+ "▁undergo",
+ -12.347518920898438
+ ],
+ [
+ "▁petrol",
+ -12.347558975219727
+ ],
+ [
+ "▁dealership",
+ -12.3475923538208
+ ],
+ [
+ "▁commander",
+ -12.347596168518066
+ ],
+ [
+ "▁Monate",
+ -12.347599983215332
+ ],
+ [
+ "▁Guardian",
+ -12.347665786743164
+ ],
+ [
+ "▁Todd",
+ -12.347774505615234
+ ],
+ [
+ "Ann",
+ -12.347825050354004
+ ],
+ [
+ "ibilité",
+ -12.347918510437012
+ ],
+ [
+ "▁Quarter",
+ -12.347987174987793
+ ],
+ [
+ "▁portray",
+ -12.348097801208496
+ ],
+ [
+ "▁Tai",
+ -12.34813404083252
+ ],
+ [
+ "▁strikes",
+ -12.348224639892578
+ ],
+ [
+ "illage",
+ -12.348381042480469
+ ],
+ [
+ "▁IRS",
+ -12.348417282104492
+ ],
+ [
+ "▁lupta",
+ -12.348455429077148
+ ],
+ [
+ "▁Sper",
+ -12.348493576049805
+ ],
+ [
+ "PRO",
+ -12.348530769348145
+ ],
+ [
+ "▁Export",
+ -12.348549842834473
+ ],
+ [
+ "▁crypto",
+ -12.348587989807129
+ ],
+ [
+ "▁barbecue",
+ -12.348692893981934
+ ],
+ [
+ "▁portions",
+ -12.348787307739258
+ ],
+ [
+ "▁explicit",
+ -12.348793983459473
+ ],
+ [
+ "▁angenehm",
+ -12.348834037780762
+ ],
+ [
+ "▁marathon",
+ -12.348946571350098
+ ],
+ [
+ "▁apartament",
+ -12.348982810974121
+ ],
+ [
+ "▁Eva",
+ -12.349079132080078
+ ],
+ [
+ "plate",
+ -12.349181175231934
+ ],
+ [
+ "viel",
+ -12.34925365447998
+ ],
+ [
+ "FIN",
+ -12.34926986694336
+ ],
+ [
+ "dependent",
+ -12.34935188293457
+ ],
+ [
+ "▁cercet",
+ -12.34942626953125
+ ],
+ [
+ "▁midnight",
+ -12.349499702453613
+ ],
+ [
+ "copie",
+ -12.349563598632812
+ ],
+ [
+ "▁companii",
+ -12.349621772766113
+ ],
+ [
+ "▁tenu",
+ -12.349660873413086
+ ],
+ [
+ "1/2",
+ -12.349662780761719
+ ],
+ [
+ "2.4",
+ -12.349693298339844
+ ],
+ [
+ "abri",
+ -12.349699974060059
+ ],
+ [
+ "▁warn",
+ -12.34980297088623
+ ],
+ [
+ "▁luggage",
+ -12.349875450134277
+ ],
+ [
+ "numarul",
+ -12.349968910217285
+ ],
+ [
+ "▁contour",
+ -12.350014686584473
+ ],
+ [
+ "▁Ghost",
+ -12.350016593933105
+ ],
+ [
+ "Angaben",
+ -12.35012435913086
+ ],
+ [
+ "▁unemployment",
+ -12.350296020507812
+ ],
+ [
+ "▁rău",
+ -12.350380897521973
+ ],
+ [
+ "▁dispatch",
+ -12.350445747375488
+ ],
+ [
+ "investissement",
+ -12.350547790527344
+ ],
+ [
+ "▁passt",
+ -12.35057258605957
+ ],
+ [
+ "▁Germania",
+ -12.350578308105469
+ ],
+ [
+ "▁webpage",
+ -12.350651741027832
+ ],
+ [
+ "▁reservations",
+ -12.350688934326172
+ ],
+ [
+ "▁Kai",
+ -12.350743293762207
+ ],
+ [
+ "▁Cav",
+ -12.350890159606934
+ ],
+ [
+ "▁Patient",
+ -12.351109504699707
+ ],
+ [
+ "ер",
+ -12.351213455200195
+ ],
+ [
+ "▁Belle",
+ -12.351236343383789
+ ],
+ [
+ "▁Nashville",
+ -12.351296424865723
+ ],
+ [
+ "▁Talent",
+ -12.351332664489746
+ ],
+ [
+ "ouvrage",
+ -12.351364135742188
+ ],
+ [
+ "▁bekommt",
+ -12.351365089416504
+ ],
+ [
+ "USA",
+ -12.351430892944336
+ ],
+ [
+ "CES",
+ -12.351432800292969
+ ],
+ [
+ "▁Peru",
+ -12.351499557495117
+ ],
+ [
+ "▁erkennen",
+ -12.35153579711914
+ ],
+ [
+ "prinde",
+ -12.351569175720215
+ ],
+ [
+ "▁constitution",
+ -12.351922035217285
+ ],
+ [
+ "itatile",
+ -12.351998329162598
+ ],
+ [
+ "bah",
+ -12.352147102355957
+ ],
+ [
+ "▁avail",
+ -12.352148056030273
+ ],
+ [
+ "▁disponibile",
+ -12.352149963378906
+ ],
+ [
+ "hér",
+ -12.352258682250977
+ ],
+ [
+ "ол",
+ -12.352411270141602
+ ],
+ [
+ "▁startups",
+ -12.352435111999512
+ ],
+ [
+ "▁carton",
+ -12.352485656738281
+ ],
+ [
+ "▁Newsletter",
+ -12.35251235961914
+ ],
+ [
+ "éti",
+ -12.352560997009277
+ ],
+ [
+ "▁investigating",
+ -12.352779388427734
+ ],
+ [
+ "itul",
+ -12.352925300598145
+ ],
+ [
+ "touch",
+ -12.352962493896484
+ ],
+ [
+ "Sport",
+ -12.353137016296387
+ ],
+ [
+ "AME",
+ -12.353203773498535
+ ],
+ [
+ "MIN",
+ -12.353222846984863
+ ],
+ [
+ "metry",
+ -12.353371620178223
+ ],
+ [
+ "icy",
+ -12.353492736816406
+ ],
+ [
+ "▁Luna",
+ -12.35351848602295
+ ],
+ [
+ "▁asthma",
+ -12.353614807128906
+ ],
+ [
+ "▁conduc",
+ -12.35365104675293
+ ],
+ [
+ "▁Ari",
+ -12.35369873046875
+ ],
+ [
+ "trust",
+ -12.353832244873047
+ ],
+ [
+ "▁defines",
+ -12.353894233703613
+ ],
+ [
+ "▁Blend",
+ -12.353927612304688
+ ],
+ [
+ "azo",
+ -12.353989601135254
+ ],
+ [
+ "▁sweep",
+ -12.354169845581055
+ ],
+ [
+ "lope",
+ -12.354331016540527
+ ],
+ [
+ "ţinut",
+ -12.35439682006836
+ ],
+ [
+ "WD",
+ -12.354503631591797
+ ],
+ [
+ "▁appetite",
+ -12.354619979858398
+ ],
+ [
+ "▁Seed",
+ -12.354753494262695
+ ],
+ [
+ "Friend",
+ -12.354854583740234
+ ],
+ [
+ "▁repet",
+ -12.354876518249512
+ ],
+ [
+ "▁throat",
+ -12.354936599731445
+ ],
+ [
+ "philosoph",
+ -12.355141639709473
+ ],
+ [
+ "▁connaître",
+ -12.355156898498535
+ ],
+ [
+ "▁Counter",
+ -12.355299949645996
+ ],
+ [
+ "▁Anforderungen",
+ -12.35533332824707
+ ],
+ [
+ "▁Polit",
+ -12.355363845825195
+ ],
+ [
+ "▁Weather",
+ -12.3554048538208
+ ],
+ [
+ "bow",
+ -12.355423927307129
+ ],
+ [
+ "▁recreation",
+ -12.355484008789062
+ ],
+ [
+ "▁culinary",
+ -12.355571746826172
+ ],
+ [
+ "▁plage",
+ -12.355609893798828
+ ],
+ [
+ "▁Cruz",
+ -12.355659484863281
+ ],
+ [
+ "▁equip",
+ -12.355668067932129
+ ],
+ [
+ "▁Recent",
+ -12.355697631835938
+ ],
+ [
+ "LED",
+ -12.355767250061035
+ ],
+ [
+ "▁steak",
+ -12.355772972106934
+ ],
+ [
+ "▁belly",
+ -12.355880737304688
+ ],
+ [
+ "photo",
+ -12.356130599975586
+ ],
+ [
+ "▁lakes",
+ -12.35623836517334
+ ],
+ [
+ "▁intact",
+ -12.356287956237793
+ ],
+ [
+ "▁spiral",
+ -12.356386184692383
+ ],
+ [
+ "▁Billy",
+ -12.356468200683594
+ ],
+ [
+ "▁Understanding",
+ -12.356534957885742
+ ],
+ [
+ "▁Lay",
+ -12.356558799743652
+ ],
+ [
+ "▁roster",
+ -12.356632232666016
+ ],
+ [
+ "▁admire",
+ -12.356647491455078
+ ],
+ [
+ "▁android",
+ -12.356732368469238
+ ],
+ [
+ "▁technician",
+ -12.356734275817871
+ ],
+ [
+ "gène",
+ -12.356818199157715
+ ],
+ [
+ "motiv",
+ -12.356954574584961
+ ],
+ [
+ "▁Boat",
+ -12.356988906860352
+ ],
+ [
+ "▁genießen",
+ -12.357000350952148
+ ],
+ [
+ "▁Geschmack",
+ -12.357001304626465
+ ],
+ [
+ "▁heroes",
+ -12.3570556640625
+ ],
+ [
+ "▁1800",
+ -12.357137680053711
+ ],
+ [
+ "numeroase",
+ -12.35776138305664
+ ],
+ [
+ "▁anschließend",
+ -12.357802391052246
+ ],
+ [
+ "▁Spur",
+ -12.357813835144043
+ ],
+ [
+ "▁clarify",
+ -12.35784912109375
+ ],
+ [
+ "▁warmer",
+ -12.357889175415039
+ ],
+ [
+ "▁Ranch",
+ -12.357955932617188
+ ],
+ [
+ "▁simti",
+ -12.358024597167969
+ ],
+ [
+ "Thank",
+ -12.35838508605957
+ ],
+ [
+ "▁freight",
+ -12.358434677124023
+ ],
+ [
+ "▁administrators",
+ -12.358453750610352
+ ],
+ [
+ "Reg",
+ -12.358588218688965
+ ],
+ [
+ "Această",
+ -12.358670234680176
+ ],
+ [
+ "▁legume",
+ -12.358741760253906
+ ],
+ [
+ "▁utilizare",
+ -12.358786582946777
+ ],
+ [
+ "CON",
+ -12.358904838562012
+ ],
+ [
+ "urgi",
+ -12.358917236328125
+ ],
+ [
+ "▁Gesicht",
+ -12.358920097351074
+ ],
+ [
+ "▁counselor",
+ -12.358954429626465
+ ],
+ [
+ "▁mondiale",
+ -12.359009742736816
+ ],
+ [
+ "helm",
+ -12.359137535095215
+ ],
+ [
+ "▁Promo",
+ -12.359156608581543
+ ],
+ [
+ "▁Schweiz",
+ -12.35917854309082
+ ],
+ [
+ "Ich",
+ -12.35929012298584
+ ],
+ [
+ "▁intalni",
+ -12.359295845031738
+ ],
+ [
+ "▁Bloom",
+ -12.359318733215332
+ ],
+ [
+ "▁Score",
+ -12.359362602233887
+ ],
+ [
+ "▁Fruit",
+ -12.35944652557373
+ ],
+ [
+ "▁constraints",
+ -12.359447479248047
+ ],
+ [
+ "▁farmer",
+ -12.359745979309082
+ ],
+ [
+ "▁précise",
+ -12.359807014465332
+ ],
+ [
+ "evaluating",
+ -12.359868049621582
+ ],
+ [
+ "▁Period",
+ -12.359891891479492
+ ],
+ [
+ "byte",
+ -12.359893798828125
+ ],
+ [
+ "wah",
+ -12.360025405883789
+ ],
+ [
+ "Mac",
+ -12.360123634338379
+ ],
+ [
+ "iron",
+ -12.360197067260742
+ ],
+ [
+ "′",
+ -12.360337257385254
+ ],
+ [
+ "▁tehnic",
+ -12.360539436340332
+ ],
+ [
+ "▁legat",
+ -12.36054515838623
+ ],
+ [
+ "▁Pilot",
+ -12.360574722290039
+ ],
+ [
+ "▁Carpet",
+ -12.36064624786377
+ ],
+ [
+ "TEN",
+ -12.360812187194824
+ ],
+ [
+ "▁shareholders",
+ -12.36082649230957
+ ],
+ [
+ "vină",
+ -12.360880851745605
+ ],
+ [
+ "▁parole",
+ -12.360939979553223
+ ],
+ [
+ "ătă",
+ -12.360984802246094
+ ],
+ [
+ "bbing",
+ -12.361000061035156
+ ],
+ [
+ "▁switched",
+ -12.361002922058105
+ ],
+ [
+ "▁Petro",
+ -12.361010551452637
+ ],
+ [
+ "▁Vertrags",
+ -12.36111831665039
+ ],
+ [
+ "cham",
+ -12.361178398132324
+ ],
+ [
+ "wang",
+ -12.361284255981445
+ ],
+ [
+ "▁Bean",
+ -12.36139965057373
+ ],
+ [
+ "minister",
+ -12.361442565917969
+ ],
+ [
+ "▁Wu",
+ -12.361522674560547
+ ],
+ [
+ "▁Olympics",
+ -12.361539840698242
+ ],
+ [
+ "tipul",
+ -12.361542701721191
+ ],
+ [
+ "▁Citi",
+ -12.36166763305664
+ ],
+ [
+ "▁Fold",
+ -12.361873626708984
+ ],
+ [
+ "▁Partei",
+ -12.361940383911133
+ ],
+ [
+ "▁centrale",
+ -12.361984252929688
+ ],
+ [
+ "île",
+ -12.362032890319824
+ ],
+ [
+ "pflicht",
+ -12.362175941467285
+ ],
+ [
+ "heli",
+ -12.362398147583008
+ ],
+ [
+ "▁erwartet",
+ -12.362414360046387
+ ],
+ [
+ "▁oferta",
+ -12.362458229064941
+ ],
+ [
+ "▁NHS",
+ -12.36246395111084
+ ],
+ [
+ "annon",
+ -12.362570762634277
+ ],
+ [
+ "▁Rud",
+ -12.362701416015625
+ ],
+ [
+ "▁Stuttgart",
+ -12.362737655639648
+ ],
+ [
+ "▁rămas",
+ -12.362746238708496
+ ],
+ [
+ "▁eliminated",
+ -12.36275577545166
+ ],
+ [
+ "▁hiding",
+ -12.362797737121582
+ ],
+ [
+ "▁cadeau",
+ -12.362832069396973
+ ],
+ [
+ "▁mock",
+ -12.363115310668945
+ ],
+ [
+ "▁elder",
+ -12.363333702087402
+ ],
+ [
+ "▁Liz",
+ -12.363364219665527
+ ],
+ [
+ "aji",
+ -12.363544464111328
+ ],
+ [
+ "▁endlich",
+ -12.363653182983398
+ ],
+ [
+ "sufficient",
+ -12.363668441772461
+ ],
+ [
+ "▁zusätzliche",
+ -12.363712310791016
+ ],
+ [
+ "scient",
+ -12.363757133483887
+ ],
+ [
+ "▁Adjust",
+ -12.363883972167969
+ ],
+ [
+ "▁incentive",
+ -12.363945007324219
+ ],
+ [
+ "▁Papa",
+ -12.364012718200684
+ ],
+ [
+ "▁Pharma",
+ -12.364041328430176
+ ],
+ [
+ "▁conflicts",
+ -12.364107131958008
+ ],
+ [
+ "zählen",
+ -12.364113807678223
+ ],
+ [
+ "▁chien",
+ -12.364118576049805
+ ],
+ [
+ "KB",
+ -12.36413288116455
+ ],
+ [
+ "ultimi",
+ -12.364188194274902
+ ],
+ [
+ "▁Jul",
+ -12.36421012878418
+ ],
+ [
+ "▁Male",
+ -12.36422061920166
+ ],
+ [
+ "▁viewer",
+ -12.36427116394043
+ ],
+ [
+ "▁Sector",
+ -12.364328384399414
+ ],
+ [
+ "▁REAL",
+ -12.364344596862793
+ ],
+ [
+ "▁arbitr",
+ -12.36436939239502
+ ],
+ [
+ "resistant",
+ -12.364399909973145
+ ],
+ [
+ "▁Bristol",
+ -12.364423751831055
+ ],
+ [
+ "▁shy",
+ -12.364540100097656
+ ],
+ [
+ "SW",
+ -12.364593505859375
+ ],
+ [
+ "▁Kirk",
+ -12.36460018157959
+ ],
+ [
+ "centrul",
+ -12.364653587341309
+ ],
+ [
+ "▁Venezuela",
+ -12.364657402038574
+ ],
+ [
+ "▁communicating",
+ -12.364657402038574
+ ],
+ [
+ "▁Chemical",
+ -12.364663124084473
+ ],
+ [
+ "▁surprises",
+ -12.364843368530273
+ ],
+ [
+ "▁Jamie",
+ -12.364933967590332
+ ],
+ [
+ "▁Heavy",
+ -12.364965438842773
+ ],
+ [
+ "▁turnover",
+ -12.36498737335205
+ ],
+ [
+ "▁étudiants",
+ -12.365114212036133
+ ],
+ [
+ "welcher",
+ -12.365124702453613
+ ],
+ [
+ "▁preturi",
+ -12.365200996398926
+ ],
+ [
+ "▁Mono",
+ -12.365283966064453
+ ],
+ [
+ "▁paddle",
+ -12.365309715270996
+ ],
+ [
+ "▁accountability",
+ -12.365364074707031
+ ],
+ [
+ "OUS",
+ -12.365592956542969
+ ],
+ [
+ "▁marketers",
+ -12.365762710571289
+ ],
+ [
+ "fection",
+ -12.365900993347168
+ ],
+ [
+ "▁Outside",
+ -12.365921020507812
+ ],
+ [
+ "▁Jefferson",
+ -12.366114616394043
+ ],
+ [
+ "oaie",
+ -12.36617660522461
+ ],
+ [
+ "tenue",
+ -12.366275787353516
+ ],
+ [
+ "HU",
+ -12.366329193115234
+ ],
+ [
+ "Très",
+ -12.36639404296875
+ ],
+ [
+ "valoarea",
+ -12.36642837524414
+ ],
+ [
+ "103",
+ -12.366482734680176
+ ],
+ [
+ "▁Privacy",
+ -12.366580963134766
+ ],
+ [
+ "▁Leistungen",
+ -12.366598129272461
+ ],
+ [
+ "(3)",
+ -12.36662483215332
+ ],
+ [
+ "▁études",
+ -12.366734504699707
+ ],
+ [
+ "sko",
+ -12.366750717163086
+ ],
+ [
+ "drum",
+ -12.366822242736816
+ ],
+ [
+ "▁lamb",
+ -12.366842269897461
+ ],
+ [
+ "▁nicio",
+ -12.367094993591309
+ ],
+ [
+ "▁NATO",
+ -12.367104530334473
+ ],
+ [
+ "▁Freitag",
+ -12.367178916931152
+ ],
+ [
+ "▁precedent",
+ -12.367178916931152
+ ],
+ [
+ "▁partenaires",
+ -12.367202758789062
+ ],
+ [
+ "▁companiei",
+ -12.367234230041504
+ ],
+ [
+ "▁Plaza",
+ -12.367249488830566
+ ],
+ [
+ "▁disruption",
+ -12.367274284362793
+ ],
+ [
+ "▁violations",
+ -12.367338180541992
+ ],
+ [
+ "▁Reference",
+ -12.367446899414062
+ ],
+ [
+ "▁habitants",
+ -12.36770248413086
+ ],
+ [
+ "▁compost",
+ -12.36776351928711
+ ],
+ [
+ "▁citoyen",
+ -12.367785453796387
+ ],
+ [
+ "▁Historical",
+ -12.367857933044434
+ ],
+ [
+ "vollen",
+ -12.36793327331543
+ ],
+ [
+ "▁Eck",
+ -12.36815357208252
+ ],
+ [
+ "▁lumii",
+ -12.368180274963379
+ ],
+ [
+ "▁reusit",
+ -12.368278503417969
+ ],
+ [
+ "genic",
+ -12.368307113647461
+ ],
+ [
+ "Why",
+ -12.368436813354492
+ ],
+ [
+ "ASE",
+ -12.368474006652832
+ ],
+ [
+ "▁athlete",
+ -12.36854076385498
+ ],
+ [
+ "▁Spitze",
+ -12.368559837341309
+ ],
+ [
+ "▁schimbat",
+ -12.368566513061523
+ ],
+ [
+ "▁anonymous",
+ -12.368850708007812
+ ],
+ [
+ "jedes",
+ -12.368856430053711
+ ],
+ [
+ "exclu",
+ -12.368874549865723
+ ],
+ [
+ "factor",
+ -12.369199752807617
+ ],
+ [
+ "▁Dezember",
+ -12.369231224060059
+ ],
+ [
+ "▁scientist",
+ -12.369373321533203
+ ],
+ [
+ "▁likelihood",
+ -12.36947250366211
+ ],
+ [
+ "▁Rhode",
+ -12.369488716125488
+ ],
+ [
+ "▁Balance",
+ -12.369521141052246
+ ],
+ [
+ "istoria",
+ -12.36959457397461
+ ],
+ [
+ "▁Neil",
+ -12.369780540466309
+ ],
+ [
+ "▁bush",
+ -12.369919776916504
+ ],
+ [
+ "▁Ergebnisse",
+ -12.369935989379883
+ ],
+ [
+ "▁Sinn",
+ -12.369956016540527
+ ],
+ [
+ "▁spezielle",
+ -12.370128631591797
+ ],
+ [
+ "▁jucat",
+ -12.37015438079834
+ ],
+ [
+ "▁spite",
+ -12.370179176330566
+ ],
+ [
+ "▁Ultimate",
+ -12.370365142822266
+ ],
+ [
+ "▁fructe",
+ -12.370401382446289
+ ],
+ [
+ "▁asleep",
+ -12.370441436767578
+ ],
+ [
+ "▁Goal",
+ -12.370539665222168
+ ],
+ [
+ "▁PAR",
+ -12.370631217956543
+ ],
+ [
+ "▁rows",
+ -12.370705604553223
+ ],
+ [
+ "▁Fol",
+ -12.3709135055542
+ ],
+ [
+ "▁durata",
+ -12.370945930480957
+ ],
+ [
+ "▁traditionnel",
+ -12.37100887298584
+ ],
+ [
+ "▁tema",
+ -12.37122917175293
+ ],
+ [
+ "▁crédit",
+ -12.371232986450195
+ ],
+ [
+ "smallest",
+ -12.371358871459961
+ ],
+ [
+ "▁amino",
+ -12.371358871459961
+ ],
+ [
+ "▁elephant",
+ -12.371405601501465
+ ],
+ [
+ "▁tubes",
+ -12.371685028076172
+ ],
+ [
+ "▁Verwendung",
+ -12.371719360351562
+ ],
+ [
+ "▁Excellence",
+ -12.371889114379883
+ ],
+ [
+ "▁utilities",
+ -12.371962547302246
+ ],
+ [
+ "frau",
+ -12.372111320495605
+ ],
+ [
+ "▁poze",
+ -12.3721342086792
+ ],
+ [
+ "août",
+ -12.372307777404785
+ ],
+ [
+ "ango",
+ -12.372514724731445
+ ],
+ [
+ "give",
+ -12.372532844543457
+ ],
+ [
+ "▁appelé",
+ -12.372576713562012
+ ],
+ [
+ "▁yeast",
+ -12.372671127319336
+ ],
+ [
+ "▁enrollment",
+ -12.372676849365234
+ ],
+ [
+ "organiz",
+ -12.3727445602417
+ ],
+ [
+ "▁asociat",
+ -12.372753143310547
+ ],
+ [
+ "▁cattle",
+ -12.372772216796875
+ ],
+ [
+ "▁Solution",
+ -12.372798919677734
+ ],
+ [
+ "evoke",
+ -12.372807502746582
+ ],
+ [
+ "▁Hampshire",
+ -12.372857093811035
+ ],
+ [
+ "▁yeah",
+ -12.372878074645996
+ ],
+ [
+ "▁Argentina",
+ -12.372928619384766
+ ],
+ [
+ "▁abnormal",
+ -12.373022079467773
+ ],
+ [
+ "▁Heights",
+ -12.373082160949707
+ ],
+ [
+ "▁Mitchell",
+ -12.373099327087402
+ ],
+ [
+ "▁Quad",
+ -12.373350143432617
+ ],
+ [
+ "▁textures",
+ -12.373382568359375
+ ],
+ [
+ "▁coalition",
+ -12.373384475708008
+ ],
+ [
+ "▁dataset",
+ -12.37338924407959
+ ],
+ [
+ "World",
+ -12.373438835144043
+ ],
+ [
+ "ständ",
+ -12.373456001281738
+ ],
+ [
+ "▁groove",
+ -12.373476028442383
+ ],
+ [
+ "▁emotionally",
+ -12.373562812805176
+ ],
+ [
+ "▁preciz",
+ -12.373636245727539
+ ],
+ [
+ "kte",
+ -12.373741149902344
+ ],
+ [
+ "berechtigt",
+ -12.373828887939453
+ ],
+ [
+ "▁1971",
+ -12.373888969421387
+ ],
+ [
+ "grandes",
+ -12.373907089233398
+ ],
+ [
+ "▁Broadway",
+ -12.37391185760498
+ ],
+ [
+ "▁comunicat",
+ -12.373994827270508
+ ],
+ [
+ "nui",
+ -12.37402629852295
+ ],
+ [
+ "GER",
+ -12.374079704284668
+ ],
+ [
+ "pick",
+ -12.374125480651855
+ ],
+ [
+ "inscrit",
+ -12.37414264678955
+ ],
+ [
+ "▁Gross",
+ -12.374258995056152
+ ],
+ [
+ "▁McDonald",
+ -12.374310493469238
+ ],
+ [
+ "▁Zero",
+ -12.374330520629883
+ ],
+ [
+ "▁Halb",
+ -12.374341011047363
+ ],
+ [
+ "▁caractère",
+ -12.374553680419922
+ ],
+ [
+ "▁doctrine",
+ -12.374553680419922
+ ],
+ [
+ "▁Sinne",
+ -12.37458610534668
+ ],
+ [
+ "MLS",
+ -12.374594688415527
+ ],
+ [
+ "▁réel",
+ -12.374759674072266
+ ],
+ [
+ "▁Ful",
+ -12.37476921081543
+ ],
+ [
+ "limiting",
+ -12.37483024597168
+ ],
+ [
+ "▁Gan",
+ -12.374870300292969
+ ],
+ [
+ "▁exclude",
+ -12.37490463256836
+ ],
+ [
+ "imba",
+ -12.374974250793457
+ ],
+ [
+ "rolul",
+ -12.374991416931152
+ ],
+ [
+ "▁veggies",
+ -12.375059127807617
+ ],
+ [
+ "▁fasci",
+ -12.375092506408691
+ ],
+ [
+ "▁oval",
+ -12.375173568725586
+ ],
+ [
+ "▁contacter",
+ -12.375221252441406
+ ],
+ [
+ "▁linking",
+ -12.375279426574707
+ ],
+ [
+ "▁knit",
+ -12.375308990478516
+ ],
+ [
+ "▁enroll",
+ -12.375504493713379
+ ],
+ [
+ "▁dédié",
+ -12.375533103942871
+ ],
+ [
+ "▁renting",
+ -12.375541687011719
+ ],
+ [
+ "▁genera",
+ -12.37567138671875
+ ],
+ [
+ "citing",
+ -12.375691413879395
+ ],
+ [
+ "▁bend",
+ -12.375700950622559
+ ],
+ [
+ "guin",
+ -12.375752449035645
+ ],
+ [
+ "▁caregiver",
+ -12.375768661499023
+ ],
+ [
+ "▁könnt",
+ -12.375791549682617
+ ],
+ [
+ "▁Scripture",
+ -12.375795364379883
+ ],
+ [
+ "▁Mic",
+ -12.375899314880371
+ ],
+ [
+ "▁Denmark",
+ -12.37590217590332
+ ],
+ [
+ "▁qualifying",
+ -12.375917434692383
+ ],
+ [
+ "▁costumes",
+ -12.375958442687988
+ ],
+ [
+ "▁dwelling",
+ -12.37601375579834
+ ],
+ [
+ "▁recrut",
+ -12.376099586486816
+ ],
+ [
+ "▁bedding",
+ -12.37618637084961
+ ],
+ [
+ "gesprochen",
+ -12.376253128051758
+ ],
+ [
+ "▁editors",
+ -12.376386642456055
+ ],
+ [
+ "/12",
+ -12.37657642364502
+ ],
+ [
+ "▁cumparat",
+ -12.376583099365234
+ ],
+ [
+ "fiction",
+ -12.376730918884277
+ ],
+ [
+ "▁spinal",
+ -12.376740455627441
+ ],
+ [
+ "▁pathway",
+ -12.376799583435059
+ ],
+ [
+ "▁vârst",
+ -12.37683391571045
+ ],
+ [
+ "mba",
+ -12.376874923706055
+ ],
+ [
+ "▁enthusiastic",
+ -12.37692642211914
+ ],
+ [
+ "▁Watt",
+ -12.37697982788086
+ ],
+ [
+ "symptom",
+ -12.376992225646973
+ ],
+ [
+ "▁pup",
+ -12.37712287902832
+ ],
+ [
+ "▁glorious",
+ -12.377225875854492
+ ],
+ [
+ "▁fața",
+ -12.377228736877441
+ ],
+ [
+ "▁prohibited",
+ -12.377256393432617
+ ],
+ [
+ "vergleich",
+ -12.377286911010742
+ ],
+ [
+ "▁suspected",
+ -12.377334594726562
+ ],
+ [
+ "▁Railway",
+ -12.377381324768066
+ ],
+ [
+ "▁Aujourd",
+ -12.377469062805176
+ ],
+ [
+ "▁Patients",
+ -12.377476692199707
+ ],
+ [
+ "▁séance",
+ -12.377501487731934
+ ],
+ [
+ "▁contraire",
+ -12.377503395080566
+ ],
+ [
+ "▁cuvânt",
+ -12.37771224975586
+ ],
+ [
+ "▁trotzdem",
+ -12.37773609161377
+ ],
+ [
+ "émission",
+ -12.377795219421387
+ ],
+ [
+ "▁bore",
+ -12.37782096862793
+ ],
+ [
+ "▁safeguard",
+ -12.377851486206055
+ ],
+ [
+ "▁galleries",
+ -12.37820053100586
+ ],
+ [
+ "cron",
+ -12.378268241882324
+ ],
+ [
+ "▁Rica",
+ -12.378335952758789
+ ],
+ [
+ "fläche",
+ -12.37839126586914
+ ],
+ [
+ "▁Slow",
+ -12.37842082977295
+ ],
+ [
+ "▁vara",
+ -12.378549575805664
+ ],
+ [
+ "▁Swan",
+ -12.378564834594727
+ ],
+ [
+ "▁compounds",
+ -12.378564834594727
+ ],
+ [
+ "▁Slo",
+ -12.378621101379395
+ ],
+ [
+ "▁accommodations",
+ -12.378621101379395
+ ],
+ [
+ "▁Putin",
+ -12.378708839416504
+ ],
+ [
+ "▁undertaken",
+ -12.378767967224121
+ ],
+ [
+ "▁prépar",
+ -12.37879467010498
+ ],
+ [
+ "▁gandi",
+ -12.37881088256836
+ ],
+ [
+ "sediul",
+ -12.378924369812012
+ ],
+ [
+ "▁Nathan",
+ -12.379143714904785
+ ],
+ [
+ "▁fountain",
+ -12.379173278808594
+ ],
+ [
+ "▁mère",
+ -12.379194259643555
+ ],
+ [
+ "fatty",
+ -12.379201889038086
+ ],
+ [
+ "▁concentrated",
+ -12.379241943359375
+ ],
+ [
+ "richtung",
+ -12.379300117492676
+ ],
+ [
+ "▁appropriately",
+ -12.37955379486084
+ ],
+ [
+ "107",
+ -12.379631996154785
+ ],
+ [
+ "▁shark",
+ -12.379735946655273
+ ],
+ [
+ "▁Topic",
+ -12.379867553710938
+ ],
+ [
+ "▁Ausstellung",
+ -12.379880905151367
+ ],
+ [
+ "▁SUA",
+ -12.380267143249512
+ ],
+ [
+ "SER",
+ -12.380359649658203
+ ],
+ [
+ "▁Nicole",
+ -12.38039779663086
+ ],
+ [
+ "▁utilisateurs",
+ -12.380620956420898
+ ],
+ [
+ "▁Brazilian",
+ -12.380753517150879
+ ],
+ [
+ "▁continut",
+ -12.380865097045898
+ ],
+ [
+ "▁sanatate",
+ -12.380881309509277
+ ],
+ [
+ "faudra",
+ -12.380882263183594
+ ],
+ [
+ "nahm",
+ -12.380938529968262
+ ],
+ [
+ "▁Specific",
+ -12.381153106689453
+ ],
+ [
+ "aiba",
+ -12.381199836730957
+ ],
+ [
+ "cepând",
+ -12.381296157836914
+ ],
+ [
+ "▁Beer",
+ -12.381366729736328
+ ],
+ [
+ "roni",
+ -12.381616592407227
+ ],
+ [
+ "kay",
+ -12.381636619567871
+ ],
+ [
+ "▁gravity",
+ -12.381844520568848
+ ],
+ [
+ "▁verfügt",
+ -12.381856918334961
+ ],
+ [
+ "7:30",
+ -12.381878852844238
+ ],
+ [
+ "▁Players",
+ -12.381945610046387
+ ],
+ [
+ "▁Industries",
+ -12.38198184967041
+ ],
+ [
+ "punkte",
+ -12.382119178771973
+ ],
+ [
+ "▁yacht",
+ -12.382135391235352
+ ],
+ [
+ "-04",
+ -12.382149696350098
+ ],
+ [
+ "onné",
+ -12.382192611694336
+ ],
+ [
+ "▁Cards",
+ -12.382221221923828
+ ],
+ [
+ "▁fete",
+ -12.382420539855957
+ ],
+ [
+ "breaking",
+ -12.38257884979248
+ ],
+ [
+ "baum",
+ -12.382621765136719
+ ],
+ [
+ "nada",
+ -12.382651329040527
+ ],
+ [
+ "▁geplant",
+ -12.382750511169434
+ ],
+ [
+ "genuinely",
+ -12.382766723632812
+ ],
+ [
+ "talk",
+ -12.382871627807617
+ ],
+ [
+ "▁disadvantage",
+ -12.382920265197754
+ ],
+ [
+ "▁shutter",
+ -12.383003234863281
+ ],
+ [
+ "virus",
+ -12.38302230834961
+ ],
+ [
+ "▁cricket",
+ -12.38308048248291
+ ],
+ [
+ "▁comenzi",
+ -12.383102416992188
+ ],
+ [
+ "hier",
+ -12.383170127868652
+ ],
+ [
+ "▁aufzu",
+ -12.383198738098145
+ ],
+ [
+ "▁Rez",
+ -12.38321304321289
+ ],
+ [
+ "▁conclusions",
+ -12.383329391479492
+ ],
+ [
+ "▁Wang",
+ -12.383509635925293
+ ],
+ [
+ "Darüber",
+ -12.383524894714355
+ ],
+ [
+ "▁CSS",
+ -12.383573532104492
+ ],
+ [
+ "CW",
+ -12.383780479431152
+ ],
+ [
+ "▁Chr",
+ -12.383790969848633
+ ],
+ [
+ "▁traded",
+ -12.383843421936035
+ ],
+ [
+ "▁Schon",
+ -12.384265899658203
+ ],
+ [
+ "mped",
+ -12.38429069519043
+ ],
+ [
+ "▁alloy",
+ -12.384385108947754
+ ],
+ [
+ "AVE",
+ -12.38451099395752
+ ],
+ [
+ "▁imagery",
+ -12.384542465209961
+ ],
+ [
+ "▁resurse",
+ -12.38479995727539
+ ],
+ [
+ "▁Thunder",
+ -12.384834289550781
+ ],
+ [
+ "▁schimbare",
+ -12.384860038757324
+ ],
+ [
+ "▁Youtube",
+ -12.38499927520752
+ ],
+ [
+ "▁Monster",
+ -12.385189056396484
+ ],
+ [
+ "phil",
+ -12.385234832763672
+ ],
+ [
+ "▁bébé",
+ -12.385284423828125
+ ],
+ [
+ "Creating",
+ -12.385428428649902
+ ],
+ [
+ "ănă",
+ -12.385466575622559
+ ],
+ [
+ "▁Staat",
+ -12.385504722595215
+ ],
+ [
+ "adică",
+ -12.385531425476074
+ ],
+ [
+ "▁boyfriend",
+ -12.385552406311035
+ ],
+ [
+ "▁Winner",
+ -12.385594367980957
+ ],
+ [
+ "▁disputes",
+ -12.385653495788574
+ ],
+ [
+ "▁lush",
+ -12.3856840133667
+ ],
+ [
+ "▁CMS",
+ -12.385719299316406
+ ],
+ [
+ "▁locaux",
+ -12.385725021362305
+ ],
+ [
+ "▁Verfahren",
+ -12.38576889038086
+ ],
+ [
+ "▁Café",
+ -12.385786056518555
+ ],
+ [
+ "▁Vorstand",
+ -12.385870933532715
+ ],
+ [
+ "▁lucrat",
+ -12.385960578918457
+ ],
+ [
+ "▁Root",
+ -12.38602352142334
+ ],
+ [
+ "▁decis",
+ -12.386059761047363
+ ],
+ [
+ "▁Shadow",
+ -12.386062622070312
+ ],
+ [
+ "▁countryside",
+ -12.386067390441895
+ ],
+ [
+ "▁analiza",
+ -12.386114120483398
+ ],
+ [
+ "obos",
+ -12.38616943359375
+ ],
+ [
+ "opera",
+ -12.386175155639648
+ ],
+ [
+ "actu",
+ -12.386207580566406
+ ],
+ [
+ "▁Songs",
+ -12.3864164352417
+ ],
+ [
+ "reifen",
+ -12.38648509979248
+ ],
+ [
+ "▁hilft",
+ -12.386650085449219
+ ],
+ [
+ "region",
+ -12.386727333068848
+ ],
+ [
+ "▁categoria",
+ -12.387001991271973
+ ],
+ [
+ "capturing",
+ -12.38701343536377
+ ],
+ [
+ "▁1967",
+ -12.387025833129883
+ ],
+ [
+ "▁optimized",
+ -12.387032508850098
+ ],
+ [
+ "▁Dim",
+ -12.387353897094727
+ ],
+ [
+ "▁adapté",
+ -12.387447357177734
+ ],
+ [
+ "zeichnet",
+ -12.387524604797363
+ ],
+ [
+ "▁strada",
+ -12.387625694274902
+ ],
+ [
+ "fulness",
+ -12.38774585723877
+ ],
+ [
+ "▁technically",
+ -12.38774585723877
+ ],
+ [
+ "▁marker",
+ -12.387757301330566
+ ],
+ [
+ "▁vizita",
+ -12.387808799743652
+ ],
+ [
+ "▁imperative",
+ -12.387986183166504
+ ],
+ [
+ "▁pensé",
+ -12.38802719116211
+ ],
+ [
+ "▁drilling",
+ -12.388030052185059
+ ],
+ [
+ "ISA",
+ -12.38818073272705
+ ],
+ [
+ "▁Massage",
+ -12.388201713562012
+ ],
+ [
+ "▁Terry",
+ -12.388238906860352
+ ],
+ [
+ "▁pourtant",
+ -12.38835334777832
+ ],
+ [
+ "▁declaration",
+ -12.388440132141113
+ ],
+ [
+ "▁instructors",
+ -12.388453483581543
+ ],
+ [
+ "Eventually",
+ -12.38847827911377
+ ],
+ [
+ "▁banned",
+ -12.38847827911377
+ ],
+ [
+ "MAT",
+ -12.388520240783691
+ ],
+ [
+ "▁medici",
+ -12.38856315612793
+ ],
+ [
+ "▁Warm",
+ -12.388615608215332
+ ],
+ [
+ "▁trec",
+ -12.388731002807617
+ ],
+ [
+ "▁ecran",
+ -12.388763427734375
+ ],
+ [
+ "▁goat",
+ -12.388838768005371
+ ],
+ [
+ "▁manipulation",
+ -12.388850212097168
+ ],
+ [
+ "▁mayor",
+ -12.388898849487305
+ ],
+ [
+ "▁unterwegs",
+ -12.388975143432617
+ ],
+ [
+ "▁journals",
+ -12.3890380859375
+ ],
+ [
+ "▁hedge",
+ -12.389239311218262
+ ],
+ [
+ "Merc",
+ -12.389300346374512
+ ],
+ [
+ "▁joueurs",
+ -12.389411926269531
+ ],
+ [
+ "▁Religion",
+ -12.3894624710083
+ ],
+ [
+ "▁Mountains",
+ -12.389477729797363
+ ],
+ [
+ "▁renewed",
+ -12.389497756958008
+ ],
+ [
+ "▁Limit",
+ -12.389543533325195
+ ],
+ [
+ "ikea",
+ -12.389771461486816
+ ],
+ [
+ "▁utiliza",
+ -12.38977336883545
+ ],
+ [
+ "sogenannte",
+ -12.389808654785156
+ ],
+ [
+ "0.2",
+ -12.389836311340332
+ ],
+ [
+ "▁Organ",
+ -12.38987922668457
+ ],
+ [
+ "▁Shakespeare",
+ -12.389952659606934
+ ],
+ [
+ "▁Maintenance",
+ -12.38995361328125
+ ],
+ [
+ "▁Wärme",
+ -12.389954566955566
+ ],
+ [
+ "▁Northwest",
+ -12.390060424804688
+ ],
+ [
+ "▁numit",
+ -12.390106201171875
+ ],
+ [
+ "▁mica",
+ -12.390165328979492
+ ],
+ [
+ "turm",
+ -12.390168190002441
+ ],
+ [
+ "▁motivate",
+ -12.390250205993652
+ ],
+ [
+ "▁Staats",
+ -12.390355110168457
+ ],
+ [
+ "optimum",
+ -12.390487670898438
+ ],
+ [
+ "▁sortir",
+ -12.390546798706055
+ ],
+ [
+ "▁Asset",
+ -12.390555381774902
+ ],
+ [
+ "▁hervorragend",
+ -12.390692710876465
+ ],
+ [
+ "▁commentary",
+ -12.39071273803711
+ ],
+ [
+ "▁actuellement",
+ -12.390732765197754
+ ],
+ [
+ "NER",
+ -12.390765190124512
+ ],
+ [
+ "NL",
+ -12.390789985656738
+ ],
+ [
+ "ritt",
+ -12.390803337097168
+ ],
+ [
+ "▁Wirtschafts",
+ -12.390813827514648
+ ],
+ [
+ "träger",
+ -12.390840530395508
+ ],
+ [
+ "▁Versand",
+ -12.390870094299316
+ ],
+ [
+ "▁nostri",
+ -12.390953063964844
+ ],
+ [
+ "▁enorm",
+ -12.391227722167969
+ ],
+ [
+ "▁whale",
+ -12.391260147094727
+ ],
+ [
+ "▁Aufgabe",
+ -12.391277313232422
+ ],
+ [
+ "▁unfair",
+ -12.391291618347168
+ ],
+ [
+ "▁Cord",
+ -12.391315460205078
+ ],
+ [
+ "incorporating",
+ -12.39134693145752
+ ],
+ [
+ "luck",
+ -12.39157772064209
+ ],
+ [
+ "Afrique",
+ -12.39168643951416
+ ],
+ [
+ "▁coated",
+ -12.391857147216797
+ ],
+ [
+ "▁india",
+ -12.391908645629883
+ ],
+ [
+ "▁temporarily",
+ -12.39193058013916
+ ],
+ [
+ "▁ciuda",
+ -12.392097473144531
+ ],
+ [
+ "▁coral",
+ -12.392184257507324
+ ],
+ [
+ "▁wirkt",
+ -12.392203330993652
+ ],
+ [
+ "▁folding",
+ -12.392309188842773
+ ],
+ [
+ "wichtigsten",
+ -12.392398834228516
+ ],
+ [
+ "impacted",
+ -12.392422676086426
+ ],
+ [
+ "▁wählen",
+ -12.392423629760742
+ ],
+ [
+ "▁differentiate",
+ -12.392492294311523
+ ],
+ [
+ "▁froid",
+ -12.392544746398926
+ ],
+ [
+ "▁hug",
+ -12.39255142211914
+ ],
+ [
+ "▁construi",
+ -12.39255428314209
+ ],
+ [
+ "▁membru",
+ -12.392603874206543
+ ],
+ [
+ "▁masculin",
+ -12.392667770385742
+ ],
+ [
+ "partisan",
+ -12.392711639404297
+ ],
+ [
+ "▁schimba",
+ -12.392725944519043
+ ],
+ [
+ "▁economies",
+ -12.392827987670898
+ ],
+ [
+ "▁Abraham",
+ -12.392914772033691
+ ],
+ [
+ "wesen",
+ -12.393013954162598
+ ],
+ [
+ "enia",
+ -12.393026351928711
+ ],
+ [
+ "▁answering",
+ -12.393080711364746
+ ],
+ [
+ "▁activități",
+ -12.39309024810791
+ ],
+ [
+ "▁mémoire",
+ -12.393160820007324
+ ],
+ [
+ "▁versucht",
+ -12.393305778503418
+ ],
+ [
+ "ember",
+ -12.39333438873291
+ ],
+ [
+ "▁instala",
+ -12.39334774017334
+ ],
+ [
+ "▁eligibility",
+ -12.393407821655273
+ ],
+ [
+ "▁enjoyment",
+ -12.393409729003906
+ ],
+ [
+ "▁Arme",
+ -12.39350414276123
+ ],
+ [
+ "although",
+ -12.393534660339355
+ ],
+ [
+ "▁encompass",
+ -12.393596649169922
+ ],
+ [
+ "▁zufrieden",
+ -12.393658638000488
+ ],
+ [
+ "Script",
+ -12.393691062927246
+ ],
+ [
+ "KG",
+ -12.39385986328125
+ ],
+ [
+ "▁adhesive",
+ -12.393902778625488
+ ],
+ [
+ "▁Verkehrs",
+ -12.393908500671387
+ ],
+ [
+ "▁monitored",
+ -12.394103050231934
+ ],
+ [
+ "▁Conservation",
+ -12.394148826599121
+ ],
+ [
+ "hav",
+ -12.394156455993652
+ ],
+ [
+ "▁Above",
+ -12.394174575805664
+ ],
+ [
+ "▁Former",
+ -12.394241333007812
+ ],
+ [
+ "▁Certain",
+ -12.394250869750977
+ ],
+ [
+ "saving",
+ -12.394311904907227
+ ],
+ [
+ "▁Pun",
+ -12.394390106201172
+ ],
+ [
+ "▁awkward",
+ -12.394397735595703
+ ],
+ [
+ "▁Pretty",
+ -12.394410133361816
+ ],
+ [
+ "▁scanning",
+ -12.394417762756348
+ ],
+ [
+ "layer",
+ -12.394527435302734
+ ],
+ [
+ "motor",
+ -12.39453125
+ ],
+ [
+ "▁beginnt",
+ -12.39455795288086
+ ],
+ [
+ "▁affiliated",
+ -12.394681930541992
+ ],
+ [
+ "▁archives",
+ -12.394686698913574
+ ],
+ [
+ "▁sunshine",
+ -12.394892692565918
+ ],
+ [
+ "kha",
+ -12.394988059997559
+ ],
+ [
+ "▁investigated",
+ -12.395149230957031
+ ],
+ [
+ "▁fantas",
+ -12.395277976989746
+ ],
+ [
+ "▁united",
+ -12.395355224609375
+ ],
+ [
+ "allegedly",
+ -12.395373344421387
+ ],
+ [
+ "▁Eugen",
+ -12.3955078125
+ ],
+ [
+ "▁proprie",
+ -12.395843505859375
+ ],
+ [
+ "uca",
+ -12.396183013916016
+ ],
+ [
+ "DES",
+ -12.396187782287598
+ ],
+ [
+ "ştii",
+ -12.396190643310547
+ ],
+ [
+ "▁Running",
+ -12.39620590209961
+ ],
+ [
+ "lbstverständlich",
+ -12.396248817443848
+ ],
+ [
+ "index",
+ -12.396300315856934
+ ],
+ [
+ "▁studiu",
+ -12.396512031555176
+ ],
+ [
+ "URE",
+ -12.396553039550781
+ ],
+ [
+ "gültig",
+ -12.396627426147461
+ ],
+ [
+ "▁lundi",
+ -12.396649360656738
+ ],
+ [
+ "▁Zucker",
+ -12.396650314331055
+ ],
+ [
+ "▁positively",
+ -12.396721839904785
+ ],
+ [
+ "folgenden",
+ -12.396758079528809
+ ],
+ [
+ "anță",
+ -12.396800994873047
+ ],
+ [
+ "▁clan",
+ -12.396866798400879
+ ],
+ [
+ "▁literacy",
+ -12.396879196166992
+ ],
+ [
+ "▁ober",
+ -12.39699935913086
+ ],
+ [
+ "John",
+ -12.397003173828125
+ ],
+ [
+ "greg",
+ -12.39700984954834
+ ],
+ [
+ "▁titlu",
+ -12.397049903869629
+ ],
+ [
+ "▁ţări",
+ -12.39707088470459
+ ],
+ [
+ "Bra",
+ -12.397100448608398
+ ],
+ [
+ "▁Evans",
+ -12.397164344787598
+ ],
+ [
+ "modern",
+ -12.397172927856445
+ ],
+ [
+ "▁hauteur",
+ -12.397353172302246
+ ],
+ [
+ "refers",
+ -12.397416114807129
+ ],
+ [
+ "▁plasma",
+ -12.397575378417969
+ ],
+ [
+ "▁optic",
+ -12.397595405578613
+ ],
+ [
+ "▁shampoo",
+ -12.397619247436523
+ ],
+ [
+ "▁cheek",
+ -12.397727966308594
+ ],
+ [
+ "opted",
+ -12.397741317749023
+ ],
+ [
+ "▁persönlich",
+ -12.397832870483398
+ ],
+ [
+ "▁1945",
+ -12.398118019104004
+ ],
+ [
+ "ICI",
+ -12.398193359375
+ ],
+ [
+ "biotic",
+ -12.398222923278809
+ ],
+ [
+ "▁Beruf",
+ -12.398372650146484
+ ],
+ [
+ "▁trez",
+ -12.398383140563965
+ ],
+ [
+ "▁diploma",
+ -12.398388862609863
+ ],
+ [
+ "nahmen",
+ -12.398421287536621
+ ],
+ [
+ "▁curl",
+ -12.398625373840332
+ ],
+ [
+ "▁agricole",
+ -12.398824691772461
+ ],
+ [
+ "▁recomand",
+ -12.398844718933105
+ ],
+ [
+ "▁pediatric",
+ -12.398862838745117
+ ],
+ [
+ "Fiecare",
+ -12.39887523651123
+ ],
+ [
+ "Anlage",
+ -12.398906707763672
+ ],
+ [
+ "weiß",
+ -12.398974418640137
+ ],
+ [
+ "elecommunication",
+ -12.39898681640625
+ ],
+ [
+ "hog",
+ -12.399184226989746
+ ],
+ [
+ "▁Stamp",
+ -12.399364471435547
+ ],
+ [
+ "▁Tipp",
+ -12.399369239807129
+ ],
+ [
+ "▁kindness",
+ -12.399415969848633
+ ],
+ [
+ "▁Marina",
+ -12.399577140808105
+ ],
+ [
+ "▁Gleich",
+ -12.39963436126709
+ ],
+ [
+ "▁grij",
+ -12.39970588684082
+ ],
+ [
+ "▁desperate",
+ -12.39974594116211
+ ],
+ [
+ "▁recordings",
+ -12.399842262268066
+ ],
+ [
+ "▁neglect",
+ -12.399861335754395
+ ],
+ [
+ "▁inherent",
+ -12.400035858154297
+ ],
+ [
+ "▁Rezept",
+ -12.400138854980469
+ ],
+ [
+ "▁soins",
+ -12.400164604187012
+ ],
+ [
+ "▁brut",
+ -12.400250434875488
+ ],
+ [
+ "▁revolutionary",
+ -12.400495529174805
+ ],
+ [
+ "▁liberté",
+ -12.400530815124512
+ ],
+ [
+ "cours",
+ -12.400945663452148
+ ],
+ [
+ "▁Similar",
+ -12.401247024536133
+ ],
+ [
+ "▁cheveux",
+ -12.40136432647705
+ ],
+ [
+ "▁ieftin",
+ -12.401599884033203
+ ],
+ [
+ "▁promovare",
+ -12.40160846710205
+ ],
+ [
+ "▁grains",
+ -12.401729583740234
+ ],
+ [
+ "ти",
+ -12.401749610900879
+ ],
+ [
+ "▁fonctionnement",
+ -12.401789665222168
+ ],
+ [
+ "▁Coming",
+ -12.401832580566406
+ ],
+ [
+ "▁analytical",
+ -12.401847839355469
+ ],
+ [
+ "▁simplify",
+ -12.401856422424316
+ ],
+ [
+ "▁chambres",
+ -12.401893615722656
+ ],
+ [
+ "▁fifty",
+ -12.401930809020996
+ ],
+ [
+ "jour",
+ -12.402070999145508
+ ],
+ [
+ "▁(17",
+ -12.402194023132324
+ ],
+ [
+ "cărui",
+ -12.402292251586914
+ ],
+ [
+ "▁harmony",
+ -12.402352333068848
+ ],
+ [
+ "grin",
+ -12.402355194091797
+ ],
+ [
+ "▁drunk",
+ -12.402359962463379
+ ],
+ [
+ "260",
+ -12.402374267578125
+ ],
+ [
+ "3-5",
+ -12.40243148803711
+ ],
+ [
+ "▁articole",
+ -12.402442932128906
+ ],
+ [
+ "▁flooding",
+ -12.402482986450195
+ ],
+ [
+ "halle",
+ -12.402580261230469
+ ],
+ [
+ "▁defects",
+ -12.40276050567627
+ ],
+ [
+ "▁rifle",
+ -12.402839660644531
+ ],
+ [
+ "▁Boc",
+ -12.402843475341797
+ ],
+ [
+ "▁Athletic",
+ -12.40284538269043
+ ],
+ [
+ "▁acordat",
+ -12.40292739868164
+ ],
+ [
+ "AIR",
+ -12.402969360351562
+ ],
+ [
+ "▁entwickeln",
+ -12.403104782104492
+ ],
+ [
+ "▁Advance",
+ -12.403188705444336
+ ],
+ [
+ "▁Heil",
+ -12.403216361999512
+ ],
+ [
+ "Stainless",
+ -12.403345108032227
+ ],
+ [
+ "▁Psychology",
+ -12.40337085723877
+ ],
+ [
+ "▁omul",
+ -12.403435707092285
+ ],
+ [
+ "▁Arbeiten",
+ -12.403494834899902
+ ],
+ [
+ "▁rabbit",
+ -12.403495788574219
+ ],
+ [
+ "▁méta",
+ -12.40351390838623
+ ],
+ [
+ "ismul",
+ -12.403534889221191
+ ],
+ [
+ "▁Herausforderung",
+ -12.403594970703125
+ ],
+ [
+ "▁Euch",
+ -12.403654098510742
+ ],
+ [
+ "geschichte",
+ -12.40390682220459
+ ],
+ [
+ "▁Milk",
+ -12.404057502746582
+ ],
+ [
+ "▁pregăt",
+ -12.404065132141113
+ ],
+ [
+ "▁Standort",
+ -12.404141426086426
+ ],
+ [
+ "Val",
+ -12.404180526733398
+ ],
+ [
+ "▁Ronald",
+ -12.404350280761719
+ ],
+ [
+ "▁Werbe",
+ -12.404558181762695
+ ],
+ [
+ "▁restrict",
+ -12.404658317565918
+ ],
+ [
+ "▁tablespoon",
+ -12.404844284057617
+ ],
+ [
+ "▁Amendment",
+ -12.404845237731934
+ ],
+ [
+ "▁Johnny",
+ -12.404914855957031
+ ],
+ [
+ "▁lively",
+ -12.404938697814941
+ ],
+ [
+ "ORD",
+ -12.405147552490234
+ ],
+ [
+ "▁mulţi",
+ -12.40523624420166
+ ],
+ [
+ "èrent",
+ -12.405241012573242
+ ],
+ [
+ "Every",
+ -12.405277252197266
+ ],
+ [
+ "eignet",
+ -12.405296325683594
+ ],
+ [
+ "GD",
+ -12.40546989440918
+ ],
+ [
+ "▁Ghana",
+ -12.405628204345703
+ ],
+ [
+ "▁wealthy",
+ -12.40576171875
+ ],
+ [
+ "▁advocates",
+ -12.405818939208984
+ ],
+ [
+ "▁Campaign",
+ -12.40584659576416
+ ],
+ [
+ "▁posters",
+ -12.405964851379395
+ ],
+ [
+ "flug",
+ -12.406011581420898
+ ],
+ [
+ "▁métier",
+ -12.406139373779297
+ ],
+ [
+ "kir",
+ -12.406148910522461
+ ],
+ [
+ "bond",
+ -12.406176567077637
+ ],
+ [
+ "datorita",
+ -12.406188011169434
+ ],
+ [
+ "▁Hochzeit",
+ -12.406230926513672
+ ],
+ [
+ "▁effectué",
+ -12.406271934509277
+ ],
+ [
+ "▁angles",
+ -12.40654182434082
+ ],
+ [
+ "▁Electrical",
+ -12.406705856323242
+ ],
+ [
+ "▁Administrator",
+ -12.40674114227295
+ ],
+ [
+ "▁spur",
+ -12.407389640808105
+ ],
+ [
+ "▁größere",
+ -12.407444953918457
+ ],
+ [
+ "woke",
+ -12.407515525817871
+ ],
+ [
+ "▁gewinnen",
+ -12.407689094543457
+ ],
+ [
+ "▁ajută",
+ -12.407712936401367
+ ],
+ [
+ "▁ventilation",
+ -12.407853126525879
+ ],
+ [
+ "▁viaţa",
+ -12.407853126525879
+ ],
+ [
+ "▁Dinner",
+ -12.408079147338867
+ ],
+ [
+ "respond",
+ -12.408095359802246
+ ],
+ [
+ "▁OEM",
+ -12.408120155334473
+ ],
+ [
+ "▁affair",
+ -12.4081392288208
+ ],
+ [
+ "▁öffentlich",
+ -12.408143043518066
+ ],
+ [
+ "ENS",
+ -12.408209800720215
+ ],
+ [
+ "▁Cent",
+ -12.408224105834961
+ ],
+ [
+ "▁făc",
+ -12.408267974853516
+ ],
+ [
+ "▁Doppel",
+ -12.408285140991211
+ ],
+ [
+ "▁fericit",
+ -12.408363342285156
+ ],
+ [
+ "▁coordon",
+ -12.40845775604248
+ ],
+ [
+ "geht",
+ -12.408547401428223
+ ],
+ [
+ "▁perfekte",
+ -12.408610343933105
+ ],
+ [
+ "▁sportive",
+ -12.408700942993164
+ ],
+ [
+ "▁proiectul",
+ -12.40870189666748
+ ],
+ [
+ "▁deadly",
+ -12.408804893493652
+ ],
+ [
+ "Geschäft",
+ -12.408822059631348
+ ],
+ [
+ "▁inspirational",
+ -12.408854484558105
+ ],
+ [
+ "+1",
+ -12.409013748168945
+ ],
+ [
+ "▁pearl",
+ -12.409022331237793
+ ],
+ [
+ "▁scrub",
+ -12.409036636352539
+ ],
+ [
+ "▁scheint",
+ -12.409079551696777
+ ],
+ [
+ "poo",
+ -12.409147262573242
+ ],
+ [
+ "▁Pier",
+ -12.409220695495605
+ ],
+ [
+ "▁commented",
+ -12.409285545349121
+ ],
+ [
+ "lute",
+ -12.409302711486816
+ ],
+ [
+ "▁cancelled",
+ -12.409488677978516
+ ],
+ [
+ "Win",
+ -12.409605979919434
+ ],
+ [
+ "▁payroll",
+ -12.409781455993652
+ ],
+ [
+ "▁varsta",
+ -12.409881591796875
+ ],
+ [
+ "stuffed",
+ -12.410097122192383
+ ],
+ [
+ "▁beads",
+ -12.410138130187988
+ ],
+ [
+ "▁poems",
+ -12.410356521606445
+ ],
+ [
+ "pokesman",
+ -12.410399436950684
+ ],
+ [
+ "▁checklist",
+ -12.410523414611816
+ ],
+ [
+ "▁Mich",
+ -12.410636901855469
+ ],
+ [
+ "GEN",
+ -12.410676002502441
+ ],
+ [
+ "▁Lau",
+ -12.410783767700195
+ ],
+ [
+ "▁stie",
+ -12.410965919494629
+ ],
+ [
+ "▁Lovely",
+ -12.4110107421875
+ ],
+ [
+ "▁Anschluss",
+ -12.411062240600586
+ ],
+ [
+ "▁personaj",
+ -12.41108226776123
+ ],
+ [
+ "▁ausgestattet",
+ -12.411121368408203
+ ],
+ [
+ "▁beginners",
+ -12.411163330078125
+ ],
+ [
+ "▁noon",
+ -12.411189079284668
+ ],
+ [
+ "▁celule",
+ -12.41128921508789
+ ],
+ [
+ "Trans",
+ -12.411324501037598
+ ],
+ [
+ "boot",
+ -12.411331176757812
+ ],
+ [
+ "▁drumul",
+ -12.41136646270752
+ ],
+ [
+ "gruppen",
+ -12.41140079498291
+ ],
+ [
+ "étend",
+ -12.41140365600586
+ ],
+ [
+ "▁risques",
+ -12.411405563354492
+ ],
+ [
+ "acclaimed",
+ -12.411447525024414
+ ],
+ [
+ "▁celelalte",
+ -12.411617279052734
+ ],
+ [
+ "▁condiţii",
+ -12.411620140075684
+ ],
+ [
+ "▁skiing",
+ -12.411685943603516
+ ],
+ [
+ "▁optimale",
+ -12.411689758300781
+ ],
+ [
+ "technology",
+ -12.411773681640625
+ ],
+ [
+ "▁renew",
+ -12.411784172058105
+ ],
+ [
+ "Cloud",
+ -12.41179084777832
+ ],
+ [
+ "▁damaging",
+ -12.411905288696289
+ ],
+ [
+ "GT",
+ -12.412219047546387
+ ],
+ [
+ "▁Reform",
+ -12.41230583190918
+ ],
+ [
+ "vedem",
+ -12.412349700927734
+ ],
+ [
+ "▁indicat",
+ -12.412461280822754
+ ],
+ [
+ "▁Maker",
+ -12.412467002868652
+ ],
+ [
+ "▁lichid",
+ -12.412582397460938
+ ],
+ [
+ "3.1",
+ -12.412614822387695
+ ],
+ [
+ "păt",
+ -12.412620544433594
+ ],
+ [
+ "lumina",
+ -12.41264820098877
+ ],
+ [
+ "▁Situ",
+ -12.412806510925293
+ ],
+ [
+ "▁Archives",
+ -12.412857055664062
+ ],
+ [
+ "▁allergies",
+ -12.41287899017334
+ ],
+ [
+ "▁Cameron",
+ -12.412883758544922
+ ],
+ [
+ "▁Immun",
+ -12.412899017333984
+ ],
+ [
+ "wissenschaftlich",
+ -12.41301441192627
+ ],
+ [
+ "▁supplémentaire",
+ -12.413128852844238
+ ],
+ [
+ "▁puterea",
+ -12.413261413574219
+ ],
+ [
+ "Lab",
+ -12.413331985473633
+ ],
+ [
+ "inspired",
+ -12.413384437561035
+ ],
+ [
+ "▁shrink",
+ -12.413403511047363
+ ],
+ [
+ "▁voit",
+ -12.413426399230957
+ ],
+ [
+ "▁chopped",
+ -12.413467407226562
+ ],
+ [
+ "▁Franz",
+ -12.413537979125977
+ ],
+ [
+ "oku",
+ -12.413652420043945
+ ],
+ [
+ "▁suppress",
+ -12.413673400878906
+ ],
+ [
+ "▁impress",
+ -12.413751602172852
+ ],
+ [
+ "▁Liga",
+ -12.413755416870117
+ ],
+ [
+ "▁Eight",
+ -12.41378402709961
+ ],
+ [
+ "720",
+ -12.413795471191406
+ ],
+ [
+ "▁securely",
+ -12.413870811462402
+ ],
+ [
+ "KU",
+ -12.413934707641602
+ ],
+ [
+ "modell",
+ -12.413992881774902
+ ],
+ [
+ "Ensure",
+ -12.414154052734375
+ ],
+ [
+ "größte",
+ -12.414204597473145
+ ],
+ [
+ "▁réuni",
+ -12.414215087890625
+ ],
+ [
+ "▁Internal",
+ -12.41423225402832
+ ],
+ [
+ "▁Punkte",
+ -12.414320945739746
+ ],
+ [
+ "▁replicate",
+ -12.414412498474121
+ ],
+ [
+ "▁spreadsheet",
+ -12.414434432983398
+ ],
+ [
+ "▁Hindu",
+ -12.414549827575684
+ ],
+ [
+ "▁Cham",
+ -12.414578437805176
+ ],
+ [
+ "nati",
+ -12.414670944213867
+ ],
+ [
+ "imply",
+ -12.414679527282715
+ ],
+ [
+ "funded",
+ -12.414894104003906
+ ],
+ [
+ "▁charitable",
+ -12.414896011352539
+ ],
+ [
+ "▁imagined",
+ -12.415014266967773
+ ],
+ [
+ "hausen",
+ -12.41517448425293
+ ],
+ [
+ "Keeping",
+ -12.415239334106445
+ ],
+ [
+ "▁attitudes",
+ -12.415287971496582
+ ],
+ [
+ "esque",
+ -12.415365219116211
+ ],
+ [
+ "▁Tennis",
+ -12.415409088134766
+ ],
+ [
+ "Jeremy",
+ -12.415410041809082
+ ],
+ [
+ "▁majeur",
+ -12.415475845336914
+ ],
+ [
+ "▁stii",
+ -12.4155912399292
+ ],
+ [
+ "▁herbal",
+ -12.415790557861328
+ ],
+ [
+ "▁cauta",
+ -12.41580867767334
+ ],
+ [
+ "▁voluntary",
+ -12.415828704833984
+ ],
+ [
+ "wohl",
+ -12.415877342224121
+ ],
+ [
+ "▁ideea",
+ -12.41588306427002
+ ],
+ [
+ "▁WW",
+ -12.415899276733398
+ ],
+ [
+ "▁erneut",
+ -12.416010856628418
+ ],
+ [
+ "größten",
+ -12.416094779968262
+ ],
+ [
+ "Grâce",
+ -12.416159629821777
+ ],
+ [
+ "▁Köln",
+ -12.416193008422852
+ ],
+ [
+ "▁mobilier",
+ -12.416199684143066
+ ],
+ [
+ "▁fool",
+ -12.416254043579102
+ ],
+ [
+ "▁Calcul",
+ -12.416295051574707
+ ],
+ [
+ "attaque",
+ -12.41637897491455
+ ],
+ [
+ "▁digestive",
+ -12.41656494140625
+ ],
+ [
+ "performance",
+ -12.416647911071777
+ ],
+ [
+ "▁homeowner",
+ -12.41675853729248
+ ],
+ [
+ "▁hunger",
+ -12.4169282913208
+ ],
+ [
+ "2.3",
+ -12.41696834564209
+ ],
+ [
+ "▁Sort",
+ -12.417085647583008
+ ],
+ [
+ "▁Dennis",
+ -12.41723918914795
+ ],
+ [
+ "▁certificat",
+ -12.417250633239746
+ ],
+ [
+ "▁Canal",
+ -12.417337417602539
+ ],
+ [
+ "▁Yesterday",
+ -12.417424201965332
+ ],
+ [
+ "▁sausage",
+ -12.417499542236328
+ ],
+ [
+ "▁perdu",
+ -12.417736053466797
+ ],
+ [
+ "ösen",
+ -12.417741775512695
+ ],
+ [
+ "▁preserved",
+ -12.417750358581543
+ ],
+ [
+ "▁trendy",
+ -12.4177885055542
+ ],
+ [
+ "▁iubire",
+ -12.417935371398926
+ ],
+ [
+ "▁grandfather",
+ -12.417961120605469
+ ],
+ [
+ "▁shoppers",
+ -12.41820240020752
+ ],
+ [
+ "▁verschieden",
+ -12.418252944946289
+ ],
+ [
+ "▁gagner",
+ -12.41826343536377
+ ],
+ [
+ "▁lucra",
+ -12.418437004089355
+ ],
+ [
+ "metru",
+ -12.418464660644531
+ ],
+ [
+ "buz",
+ -12.418469429016113
+ ],
+ [
+ "▁flourish",
+ -12.418484687805176
+ ],
+ [
+ "affin",
+ -12.418523788452148
+ ],
+ [
+ "▁Pflanzen",
+ -12.41858196258545
+ ],
+ [
+ "agh",
+ -12.418588638305664
+ ],
+ [
+ "▁Gill",
+ -12.418660163879395
+ ],
+ [
+ "▁Kä",
+ -12.418671607971191
+ ],
+ [
+ "▁Wege",
+ -12.41876220703125
+ ],
+ [
+ "▁Liberal",
+ -12.418929100036621
+ ],
+ [
+ "▁Glasgow",
+ -12.418944358825684
+ ],
+ [
+ "Objekt",
+ -12.4189453125
+ ],
+ [
+ "▁Huawei",
+ -12.4189453125
+ ],
+ [
+ "appropri",
+ -12.418986320495605
+ ],
+ [
+ "▁genius",
+ -12.419037818908691
+ ],
+ [
+ "▁brokers",
+ -12.419068336486816
+ ],
+ [
+ "▁themed",
+ -12.41918659210205
+ ],
+ [
+ "▁barre",
+ -12.419210433959961
+ ],
+ [
+ "1.7",
+ -12.419219017028809
+ ],
+ [
+ "▁Electro",
+ -12.419303894042969
+ ],
+ [
+ "▁umbrella",
+ -12.419333457946777
+ ],
+ [
+ "▁advisory",
+ -12.419417381286621
+ ],
+ [
+ "▁comport",
+ -12.419421195983887
+ ],
+ [
+ "▁neuer",
+ -12.419452667236328
+ ],
+ [
+ "▁Wick",
+ -12.419568061828613
+ ],
+ [
+ "wak",
+ -12.419618606567383
+ ],
+ [
+ "▁Woman",
+ -12.419695854187012
+ ],
+ [
+ "▁lesser",
+ -12.419843673706055
+ ],
+ [
+ "▁replied",
+ -12.419987678527832
+ ],
+ [
+ "▁représente",
+ -12.420050621032715
+ ],
+ [
+ "▁thé",
+ -12.420135498046875
+ ],
+ [
+ "Deutsch",
+ -12.420428276062012
+ ],
+ [
+ "Cat",
+ -12.420483589172363
+ ],
+ [
+ "▁équipes",
+ -12.420534133911133
+ ],
+ [
+ "▁spider",
+ -12.420578956604004
+ ],
+ [
+ "▁Gaming",
+ -12.420589447021484
+ ],
+ [
+ "▁Liste",
+ -12.420592308044434
+ ],
+ [
+ "▁affection",
+ -12.420639038085938
+ ],
+ [
+ "lipsa",
+ -12.420982360839844
+ ],
+ [
+ "▁Spider",
+ -12.420987129211426
+ ],
+ [
+ "▁Julia",
+ -12.421034812927246
+ ],
+ [
+ "anlagen",
+ -12.421159744262695
+ ],
+ [
+ "Kon",
+ -12.421363830566406
+ ],
+ [
+ "nței",
+ -12.421368598937988
+ ],
+ [
+ "▁Verwaltung",
+ -12.421483993530273
+ ],
+ [
+ "▁raspuns",
+ -12.421489715576172
+ ],
+ [
+ "samt",
+ -12.421491622924805
+ ],
+ [
+ "▁creștere",
+ -12.421512603759766
+ ],
+ [
+ "▁decorate",
+ -12.421701431274414
+ ],
+ [
+ "▁Chain",
+ -12.422021865844727
+ ],
+ [
+ "ów",
+ -12.422050476074219
+ ],
+ [
+ "0-0",
+ -12.422104835510254
+ ],
+ [
+ "▁Cran",
+ -12.422407150268555
+ ],
+ [
+ "▁streak",
+ -12.42242431640625
+ ],
+ [
+ "ор",
+ -12.422517776489258
+ ],
+ [
+ "▁căuta",
+ -12.422754287719727
+ ],
+ [
+ "wende",
+ -12.422801971435547
+ ],
+ [
+ "▁haine",
+ -12.42280387878418
+ ],
+ [
+ "▁landscaping",
+ -12.423009872436523
+ ],
+ [
+ "▁historian",
+ -12.423016548156738
+ ],
+ [
+ "▁grandchildren",
+ -12.423033714294434
+ ],
+ [
+ "▁crawl",
+ -12.423056602478027
+ ],
+ [
+ "▁Cub",
+ -12.423239707946777
+ ],
+ [
+ "▁nécessaires",
+ -12.423515319824219
+ ],
+ [
+ "▁swift",
+ -12.42352294921875
+ ],
+ [
+ "▁calculation",
+ -12.423656463623047
+ ],
+ [
+ "▁acteurs",
+ -12.423715591430664
+ ],
+ [
+ "VT",
+ -12.423752784729004
+ ],
+ [
+ "▁Hristos",
+ -12.423778533935547
+ ],
+ [
+ "▁slices",
+ -12.423850059509277
+ ],
+ [
+ "See",
+ -12.424203872680664
+ ],
+ [
+ "▁Bran",
+ -12.424233436584473
+ ],
+ [
+ "Symbol",
+ -12.424449920654297
+ ],
+ [
+ "▁allowance",
+ -12.424492835998535
+ ],
+ [
+ "▁Effective",
+ -12.424537658691406
+ ],
+ [
+ "▁Wünsche",
+ -12.424539566040039
+ ],
+ [
+ "▁shiny",
+ -12.424569129943848
+ ],
+ [
+ "▁professionalism",
+ -12.424715995788574
+ ],
+ [
+ "/6",
+ -12.424970626831055
+ ],
+ [
+ "▁terrasse",
+ -12.425087928771973
+ ],
+ [
+ "▁researcher",
+ -12.425156593322754
+ ],
+ [
+ "▁fragile",
+ -12.425203323364258
+ ],
+ [
+ "▁greeting",
+ -12.425274848937988
+ ],
+ [
+ "freien",
+ -12.4253511428833
+ ],
+ [
+ "▁valuation",
+ -12.425372123718262
+ ],
+ [
+ "▁incur",
+ -12.425386428833008
+ ],
+ [
+ "▁Zwischen",
+ -12.425559997558594
+ ],
+ [
+ "▁comfy",
+ -12.425569534301758
+ ],
+ [
+ "▁méthode",
+ -12.42569351196289
+ ],
+ [
+ "▁Pirate",
+ -12.425816535949707
+ ],
+ [
+ "▁Moto",
+ -12.425822257995605
+ ],
+ [
+ "(6)",
+ -12.425823211669922
+ ],
+ [
+ "▁devin",
+ -12.42582893371582
+ ],
+ [
+ "▁civic",
+ -12.425837516784668
+ ],
+ [
+ "usage",
+ -12.425889015197754
+ ],
+ [
+ "▁istorie",
+ -12.425945281982422
+ ],
+ [
+ "▁piste",
+ -12.425955772399902
+ ],
+ [
+ "▁Rug",
+ -12.426091194152832
+ ],
+ [
+ "pä",
+ -12.426129341125488
+ ],
+ [
+ "▁matur",
+ -12.426148414611816
+ ],
+ [
+ "CAS",
+ -12.426155090332031
+ ],
+ [
+ "TIC",
+ -12.42618465423584
+ ],
+ [
+ "▁Reduce",
+ -12.426234245300293
+ ],
+ [
+ "▁commemorat",
+ -12.426321983337402
+ ],
+ [
+ "▁cease",
+ -12.42653751373291
+ ],
+ [
+ "unterschiedliche",
+ -12.42656421661377
+ ],
+ [
+ "▁cinnamon",
+ -12.426581382751465
+ ],
+ [
+ "▁Font",
+ -12.426583290100098
+ ],
+ [
+ "▁justify",
+ -12.426751136779785
+ ],
+ [
+ "deteriorat",
+ -12.426797866821289
+ ],
+ [
+ "▁Schön",
+ -12.42684555053711
+ ],
+ [
+ "plain",
+ -12.426993370056152
+ ],
+ [
+ "frist",
+ -12.427002906799316
+ ],
+ [
+ "▁helmet",
+ -12.42712116241455
+ ],
+ [
+ "▁statute",
+ -12.42721939086914
+ ],
+ [
+ "accept",
+ -12.427236557006836
+ ],
+ [
+ "▁1,5",
+ -12.42724323272705
+ ],
+ [
+ "▁recon",
+ -12.42724323272705
+ ],
+ [
+ "▁Möbel",
+ -12.427348136901855
+ ],
+ [
+ "▁idées",
+ -12.427367210388184
+ ],
+ [
+ "automat",
+ -12.427552223205566
+ ],
+ [
+ "Team",
+ -12.42758846282959
+ ],
+ [
+ "▁performers",
+ -12.427688598632812
+ ],
+ [
+ "▁microphone",
+ -12.427722930908203
+ ],
+ [
+ "impotriva",
+ -12.427775382995605
+ ],
+ [
+ "▁pillows",
+ -12.42780876159668
+ ],
+ [
+ "▁accountable",
+ -12.427812576293945
+ ],
+ [
+ "▁strings",
+ -12.42782974243164
+ ],
+ [
+ "hydrate",
+ -12.427835464477539
+ ],
+ [
+ "▁Yan",
+ -12.427865028381348
+ ],
+ [
+ "starea",
+ -12.427918434143066
+ ],
+ [
+ "▁présenté",
+ -12.42793083190918
+ ],
+ [
+ "▁extensively",
+ -12.428048133850098
+ ],
+ [
+ "äst",
+ -12.428114891052246
+ ],
+ [
+ "▁correlation",
+ -12.428115844726562
+ ],
+ [
+ "bespoke",
+ -12.428119659423828
+ ],
+ [
+ "▁creste",
+ -12.428196907043457
+ ],
+ [
+ "▁Armenia",
+ -12.428248405456543
+ ],
+ [
+ "nose",
+ -12.428426742553711
+ ],
+ [
+ "▁strengthening",
+ -12.428604125976562
+ ],
+ [
+ "▁Horizon",
+ -12.428627014160156
+ ],
+ [
+ "▁obesity",
+ -12.428627967834473
+ ],
+ [
+ "seasoned",
+ -12.428686141967773
+ ],
+ [
+ "▁screenshot",
+ -12.428736686706543
+ ],
+ [
+ "girl",
+ -12.42875862121582
+ ],
+ [
+ "▁hardest",
+ -12.428826332092285
+ ],
+ [
+ "▁weakness",
+ -12.428855895996094
+ ],
+ [
+ "effectuer",
+ -12.429012298583984
+ ],
+ [
+ "▁Florence",
+ -12.429034233093262
+ ],
+ [
+ "▁Europene",
+ -12.429062843322754
+ ],
+ [
+ "triggered",
+ -12.429333686828613
+ ],
+ [
+ "Apparently",
+ -12.42939567565918
+ ],
+ [
+ "▁diagnose",
+ -12.42943286895752
+ ],
+ [
+ "rushed",
+ -12.429494857788086
+ ],
+ [
+ "▁trotz",
+ -12.429516792297363
+ ],
+ [
+ "▁spécial",
+ -12.429680824279785
+ ],
+ [
+ "▁lumi",
+ -12.429783821105957
+ ],
+ [
+ "7:00",
+ -12.429877281188965
+ ],
+ [
+ "▁publicat",
+ -12.429903984069824
+ ],
+ [
+ "ос",
+ -12.430086135864258
+ ],
+ [
+ "▁hue",
+ -12.430136680603027
+ ],
+ [
+ "▁termination",
+ -12.430139541625977
+ ],
+ [
+ "▁Nam",
+ -12.430240631103516
+ ],
+ [
+ "Well",
+ -12.430376052856445
+ ],
+ [
+ "▁Extract",
+ -12.430441856384277
+ ],
+ [
+ "atiile",
+ -12.43062686920166
+ ],
+ [
+ "▁vivid",
+ -12.43076229095459
+ ],
+ [
+ "hrs",
+ -12.430858612060547
+ ],
+ [
+ "▁povesti",
+ -12.430984497070312
+ ],
+ [
+ "stehenden",
+ -12.430988311767578
+ ],
+ [
+ "▁informieren",
+ -12.431070327758789
+ ],
+ [
+ "employed",
+ -12.431133270263672
+ ],
+ [
+ "▁armor",
+ -12.431180953979492
+ ],
+ [
+ "▁Columbus",
+ -12.431191444396973
+ ],
+ [
+ "Registr",
+ -12.431200981140137
+ ],
+ [
+ "▁Kamera",
+ -12.431203842163086
+ ],
+ [
+ "▁ugly",
+ -12.431203842163086
+ ],
+ [
+ "outil",
+ -12.431234359741211
+ ],
+ [
+ "▁evenly",
+ -12.43134593963623
+ ],
+ [
+ "lungul",
+ -12.431349754333496
+ ],
+ [
+ "koch",
+ -12.431439399719238
+ ],
+ [
+ "▁Dig",
+ -12.431450843811035
+ ],
+ [
+ "purely",
+ -12.431489944458008
+ ],
+ [
+ "▁Surf",
+ -12.431560516357422
+ ],
+ [
+ "rilla",
+ -12.431628227233887
+ ],
+ [
+ "▁Watson",
+ -12.43171215057373
+ ],
+ [
+ "trug",
+ -12.431719779968262
+ ],
+ [
+ "figuring",
+ -12.431784629821777
+ ],
+ [
+ "▁competitor",
+ -12.431807518005371
+ ],
+ [
+ "▁humid",
+ -12.431889533996582
+ ],
+ [
+ "▁Lawyer",
+ -12.43189811706543
+ ],
+ [
+ "Added",
+ -12.43205451965332
+ ],
+ [
+ "▁salva",
+ -12.432056427001953
+ ],
+ [
+ "▁drainage",
+ -12.4321870803833
+ ],
+ [
+ "Featuring",
+ -12.432220458984375
+ ],
+ [
+ "▁Pel",
+ -12.43234634399414
+ ],
+ [
+ "▁acasa",
+ -12.432611465454102
+ ],
+ [
+ "▁expectation",
+ -12.43265438079834
+ ],
+ [
+ "gibt",
+ -12.432663917541504
+ ],
+ [
+ "▁marginal",
+ -12.432831764221191
+ ],
+ [
+ "ceni",
+ -12.433028221130371
+ ],
+ [
+ "▁européen",
+ -12.433065414428711
+ ],
+ [
+ "clav",
+ -12.433090209960938
+ ],
+ [
+ "▁Shot",
+ -12.433167457580566
+ ],
+ [
+ "commun",
+ -12.43322467803955
+ ],
+ [
+ "▁Calendar",
+ -12.433247566223145
+ ],
+ [
+ "▁trek",
+ -12.433348655700684
+ ],
+ [
+ "rechtliche",
+ -12.433406829833984
+ ],
+ [
+ "▁Perry",
+ -12.43342399597168
+ ],
+ [
+ "▁surge",
+ -12.433484077453613
+ ],
+ [
+ "geschäft",
+ -12.433504104614258
+ ],
+ [
+ "paced",
+ -12.433793067932129
+ ],
+ [
+ "depend",
+ -12.433871269226074
+ ],
+ [
+ "▁Sache",
+ -12.433947563171387
+ ],
+ [
+ "▁Example",
+ -12.433998107910156
+ ],
+ [
+ "▁lider",
+ -12.434118270874023
+ ],
+ [
+ "▁nochmal",
+ -12.434240341186523
+ ],
+ [
+ "▁Present",
+ -12.434243202209473
+ ],
+ [
+ "KW",
+ -12.434335708618164
+ ],
+ [
+ "prompted",
+ -12.434350967407227
+ ],
+ [
+ "logique",
+ -12.434444427490234
+ ],
+ [
+ "Université",
+ -12.434466361999512
+ ],
+ [
+ "lith",
+ -12.434489250183105
+ ],
+ [
+ "▁Gefahr",
+ -12.434579849243164
+ ],
+ [
+ "▁Acid",
+ -12.434625625610352
+ ],
+ [
+ "objets",
+ -12.434791564941406
+ ],
+ [
+ "▁societies",
+ -12.434791564941406
+ ],
+ [
+ "▁distraction",
+ -12.434816360473633
+ ],
+ [
+ "▁puissance",
+ -12.434934616088867
+ ],
+ [
+ "▁alleviat",
+ -12.435026168823242
+ ],
+ [
+ "▁Capitol",
+ -12.435050010681152
+ ],
+ [
+ "▁Heim",
+ -12.435129165649414
+ ],
+ [
+ "judicial",
+ -12.435230255126953
+ ],
+ [
+ "▁nowadays",
+ -12.435309410095215
+ ],
+ [
+ "▁Hammer",
+ -12.435317039489746
+ ],
+ [
+ "▁metallic",
+ -12.435327529907227
+ ],
+ [
+ "▁distr",
+ -12.435388565063477
+ ],
+ [
+ "▁dispos",
+ -12.435397148132324
+ ],
+ [
+ "profile",
+ -12.435408592224121
+ ],
+ [
+ "▁Nicolas",
+ -12.435602188110352
+ ],
+ [
+ "▁presa",
+ -12.435760498046875
+ ],
+ [
+ "augh",
+ -12.43578052520752
+ ],
+ [
+ "schuss",
+ -12.435787200927734
+ ],
+ [
+ "▁Diana",
+ -12.436062812805176
+ ],
+ [
+ "4-5",
+ -12.436097145080566
+ ],
+ [
+ "▁Chapel",
+ -12.43612003326416
+ ],
+ [
+ "▁zahar",
+ -12.436150550842285
+ ],
+ [
+ "âmb",
+ -12.4362154006958
+ ],
+ [
+ "▁Tarif",
+ -12.436264991760254
+ ],
+ [
+ "▁devastating",
+ -12.436339378356934
+ ],
+ [
+ "6:00",
+ -12.4364013671875
+ ],
+ [
+ "▁100,000",
+ -12.43645191192627
+ ],
+ [
+ "NIC",
+ -12.436580657958984
+ ],
+ [
+ "▁Lucas",
+ -12.436612129211426
+ ],
+ [
+ "▁bequem",
+ -12.436662673950195
+ ],
+ [
+ "▁Motion",
+ -12.436698913574219
+ ],
+ [
+ "7,000",
+ -12.436701774597168
+ ],
+ [
+ "▁malware",
+ -12.436708450317383
+ ],
+ [
+ "▁avenue",
+ -12.436723709106445
+ ],
+ [
+ "▁manger",
+ -12.436747550964355
+ ],
+ [
+ "▁Queensland",
+ -12.436857223510742
+ ],
+ [
+ "▁Papier",
+ -12.436861991882324
+ ],
+ [
+ "▁Increase",
+ -12.436880111694336
+ ],
+ [
+ "▁implies",
+ -12.436954498291016
+ ],
+ [
+ "▁äußer",
+ -12.43697452545166
+ ],
+ [
+ "▁Meine",
+ -12.436980247497559
+ ],
+ [
+ "Reuters",
+ -12.437155723571777
+ ],
+ [
+ "▁Belt",
+ -12.437232971191406
+ ],
+ [
+ "Educat",
+ -12.437251091003418
+ ],
+ [
+ "▁Aktion",
+ -12.437355041503906
+ ],
+ [
+ "schläge",
+ -12.437372207641602
+ ],
+ [
+ "▁înregistrat",
+ -12.437426567077637
+ ],
+ [
+ "▁Ortho",
+ -12.43756103515625
+ ],
+ [
+ "▁bulbs",
+ -12.437761306762695
+ ],
+ [
+ "kap",
+ -12.437793731689453
+ ],
+ [
+ "▁peinture",
+ -12.437901496887207
+ ],
+ [
+ "▁Lounge",
+ -12.437907218933105
+ ],
+ [
+ "▁Tampa",
+ -12.438008308410645
+ ],
+ [
+ "ifiziert",
+ -12.438100814819336
+ ],
+ [
+ "kinder",
+ -12.438172340393066
+ ],
+ [
+ "▁comparativ",
+ -12.438281059265137
+ ],
+ [
+ "häuser",
+ -12.438323974609375
+ ],
+ [
+ "incarn",
+ -12.438363075256348
+ ],
+ [
+ "▁amazon",
+ -12.438464164733887
+ ],
+ [
+ "▁Southeast",
+ -12.438505172729492
+ ],
+ [
+ "▁economical",
+ -12.438667297363281
+ ],
+ [
+ "▁broth",
+ -12.438697814941406
+ ],
+ [
+ "▁Secure",
+ -12.438750267028809
+ ],
+ [
+ "damals",
+ -12.438875198364258
+ ],
+ [
+ "▁Elementary",
+ -12.438921928405762
+ ],
+ [
+ "▁Wildlife",
+ -12.438995361328125
+ ],
+ [
+ "▁Jewel",
+ -12.439001083374023
+ ],
+ [
+ "▁protocols",
+ -12.439297676086426
+ ],
+ [
+ "▁zbor",
+ -12.4393892288208
+ ],
+ [
+ "▁enthusiasts",
+ -12.439398765563965
+ ],
+ [
+ "▁Mirror",
+ -12.439444541931152
+ ],
+ [
+ "▁soak",
+ -12.439537048339844
+ ],
+ [
+ "▁Sad",
+ -12.439574241638184
+ ],
+ [
+ "▁dishwasher",
+ -12.439957618713379
+ ],
+ [
+ "▁vollständig",
+ -12.440186500549316
+ ],
+ [
+ "▁Vermont",
+ -12.440407752990723
+ ],
+ [
+ "▁caut",
+ -12.440449714660645
+ ],
+ [
+ "▁fournisseur",
+ -12.440475463867188
+ ],
+ [
+ "▁Concrete",
+ -12.44047737121582
+ ],
+ [
+ "▁Instant",
+ -12.440595626831055
+ ],
+ [
+ "▁reveni",
+ -12.440597534179688
+ ],
+ [
+ "▁Surface",
+ -12.44059944152832
+ ],
+ [
+ "zumindest",
+ -12.440713882446289
+ ],
+ [
+ "▁feast",
+ -12.440725326538086
+ ],
+ [
+ "▁stretching",
+ -12.440803527832031
+ ],
+ [
+ "ERA",
+ -12.440997123718262
+ ],
+ [
+ "▁Scholarship",
+ -12.441020965576172
+ ],
+ [
+ "▁vineyard",
+ -12.4410400390625
+ ],
+ [
+ "▁régulièrement",
+ -12.441083908081055
+ ],
+ [
+ "▁patches",
+ -12.441093444824219
+ ],
+ [
+ "▁Gamb",
+ -12.44113540649414
+ ],
+ [
+ "▁Vereins",
+ -12.441152572631836
+ ],
+ [
+ "ège",
+ -12.441372871398926
+ ],
+ [
+ "▁constitutional",
+ -12.441411018371582
+ ],
+ [
+ "erreur",
+ -12.441413879394531
+ ],
+ [
+ "▁Colombia",
+ -12.441514015197754
+ ],
+ [
+ "UF",
+ -12.441618919372559
+ ],
+ [
+ "aider",
+ -12.441665649414062
+ ],
+ [
+ "cision",
+ -12.44180965423584
+ ],
+ [
+ "▁publishers",
+ -12.441913604736328
+ ],
+ [
+ "▁prelua",
+ -12.441967964172363
+ ],
+ [
+ "▁keiner",
+ -12.441990852355957
+ ],
+ [
+ "▁amid",
+ -12.442020416259766
+ ],
+ [
+ "▁quantitative",
+ -12.442031860351562
+ ],
+ [
+ "▁decay",
+ -12.442058563232422
+ ],
+ [
+ "▁distinguished",
+ -12.4420747756958
+ ],
+ [
+ "▁Gründe",
+ -12.442209243774414
+ ],
+ [
+ "▁statului",
+ -12.442362785339355
+ ],
+ [
+ "CAT",
+ -12.442436218261719
+ ],
+ [
+ "allow",
+ -12.442481994628906
+ ],
+ [
+ "▁mathematical",
+ -12.442550659179688
+ ],
+ [
+ "▁tragedy",
+ -12.44255542755127
+ ],
+ [
+ "▁heels",
+ -12.442609786987305
+ ],
+ [
+ "opia",
+ -12.44265365600586
+ ],
+ [
+ "▁merger",
+ -12.4428071975708
+ ],
+ [
+ "dispositif",
+ -12.442813873291016
+ ],
+ [
+ "▁pneu",
+ -12.44283390045166
+ ],
+ [
+ "elte",
+ -12.443058013916016
+ ],
+ [
+ "▁Introduction",
+ -12.443070411682129
+ ],
+ [
+ "▁biscuit",
+ -12.443134307861328
+ ],
+ [
+ "▁leftover",
+ -12.443275451660156
+ ],
+ [
+ "▁tester",
+ -12.443314552307129
+ ],
+ [
+ "▁Terre",
+ -12.443380355834961
+ ],
+ [
+ "▁Oui",
+ -12.44338321685791
+ ],
+ [
+ "▁rar",
+ -12.443520545959473
+ ],
+ [
+ "▁beverages",
+ -12.443666458129883
+ ],
+ [
+ "▁parenting",
+ -12.443892478942871
+ ],
+ [
+ "1-0",
+ -12.444053649902344
+ ],
+ [
+ "▁Barry",
+ -12.44417667388916
+ ],
+ [
+ "▁Lynn",
+ -12.444209098815918
+ ],
+ [
+ "▁Tyler",
+ -12.444262504577637
+ ],
+ [
+ "▁fotbal",
+ -12.44437026977539
+ ],
+ [
+ "dron",
+ -12.444475173950195
+ ],
+ [
+ "▁donor",
+ -12.44455623626709
+ ],
+ [
+ "▁drape",
+ -12.444558143615723
+ ],
+ [
+ "▁positioning",
+ -12.444963455200195
+ ],
+ [
+ "▁Tang",
+ -12.445006370544434
+ ],
+ [
+ "▁overwhelmed",
+ -12.445161819458008
+ ],
+ [
+ "▁perte",
+ -12.445192337036133
+ ],
+ [
+ "▁blender",
+ -12.445302963256836
+ ],
+ [
+ "TG",
+ -12.445467948913574
+ ],
+ [
+ "GHz",
+ -12.445490837097168
+ ],
+ [
+ "▁administrat",
+ -12.445719718933105
+ ],
+ [
+ "▁glaube",
+ -12.445771217346191
+ ],
+ [
+ "Char",
+ -12.445947647094727
+ ],
+ [
+ "impression",
+ -12.44627571105957
+ ],
+ [
+ "proving",
+ -12.446297645568848
+ ],
+ [
+ "▁Inner",
+ -12.446434020996094
+ ],
+ [
+ "root",
+ -12.446501731872559
+ ],
+ [
+ "▁Gedanken",
+ -12.446508407592773
+ ],
+ [
+ "▁underway",
+ -12.446596145629883
+ ],
+ [
+ "coat",
+ -12.44660758972168
+ ],
+ [
+ "▁thereof",
+ -12.446663856506348
+ ],
+ [
+ "rius",
+ -12.446700096130371
+ ],
+ [
+ "▁intermediate",
+ -12.446751594543457
+ ],
+ [
+ "gmail",
+ -12.446869850158691
+ ],
+ [
+ "114",
+ -12.446893692016602
+ ],
+ [
+ "▁interfere",
+ -12.446908950805664
+ ],
+ [
+ "▁Found",
+ -12.446930885314941
+ ],
+ [
+ "LF",
+ -12.447071075439453
+ ],
+ [
+ "▁equality",
+ -12.447099685668945
+ ],
+ [
+ "▁concurrent",
+ -12.44710636138916
+ ],
+ [
+ "akh",
+ -12.447107315063477
+ ],
+ [
+ "▁touching",
+ -12.44715690612793
+ ],
+ [
+ "▁curiosity",
+ -12.447235107421875
+ ],
+ [
+ "▁rendering",
+ -12.447263717651367
+ ],
+ [
+ "▁1964",
+ -12.447442054748535
+ ],
+ [
+ "sorge",
+ -12.447468757629395
+ ],
+ [
+ "ARC",
+ -12.447505950927734
+ ],
+ [
+ "▁Desktop",
+ -12.44752311706543
+ ],
+ [
+ "▁Tak",
+ -12.44760799407959
+ ],
+ [
+ "filtration",
+ -12.447651863098145
+ ],
+ [
+ "▁gates",
+ -12.4478759765625
+ ],
+ [
+ "Sehr",
+ -12.44791316986084
+ ],
+ [
+ "▁spatiu",
+ -12.44798755645752
+ ],
+ [
+ "▁Leg",
+ -12.448103904724121
+ ],
+ [
+ "▁aviation",
+ -12.448277473449707
+ ],
+ [
+ "wandel",
+ -12.44827938079834
+ ],
+ [
+ "▁Shar",
+ -12.448323249816895
+ ],
+ [
+ "▁Volks",
+ -12.448409080505371
+ ],
+ [
+ "maz",
+ -12.448698997497559
+ ],
+ [
+ "governmental",
+ -12.44874095916748
+ ],
+ [
+ "euros",
+ -12.448819160461426
+ ],
+ [
+ "avantage",
+ -12.448823928833008
+ ],
+ [
+ "sitzt",
+ -12.448856353759766
+ ],
+ [
+ "IER",
+ -12.448920249938965
+ ],
+ [
+ "▁Theory",
+ -12.44894027709961
+ ],
+ [
+ "Cependant",
+ -12.44907283782959
+ ],
+ [
+ "▁Teachers",
+ -12.449080467224121
+ ],
+ [
+ "anspruch",
+ -12.449095726013184
+ ],
+ [
+ "▁afecta",
+ -12.449139595031738
+ ],
+ [
+ "enko",
+ -12.449193000793457
+ ],
+ [
+ "▁breeding",
+ -12.449198722839355
+ ],
+ [
+ "▁Peak",
+ -12.449457168579102
+ ],
+ [
+ "▁găsit",
+ -12.449516296386719
+ ],
+ [
+ "▁măsuri",
+ -12.4495267868042
+ ],
+ [
+ "edia",
+ -12.449625968933105
+ ],
+ [
+ "biz",
+ -12.449640274047852
+ ],
+ [
+ "zum",
+ -12.449776649475098
+ ],
+ [
+ "▁schwierig",
+ -12.449847221374512
+ ],
+ [
+ "Sense",
+ -12.450050354003906
+ ],
+ [
+ "▁Jump",
+ -12.450081825256348
+ ],
+ [
+ "▁cocktails",
+ -12.450108528137207
+ ],
+ [
+ "abhängig",
+ -12.45012378692627
+ ],
+ [
+ "realised",
+ -12.450140953063965
+ ],
+ [
+ "▁programul",
+ -12.450214385986328
+ ],
+ [
+ "▁prévu",
+ -12.450238227844238
+ ],
+ [
+ "▁twitter",
+ -12.450372695922852
+ ],
+ [
+ "Union",
+ -12.450400352478027
+ ],
+ [
+ "▁Marathon",
+ -12.45040225982666
+ ],
+ [
+ "▁Christianity",
+ -12.450432777404785
+ ],
+ [
+ "▁Alberta",
+ -12.450811386108398
+ ],
+ [
+ "einheit",
+ -12.45097827911377
+ ],
+ [
+ "▁wellbeing",
+ -12.450982093811035
+ ],
+ [
+ "phen",
+ -12.451166152954102
+ ],
+ [
+ "▁Charleston",
+ -12.451180458068848
+ ],
+ [
+ "▁uncover",
+ -12.451323509216309
+ ],
+ [
+ "▁humaine",
+ -12.451464653015137
+ ],
+ [
+ "▁bleeding",
+ -12.451531410217285
+ ],
+ [
+ "▁manipul",
+ -12.451532363891602
+ ],
+ [
+ "▁humidity",
+ -12.451570510864258
+ ],
+ [
+ "▁Puis",
+ -12.451748847961426
+ ],
+ [
+ "▁aktuell",
+ -12.451922416687012
+ ],
+ [
+ "▁Nissan",
+ -12.451943397521973
+ ],
+ [
+ "▁Eisen",
+ -12.45202922821045
+ ],
+ [
+ "treiben",
+ -12.452059745788574
+ ],
+ [
+ "cios",
+ -12.452073097229004
+ ],
+ [
+ "ikh",
+ -12.452381134033203
+ ],
+ [
+ "acquiring",
+ -12.452466011047363
+ ],
+ [
+ "▁Wallpaper",
+ -12.452488899230957
+ ],
+ [
+ "▁rond",
+ -12.452558517456055
+ ],
+ [
+ "▁Doug",
+ -12.45267391204834
+ ],
+ [
+ "sourcing",
+ -12.452696800231934
+ ],
+ [
+ "▁1900",
+ -12.452825546264648
+ ],
+ [
+ "▁buni",
+ -12.452913284301758
+ ],
+ [
+ "vest",
+ -12.452916145324707
+ ],
+ [
+ "▁Bangladesh",
+ -12.452990531921387
+ ],
+ [
+ "Home",
+ -12.453160285949707
+ ],
+ [
+ "▁wrinkle",
+ -12.453252792358398
+ ],
+ [
+ "rado",
+ -12.453290939331055
+ ],
+ [
+ "▁Pain",
+ -12.45334243774414
+ ],
+ [
+ "▁herzlich",
+ -12.453354835510254
+ ],
+ [
+ "MRI",
+ -12.453426361083984
+ ],
+ [
+ "UG",
+ -12.453631401062012
+ ],
+ [
+ "▁Desk",
+ -12.453679084777832
+ ],
+ [
+ "▁remarc",
+ -12.453718185424805
+ ],
+ [
+ "▁sodium",
+ -12.453857421875
+ ],
+ [
+ "▁Jede",
+ -12.453892707824707
+ ],
+ [
+ "▁réelle",
+ -12.453959465026855
+ ],
+ [
+ "▁Polar",
+ -12.454068183898926
+ ],
+ [
+ "▁activists",
+ -12.454273223876953
+ ],
+ [
+ "lasted",
+ -12.454300880432129
+ ],
+ [
+ "Some",
+ -12.45432186126709
+ ],
+ [
+ "ISE",
+ -12.454338073730469
+ ],
+ [
+ "▁peine",
+ -12.454671859741211
+ ],
+ [
+ "▁crude",
+ -12.454852104187012
+ ],
+ [
+ "Maur",
+ -12.454916954040527
+ ],
+ [
+ "▁forcing",
+ -12.454933166503906
+ ],
+ [
+ "▁politici",
+ -12.454970359802246
+ ],
+ [
+ "▁condiții",
+ -12.454988479614258
+ ],
+ [
+ "▁Saving",
+ -12.454999923706055
+ ],
+ [
+ "▁descoperi",
+ -12.455020904541016
+ ],
+ [
+ "avenir",
+ -12.455055236816406
+ ],
+ [
+ "Akt",
+ -12.455069541931152
+ ],
+ [
+ "▁vocabulary",
+ -12.45509147644043
+ ],
+ [
+ "▁pont",
+ -12.455168724060059
+ ],
+ [
+ "West",
+ -12.45518970489502
+ ],
+ [
+ "lenk",
+ -12.455278396606445
+ ],
+ [
+ "▁Verbraucher",
+ -12.455367088317871
+ ],
+ [
+ "affects",
+ -12.455448150634766
+ ],
+ [
+ "▁Flower",
+ -12.455543518066406
+ ],
+ [
+ "▁Nebraska",
+ -12.455617904663086
+ ],
+ [
+ "▁assortment",
+ -12.455618858337402
+ ],
+ [
+ "hock",
+ -12.455619812011719
+ ],
+ [
+ "▁discounted",
+ -12.455803871154785
+ ],
+ [
+ "▁Sensor",
+ -12.455840110778809
+ ],
+ [
+ "Lie",
+ -12.45588207244873
+ ],
+ [
+ "▁Volkswagen",
+ -12.455887794494629
+ ],
+ [
+ "isseur",
+ -12.455888748168945
+ ],
+ [
+ "indice",
+ -12.455936431884766
+ ],
+ [
+ "▁scanner",
+ -12.455986022949219
+ ],
+ [
+ "fashioned",
+ -12.456040382385254
+ ],
+ [
+ "▁postal",
+ -12.456141471862793
+ ],
+ [
+ "ouvrir",
+ -12.45615291595459
+ ],
+ [
+ "▁seminars",
+ -12.45622444152832
+ ],
+ [
+ "ioase",
+ -12.456232070922852
+ ],
+ [
+ "▁Stanley",
+ -12.456260681152344
+ ],
+ [
+ "Various",
+ -12.456335067749023
+ ],
+ [
+ "essentiel",
+ -12.45650577545166
+ ],
+ [
+ "▁administered",
+ -12.456693649291992
+ ],
+ [
+ "▁concession",
+ -12.456748008728027
+ ],
+ [
+ "▁mould",
+ -12.456789016723633
+ ],
+ [
+ "▁strongest",
+ -12.456826210021973
+ ],
+ [
+ "Erlebnis",
+ -12.456933975219727
+ ],
+ [
+ "▁ehemalige",
+ -12.456933975219727
+ ],
+ [
+ "▁Tale",
+ -12.457234382629395
+ ],
+ [
+ "▁Buyer",
+ -12.457353591918945
+ ],
+ [
+ "ück",
+ -12.457578659057617
+ ],
+ [
+ "▁Kommentar",
+ -12.457720756530762
+ ],
+ [
+ "▁Schrift",
+ -12.457756996154785
+ ],
+ [
+ "Design",
+ -12.457792282104492
+ ],
+ [
+ "▁stirring",
+ -12.457937240600586
+ ],
+ [
+ "▁towels",
+ -12.457987785339355
+ ],
+ [
+ "▁$30",
+ -12.458101272583008
+ ],
+ [
+ "sprache",
+ -12.458279609680176
+ ],
+ [
+ "▁Regierung",
+ -12.458346366882324
+ ],
+ [
+ "▁nachhaltig",
+ -12.458406448364258
+ ],
+ [
+ "▁électronique",
+ -12.458515167236328
+ ],
+ [
+ "▁Andrei",
+ -12.458587646484375
+ ],
+ [
+ "because",
+ -12.458647727966309
+ ],
+ [
+ "informatique",
+ -12.458650588989258
+ ],
+ [
+ "IGHT",
+ -12.4586820602417
+ ],
+ [
+ "stepping",
+ -12.4586820602417
+ ],
+ [
+ "▁gris",
+ -12.458748817443848
+ ],
+ [
+ "vious",
+ -12.458773612976074
+ ],
+ [
+ "▁upside",
+ -12.4591064453125
+ ],
+ [
+ "▁Examples",
+ -12.459108352661133
+ ],
+ [
+ "IU",
+ -12.459110260009766
+ ],
+ [
+ "▁princess",
+ -12.459111213684082
+ ],
+ [
+ "spielen",
+ -12.45921516418457
+ ],
+ [
+ "legung",
+ -12.45950984954834
+ ],
+ [
+ "▁reflecting",
+ -12.4597806930542
+ ],
+ [
+ "▁Processing",
+ -12.459939002990723
+ ],
+ [
+ "▁jungle",
+ -12.460033416748047
+ ],
+ [
+ "▁insects",
+ -12.46006965637207
+ ],
+ [
+ "▁Sibiu",
+ -12.460220336914062
+ ],
+ [
+ "160",
+ -12.460259437561035
+ ],
+ [
+ "▁interessante",
+ -12.460267066955566
+ ],
+ [
+ "▁multimedia",
+ -12.460455894470215
+ ],
+ [
+ "essel",
+ -12.46049690246582
+ ],
+ [
+ "/18",
+ -12.460647583007812
+ ],
+ [
+ "nière",
+ -12.460683822631836
+ ],
+ [
+ "ministru",
+ -12.46072006225586
+ ],
+ [
+ "▁implants",
+ -12.460826873779297
+ ],
+ [
+ "▁Settings",
+ -12.461360931396484
+ ],
+ [
+ "▁invaluable",
+ -12.461432456970215
+ ],
+ [
+ "stains",
+ -12.461448669433594
+ ],
+ [
+ "onym",
+ -12.461518287658691
+ ],
+ [
+ "▁searched",
+ -12.461570739746094
+ ],
+ [
+ "▁disappointment",
+ -12.461628913879395
+ ],
+ [
+ "▁Iranian",
+ -12.461630821228027
+ ],
+ [
+ "▁questionnaire",
+ -12.461630821228027
+ ],
+ [
+ "Founder",
+ -12.46178913116455
+ ],
+ [
+ "▁Bericht",
+ -12.461792945861816
+ ],
+ [
+ "▁youngest",
+ -12.461896896362305
+ ],
+ [
+ "▁Automatic",
+ -12.461956024169922
+ ],
+ [
+ "▁plecat",
+ -12.46203327178955
+ ],
+ [
+ "geber",
+ -12.462119102478027
+ ],
+ [
+ "soweit",
+ -12.462124824523926
+ ],
+ [
+ "▁unfold",
+ -12.462236404418945
+ ],
+ [
+ "▁befinden",
+ -12.462274551391602
+ ],
+ [
+ "▁susţin",
+ -12.462637901306152
+ ],
+ [
+ "▁Mack",
+ -12.462675094604492
+ ],
+ [
+ "▁dificil",
+ -12.462757110595703
+ ],
+ [
+ "enseigne",
+ -12.463038444519043
+ ],
+ [
+ "▁vitamine",
+ -12.463047981262207
+ ],
+ [
+ "▁Memory",
+ -12.463092803955078
+ ],
+ [
+ "ripping",
+ -12.463129043579102
+ ],
+ [
+ "drin",
+ -12.463146209716797
+ ],
+ [
+ "3.2",
+ -12.463278770446777
+ ],
+ [
+ "▁verstehen",
+ -12.463287353515625
+ ],
+ [
+ "▁scaun",
+ -12.46341323852539
+ ],
+ [
+ "▁procédure",
+ -12.46380615234375
+ ],
+ [
+ "▁molecules",
+ -12.463911056518555
+ ],
+ [
+ "▁Anzahl",
+ -12.46391487121582
+ ],
+ [
+ "▁yogurt",
+ -12.464071273803711
+ ],
+ [
+ "▁Dominic",
+ -12.464113235473633
+ ],
+ [
+ "▁shocked",
+ -12.464156150817871
+ ],
+ [
+ "▁zilei",
+ -12.464269638061523
+ ],
+ [
+ "▁Heiz",
+ -12.464412689208984
+ ],
+ [
+ "▁Educational",
+ -12.464571952819824
+ ],
+ [
+ "BN",
+ -12.464577674865723
+ ],
+ [
+ "analyzing",
+ -12.464601516723633
+ ],
+ [
+ "hair",
+ -12.464676856994629
+ ],
+ [
+ "spiegel",
+ -12.464871406555176
+ ],
+ [
+ "▁illusion",
+ -12.464889526367188
+ ],
+ [
+ "BG",
+ -12.46505355834961
+ ],
+ [
+ "deductible",
+ -12.46513557434082
+ ],
+ [
+ "▁adj",
+ -12.4651460647583
+ ],
+ [
+ "▁accessory",
+ -12.465166091918945
+ ],
+ [
+ "▁Draw",
+ -12.465167999267578
+ ],
+ [
+ "▁airlines",
+ -12.46518611907959
+ ],
+ [
+ "▁satisfai",
+ -12.46536636352539
+ ],
+ [
+ "▁architects",
+ -12.465447425842285
+ ],
+ [
+ "istische",
+ -12.465508460998535
+ ],
+ [
+ "▁Healthy",
+ -12.465539932250977
+ ],
+ [
+ "großer",
+ -12.465669631958008
+ ],
+ [
+ "▁comunicare",
+ -12.465764999389648
+ ],
+ [
+ "▁Meyer",
+ -12.46577262878418
+ ],
+ [
+ "▁reproduction",
+ -12.465882301330566
+ ],
+ [
+ "▁Manufacturing",
+ -12.465929985046387
+ ],
+ [
+ "immobilier",
+ -12.465930938720703
+ ],
+ [
+ "▁Unterschied",
+ -12.465958595275879
+ ],
+ [
+ "▁cumpara",
+ -12.466029167175293
+ ],
+ [
+ "▁duplicate",
+ -12.466094017028809
+ ],
+ [
+ "▁(16",
+ -12.466096878051758
+ ],
+ [
+ "▁detector",
+ -12.466279983520508
+ ],
+ [
+ "▁observat",
+ -12.466387748718262
+ ],
+ [
+ "▁1965",
+ -12.466682434082031
+ ],
+ [
+ "▁Fantasy",
+ -12.466728210449219
+ ],
+ [
+ "▁brauchen",
+ -12.466728210449219
+ ],
+ [
+ "▁Participants",
+ -12.466780662536621
+ ],
+ [
+ "▁décide",
+ -12.466817855834961
+ ],
+ [
+ "▁kicke",
+ -12.466819763183594
+ ],
+ [
+ "▁SSL",
+ -12.466885566711426
+ ],
+ [
+ "360",
+ -12.466989517211914
+ ],
+ [
+ "Anim",
+ -12.467019081115723
+ ],
+ [
+ "▁cupcake",
+ -12.467031478881836
+ ],
+ [
+ "▁Lamb",
+ -12.467107772827148
+ ],
+ [
+ "▁Sä",
+ -12.467155456542969
+ ],
+ [
+ "ntă",
+ -12.46738052368164
+ ],
+ [
+ "▁Pig",
+ -12.467421531677246
+ ],
+ [
+ "1,000",
+ -12.467677116394043
+ ],
+ [
+ "nhof",
+ -12.467782020568848
+ ],
+ [
+ "▁discret",
+ -12.467947959899902
+ ],
+ [
+ "▁deloc",
+ -12.467991828918457
+ ],
+ [
+ "▁Bücher",
+ -12.467999458312988
+ ],
+ [
+ "chor",
+ -12.468042373657227
+ ],
+ [
+ "course",
+ -12.468070030212402
+ ],
+ [
+ "▁cough",
+ -12.468076705932617
+ ],
+ [
+ "▁erstellt",
+ -12.468087196350098
+ ],
+ [
+ "▁Than",
+ -12.468097686767578
+ ],
+ [
+ "stätte",
+ -12.46812915802002
+ ],
+ [
+ "▁exceptionally",
+ -12.468162536621094
+ ],
+ [
+ "▁semnal",
+ -12.468186378479004
+ ],
+ [
+ "▁Interessen",
+ -12.468329429626465
+ ],
+ [
+ "ле",
+ -12.468356132507324
+ ],
+ [
+ "xx",
+ -12.468402862548828
+ ],
+ [
+ "▁Veterans",
+ -12.468422889709473
+ ],
+ [
+ "▁Kreuz",
+ -12.468683242797852
+ ],
+ [
+ "▁Nachricht",
+ -12.468701362609863
+ ],
+ [
+ "treated",
+ -12.468894004821777
+ ],
+ [
+ "▁tide",
+ -12.469230651855469
+ ],
+ [
+ "▁nonetheless",
+ -12.469390869140625
+ ],
+ [
+ "▁Subject",
+ -12.469439506530762
+ ],
+ [
+ "▁Stau",
+ -12.469440460205078
+ ],
+ [
+ "▁stickers",
+ -12.469463348388672
+ ],
+ [
+ "Alp",
+ -12.46950912475586
+ ],
+ [
+ "▁flagship",
+ -12.469541549682617
+ ],
+ [
+ "▁trimite",
+ -12.469619750976562
+ ],
+ [
+ "▁polyester",
+ -12.469664573669434
+ ],
+ [
+ "▁locui",
+ -12.469671249389648
+ ],
+ [
+ "▁chili",
+ -12.46968936920166
+ ],
+ [
+ "▁Browser",
+ -12.469808578491211
+ ],
+ [
+ "sieg",
+ -12.469809532165527
+ ],
+ [
+ "▁Arabic",
+ -12.469876289367676
+ ],
+ [
+ "blich",
+ -12.47001838684082
+ ],
+ [
+ "▁wunderbar",
+ -12.470090866088867
+ ],
+ [
+ "▁furnishings",
+ -12.470210075378418
+ ],
+ [
+ "rtie",
+ -12.470243453979492
+ ],
+ [
+ "8.5",
+ -12.470742225646973
+ ],
+ [
+ "▁Sponsor",
+ -12.471016883850098
+ ],
+ [
+ "▁glitter",
+ -12.471280097961426
+ ],
+ [
+ "▁piaț",
+ -12.471402168273926
+ ],
+ [
+ "▁interviewed",
+ -12.471519470214844
+ ],
+ [
+ "▁Statistics",
+ -12.471529006958008
+ ],
+ [
+ "▁cerc",
+ -12.47154712677002
+ ],
+ [
+ "augmentation",
+ -12.47155475616455
+ ],
+ [
+ "▁Navi",
+ -12.471558570861816
+ ],
+ [
+ "▁Begriff",
+ -12.47156047821045
+ ],
+ [
+ "▁știu",
+ -12.471596717834473
+ ],
+ [
+ "▁unabhängig",
+ -12.471778869628906
+ ],
+ [
+ "▁könnten",
+ -12.471978187561035
+ ],
+ [
+ "▁travaille",
+ -12.472000122070312
+ ],
+ [
+ "▁companie",
+ -12.472027778625488
+ ],
+ [
+ "▁Scientific",
+ -12.472061157226562
+ ],
+ [
+ "▁Outlook",
+ -12.472091674804688
+ ],
+ [
+ "▁fairy",
+ -12.472158432006836
+ ],
+ [
+ "zam",
+ -12.472282409667969
+ ],
+ [
+ "bak",
+ -12.472448348999023
+ ],
+ [
+ "▁Traffic",
+ -12.472596168518066
+ ],
+ [
+ "gerät",
+ -12.472671508789062
+ ],
+ [
+ "▁freezing",
+ -12.472701072692871
+ ],
+ [
+ "▁broadband",
+ -12.4727201461792
+ ],
+ [
+ "110",
+ -12.47279167175293
+ ],
+ [
+ "▁revenu",
+ -12.472887992858887
+ ],
+ [
+ "listed",
+ -12.472900390625
+ ],
+ [
+ "▁Rico",
+ -12.472941398620605
+ ],
+ [
+ "Laure",
+ -12.472990036010742
+ ],
+ [
+ "ATA",
+ -12.473112106323242
+ ],
+ [
+ "▁participer",
+ -12.47313117980957
+ ],
+ [
+ "▁sponsorship",
+ -12.473235130310059
+ ],
+ [
+ "▁distress",
+ -12.473286628723145
+ ],
+ [
+ "▁Brisbane",
+ -12.47339916229248
+ ],
+ [
+ "schönen",
+ -12.473437309265137
+ ],
+ [
+ "▁fizice",
+ -12.473465919494629
+ ],
+ [
+ "▁Political",
+ -12.47362232208252
+ ],
+ [
+ "uhr",
+ -12.473657608032227
+ ],
+ [
+ "▁procedura",
+ -12.473713874816895
+ ],
+ [
+ "▁hervor",
+ -12.473770141601562
+ ],
+ [
+ "melted",
+ -12.473776817321777
+ ],
+ [
+ "▁Emp",
+ -12.47384262084961
+ ],
+ [
+ "▁Ernährung",
+ -12.4739351272583
+ ],
+ [
+ "▁Pendant",
+ -12.473944664001465
+ ],
+ [
+ "▁recipients",
+ -12.474047660827637
+ ],
+ [
+ "Claude",
+ -12.474133491516113
+ ],
+ [
+ "▁regimen",
+ -12.47415828704834
+ ],
+ [
+ "expo",
+ -12.474346160888672
+ ],
+ [
+ "adevăr",
+ -12.47437858581543
+ ],
+ [
+ "▁critically",
+ -12.474440574645996
+ ],
+ [
+ "▁grabbe",
+ -12.474468231201172
+ ],
+ [
+ "▁Kann",
+ -12.474474906921387
+ ],
+ [
+ "▁directeur",
+ -12.474613189697266
+ ],
+ [
+ "gator",
+ -12.474908828735352
+ ],
+ [
+ "problem",
+ -12.474910736083984
+ ],
+ [
+ "scribe",
+ -12.474913597106934
+ ],
+ [
+ "▁exig",
+ -12.474920272827148
+ ],
+ [
+ "Tri",
+ -12.474969863891602
+ ],
+ [
+ "▁aqua",
+ -12.475631713867188
+ ],
+ [
+ "appréci",
+ -12.47569465637207
+ ],
+ [
+ "▁viaţă",
+ -12.47571849822998
+ ],
+ [
+ "▁dominate",
+ -12.475865364074707
+ ],
+ [
+ "disc",
+ -12.475889205932617
+ ],
+ [
+ "▁conseiller",
+ -12.47603988647461
+ ],
+ [
+ "▁shuttle",
+ -12.476180076599121
+ ],
+ [
+ "▁Status",
+ -12.47623062133789
+ ],
+ [
+ "▁ausreichend",
+ -12.476371765136719
+ ],
+ [
+ "▁spät",
+ -12.476411819458008
+ ],
+ [
+ "▁remainder",
+ -12.476417541503906
+ ],
+ [
+ "wett",
+ -12.476430892944336
+ ],
+ [
+ "schlossen",
+ -12.476491928100586
+ ],
+ [
+ "PAC",
+ -12.476505279541016
+ ],
+ [
+ "▁suprafata",
+ -12.476617813110352
+ ],
+ [
+ "5.000",
+ -12.476673126220703
+ ],
+ [
+ "supplying",
+ -12.47673225402832
+ ],
+ [
+ "▁uniquely",
+ -12.476905822753906
+ ],
+ [
+ "▁retard",
+ -12.476929664611816
+ ],
+ [
+ "▁Bang",
+ -12.477006912231445
+ ],
+ [
+ "ieuse",
+ -12.477087020874023
+ ],
+ [
+ "▁Ted",
+ -12.477248191833496
+ ],
+ [
+ "▁ermöglichen",
+ -12.47732925415039
+ ],
+ [
+ "▁builders",
+ -12.477380752563477
+ ],
+ [
+ "▁proximité",
+ -12.477423667907715
+ ],
+ [
+ "▁unforgettable",
+ -12.477423667907715
+ ],
+ [
+ "256",
+ -12.477446556091309
+ ],
+ [
+ "fähigkeit",
+ -12.477550506591797
+ ],
+ [
+ "▁procurement",
+ -12.477561950683594
+ ],
+ [
+ "▁Gewicht",
+ -12.477693557739258
+ ],
+ [
+ "▁potentiel",
+ -12.47778606414795
+ ],
+ [
+ "▁topping",
+ -12.478300094604492
+ ],
+ [
+ "▁canada",
+ -12.478304862976074
+ ],
+ [
+ "▁Destin",
+ -12.478355407714844
+ ],
+ [
+ "▁Knowing",
+ -12.478411674499512
+ ],
+ [
+ "▁retained",
+ -12.478426933288574
+ ],
+ [
+ "▁zinc",
+ -12.478470802307129
+ ],
+ [
+ "▁worrying",
+ -12.478655815124512
+ ],
+ [
+ "faţa",
+ -12.478676795959473
+ ],
+ [
+ "▁initi",
+ -12.478837966918945
+ ],
+ [
+ "ORI",
+ -12.4788818359375
+ ],
+ [
+ "▁refuz",
+ -12.478921890258789
+ ],
+ [
+ "bruch",
+ -12.479202270507812
+ ],
+ [
+ "▁impun",
+ -12.479233741760254
+ ],
+ [
+ "▁persoană",
+ -12.479308128356934
+ ],
+ [
+ "EAR",
+ -12.479347229003906
+ ],
+ [
+ "bedarf",
+ -12.479368209838867
+ ],
+ [
+ "▁Gebiet",
+ -12.47940731048584
+ ],
+ [
+ "▁Roof",
+ -12.479436874389648
+ ],
+ [
+ "▁negligence",
+ -12.47957706451416
+ ],
+ [
+ "security",
+ -12.479618072509766
+ ],
+ [
+ "▁accesorii",
+ -12.479641914367676
+ ],
+ [
+ "▁unclear",
+ -12.479667663574219
+ ],
+ [
+ "▁securitate",
+ -12.479848861694336
+ ],
+ [
+ "▁spotlight",
+ -12.479896545410156
+ ],
+ [
+ "▁speziell",
+ -12.479923248291016
+ ],
+ [
+ "▁mentally",
+ -12.479942321777344
+ ],
+ [
+ "▁preservation",
+ -12.48011589050293
+ ],
+ [
+ "▁Promotion",
+ -12.480156898498535
+ ],
+ [
+ "partnered",
+ -12.480274200439453
+ ],
+ [
+ "▁Hinter",
+ -12.48031997680664
+ ],
+ [
+ "▁punishment",
+ -12.480359077453613
+ ],
+ [
+ "▁grease",
+ -12.480713844299316
+ ],
+ [
+ "▁NW",
+ -12.480714797973633
+ ],
+ [
+ "▁curse",
+ -12.480897903442383
+ ],
+ [
+ "ckle",
+ -12.48101806640625
+ ],
+ [
+ "▁Hire",
+ -12.481043815612793
+ ],
+ [
+ "▁Whole",
+ -12.481088638305664
+ ],
+ [
+ "▁basse",
+ -12.481289863586426
+ ],
+ [
+ "▁DNS",
+ -12.481427192687988
+ ],
+ [
+ "flamm",
+ -12.481560707092285
+ ],
+ [
+ "▁scoop",
+ -12.481574058532715
+ ],
+ [
+ "Norm",
+ -12.481663703918457
+ ],
+ [
+ "▁Surgery",
+ -12.481735229492188
+ ],
+ [
+ "▁widget",
+ -12.481741905212402
+ ],
+ [
+ "connected",
+ -12.481863021850586
+ ],
+ [
+ "autorité",
+ -12.481961250305176
+ ],
+ [
+ "▁utilis",
+ -12.482096672058105
+ ],
+ [
+ "▁formă",
+ -12.482185363769531
+ ],
+ [
+ "▁clearing",
+ -12.482307434082031
+ ],
+ [
+ "▁jumătate",
+ -12.482815742492676
+ ],
+ [
+ "größe",
+ -12.482831954956055
+ ],
+ [
+ "▁Tief",
+ -12.482852935791016
+ ],
+ [
+ "épi",
+ -12.482939720153809
+ ],
+ [
+ "zunehmen",
+ -12.483174324035645
+ ],
+ [
+ "▁touchdown",
+ -12.48318099975586
+ ],
+ [
+ "▁scholarships",
+ -12.483236312866211
+ ],
+ [
+ "▁dementia",
+ -12.483319282531738
+ ],
+ [
+ "▁Jeder",
+ -12.48333740234375
+ ],
+ [
+ "▁nightmare",
+ -12.483379364013672
+ ],
+ [
+ "▁Raw",
+ -12.48342514038086
+ ],
+ [
+ "absorbed",
+ -12.483468055725098
+ ],
+ [
+ "lohnt",
+ -12.483484268188477
+ ],
+ [
+ "quent",
+ -12.483580589294434
+ ],
+ [
+ "interest",
+ -12.483626365661621
+ ],
+ [
+ "OSS",
+ -12.483649253845215
+ ],
+ [
+ "▁Leaf",
+ -12.483667373657227
+ ],
+ [
+ "▁timeless",
+ -12.48381519317627
+ ],
+ [
+ "DY",
+ -12.483865737915039
+ ],
+ [
+ "▁Remote",
+ -12.483907699584961
+ ],
+ [
+ "chner",
+ -12.483938217163086
+ ],
+ [
+ "▁Pam",
+ -12.484014511108398
+ ],
+ [
+ "urban",
+ -12.484060287475586
+ ],
+ [
+ "во",
+ -12.484146118164062
+ ],
+ [
+ "▁Kunde",
+ -12.484166145324707
+ ],
+ [
+ "▁Laptop",
+ -12.484169006347656
+ ],
+ [
+ "finder",
+ -12.484336853027344
+ ],
+ [
+ "▁Pole",
+ -12.484567642211914
+ ],
+ [
+ "2.8",
+ -12.484588623046875
+ ],
+ [
+ "finished",
+ -12.484670639038086
+ ],
+ [
+ "▁prophet",
+ -12.484697341918945
+ ],
+ [
+ "mailed",
+ -12.484758377075195
+ ],
+ [
+ "2-0",
+ -12.4849214553833
+ ],
+ [
+ "▁disciples",
+ -12.484949111938477
+ ],
+ [
+ "▁intriguing",
+ -12.484980583190918
+ ],
+ [
+ "IRA",
+ -12.485033988952637
+ ],
+ [
+ "petit",
+ -12.485077857971191
+ ],
+ [
+ "▁Membership",
+ -12.485097885131836
+ ],
+ [
+ "▁provincial",
+ -12.485177040100098
+ ],
+ [
+ "▁Prüfung",
+ -12.485292434692383
+ ],
+ [
+ "-50",
+ -12.485450744628906
+ ],
+ [
+ "▁cryptocurrency",
+ -12.485522270202637
+ ],
+ [
+ "▁journalism",
+ -12.485536575317383
+ ],
+ [
+ "▁Downtown",
+ -12.485593795776367
+ ],
+ [
+ "inserted",
+ -12.485655784606934
+ ],
+ [
+ "▁Direction",
+ -12.485718727111816
+ ],
+ [
+ "lipid",
+ -12.485732078552246
+ ],
+ [
+ "▁Sebastian",
+ -12.485793113708496
+ ],
+ [
+ "fordert",
+ -12.48591136932373
+ ],
+ [
+ "Originally",
+ -12.485989570617676
+ ],
+ [
+ "tipp",
+ -12.486048698425293
+ ],
+ [
+ "verantwortlich",
+ -12.486064910888672
+ ],
+ [
+ "▁wheelchair",
+ -12.486085891723633
+ ],
+ [
+ "▁structura",
+ -12.48609733581543
+ ],
+ [
+ "▁Danny",
+ -12.486138343811035
+ ],
+ [
+ "999",
+ -12.486284255981445
+ ],
+ [
+ "▁Schiff",
+ -12.486380577087402
+ ],
+ [
+ "formally",
+ -12.486408233642578
+ ],
+ [
+ "focused",
+ -12.486428260803223
+ ],
+ [
+ "▁Vater",
+ -12.486478805541992
+ ],
+ [
+ "▁Dear",
+ -12.486599922180176
+ ],
+ [
+ "▁reinforce",
+ -12.486794471740723
+ ],
+ [
+ "proprietar",
+ -12.48690414428711
+ ],
+ [
+ "▁Kyle",
+ -12.487004280090332
+ ],
+ [
+ "În",
+ -12.487015724182129
+ ],
+ [
+ "▁servir",
+ -12.487268447875977
+ ],
+ [
+ "length",
+ -12.48730754852295
+ ],
+ [
+ "▁showroom",
+ -12.48735237121582
+ ],
+ [
+ "reli",
+ -12.487473487854004
+ ],
+ [
+ "▁Brü",
+ -12.487529754638672
+ ],
+ [
+ "▁Schle",
+ -12.487634658813477
+ ],
+ [
+ "▁profond",
+ -12.487773895263672
+ ],
+ [
+ "▁Superior",
+ -12.487826347351074
+ ],
+ [
+ "▁lifted",
+ -12.487844467163086
+ ],
+ [
+ "highlighting",
+ -12.487850189208984
+ ],
+ [
+ "▁Connection",
+ -12.48793888092041
+ ],
+ [
+ "▁similarly",
+ -12.487998962402344
+ ],
+ [
+ "▁diferit",
+ -12.488005638122559
+ ],
+ [
+ "▁sweater",
+ -12.488014221191406
+ ],
+ [
+ "État",
+ -12.48803997039795
+ ],
+ [
+ "rooted",
+ -12.488069534301758
+ ],
+ [
+ "▁sleeves",
+ -12.488236427307129
+ ],
+ [
+ "де",
+ -12.488264083862305
+ ],
+ [
+ "▁Laboratory",
+ -12.488265991210938
+ ],
+ [
+ "ündig",
+ -12.488719940185547
+ ],
+ [
+ "▁Viking",
+ -12.488741874694824
+ ],
+ [
+ "▁Origin",
+ -12.48878002166748
+ ],
+ [
+ "▁vibr",
+ -12.488812446594238
+ ],
+ [
+ "199",
+ -12.488974571228027
+ ],
+ [
+ "▁yummy",
+ -12.489001274108887
+ ],
+ [
+ "STAR",
+ -12.489140510559082
+ ],
+ [
+ "▁repro",
+ -12.489152908325195
+ ],
+ [
+ "▁Kirchen",
+ -12.489229202270508
+ ],
+ [
+ "hopper",
+ -12.48925495147705
+ ],
+ [
+ "zza",
+ -12.489335060119629
+ ],
+ [
+ "▁vitesse",
+ -12.48934555053711
+ ],
+ [
+ "▁minimalist",
+ -12.489412307739258
+ ],
+ [
+ "▁Election",
+ -12.489420890808105
+ ],
+ [
+ "draw",
+ -12.489501953125
+ ],
+ [
+ "▁candles",
+ -12.48959732055664
+ ],
+ [
+ "▁Mund",
+ -12.489615440368652
+ ],
+ [
+ "urged",
+ -12.489901542663574
+ ],
+ [
+ "▁cânt",
+ -12.489917755126953
+ ],
+ [
+ "Ultimately",
+ -12.49002742767334
+ ],
+ [
+ "▁Lift",
+ -12.490124702453613
+ ],
+ [
+ "loaded",
+ -12.490334510803223
+ ],
+ [
+ "demand",
+ -12.490508079528809
+ ],
+ [
+ "▁aleg",
+ -12.490621566772461
+ ],
+ [
+ "▁Discovery",
+ -12.490755081176758
+ ],
+ [
+ "▁Vienna",
+ -12.490960121154785
+ ],
+ [
+ "▁Kategorie",
+ -12.490961074829102
+ ],
+ [
+ "▁Cotton",
+ -12.490962028503418
+ ],
+ [
+ "▁$200",
+ -12.491043090820312
+ ],
+ [
+ "▁Drei",
+ -12.491052627563477
+ ],
+ [
+ "▁reicht",
+ -12.491168975830078
+ ],
+ [
+ "speicher",
+ -12.491231918334961
+ ],
+ [
+ "▁Immobilien",
+ -12.491483688354492
+ ],
+ [
+ "gefühl",
+ -12.491509437561035
+ ],
+ [
+ "make",
+ -12.491525650024414
+ ],
+ [
+ "pell",
+ -12.49155044555664
+ ],
+ [
+ "▁dull",
+ -12.491598129272461
+ ],
+ [
+ "▁arbeitet",
+ -12.491681098937988
+ ],
+ [
+ "retaining",
+ -12.491700172424316
+ ],
+ [
+ "losen",
+ -12.491707801818848
+ ],
+ [
+ "match",
+ -12.491876602172852
+ ],
+ [
+ "-60",
+ -12.491880416870117
+ ],
+ [
+ "▁ecological",
+ -12.492000579833984
+ ],
+ [
+ "▁vend",
+ -12.492051124572754
+ ],
+ [
+ "▁grammar",
+ -12.492061614990234
+ ],
+ [
+ "▁1:1",
+ -12.492225646972656
+ ],
+ [
+ "grilled",
+ -12.492279052734375
+ ],
+ [
+ "geordnet",
+ -12.492321014404297
+ ],
+ [
+ "▁Pav",
+ -12.49236011505127
+ ],
+ [
+ "▁Depot",
+ -12.492368698120117
+ ],
+ [
+ "▁Walking",
+ -12.492372512817383
+ ],
+ [
+ "teamed",
+ -12.492402076721191
+ ],
+ [
+ "▁torque",
+ -12.492537498474121
+ ],
+ [
+ "▁Venture",
+ -12.492659568786621
+ ],
+ [
+ "▁beginner",
+ -12.49269962310791
+ ],
+ [
+ "▁Monaten",
+ -12.492712020874023
+ ],
+ [
+ "▁Pune",
+ -12.493054389953613
+ ],
+ [
+ "connect",
+ -12.493075370788574
+ ],
+ [
+ "▁textbook",
+ -12.493132591247559
+ ],
+ [
+ "▁unprecedented",
+ -12.49314022064209
+ ],
+ [
+ "▁implied",
+ -12.493168830871582
+ ],
+ [
+ "▁cubic",
+ -12.493668556213379
+ ],
+ [
+ "enthält",
+ -12.493696212768555
+ ],
+ [
+ "▁Brenn",
+ -12.49388313293457
+ ],
+ [
+ "▁Expect",
+ -12.49394416809082
+ ],
+ [
+ "▁lever",
+ -12.4939603805542
+ ],
+ [
+ "veux",
+ -12.49399185180664
+ ],
+ [
+ "▁Claire",
+ -12.494112968444824
+ ],
+ [
+ "Acc",
+ -12.49432373046875
+ ],
+ [
+ "▁Typ",
+ -12.494478225708008
+ ],
+ [
+ "▁smoothie",
+ -12.494501113891602
+ ],
+ [
+ "▁Idaho",
+ -12.494780540466309
+ ],
+ [
+ "▁spati",
+ -12.494802474975586
+ ],
+ [
+ "▁bénéficier",
+ -12.49488353729248
+ ],
+ [
+ "▁Kle",
+ -12.495161056518555
+ ],
+ [
+ "▁serviciilor",
+ -12.495169639587402
+ ],
+ [
+ "▁prohibit",
+ -12.495267868041992
+ ],
+ [
+ "EAD",
+ -12.495417594909668
+ ],
+ [
+ "▁Turner",
+ -12.495418548583984
+ ],
+ [
+ "▁elibera",
+ -12.49543571472168
+ ],
+ [
+ "▁payday",
+ -12.495464324951172
+ ],
+ [
+ "▁prolong",
+ -12.495466232299805
+ ],
+ [
+ "▁sued",
+ -12.495481491088867
+ ],
+ [
+ "▁Devil",
+ -12.495536804199219
+ ],
+ [
+ "▁Skills",
+ -12.495552062988281
+ ],
+ [
+ "▁Marcel",
+ -12.495553970336914
+ ],
+ [
+ "▁silhouette",
+ -12.495601654052734
+ ],
+ [
+ "▁preț",
+ -12.495742797851562
+ ],
+ [
+ "▁Gö",
+ -12.495747566223145
+ ],
+ [
+ "▁Creator",
+ -12.495774269104004
+ ],
+ [
+ "fed",
+ -12.4959077835083
+ ],
+ [
+ "Cap",
+ -12.495997428894043
+ ],
+ [
+ "▁dedicate",
+ -12.496042251586914
+ ],
+ [
+ "0000",
+ -12.496124267578125
+ ],
+ [
+ "▁VAT",
+ -12.496259689331055
+ ],
+ [
+ "▁Firefox",
+ -12.496443748474121
+ ],
+ [
+ "▁therapies",
+ -12.496477127075195
+ ],
+ [
+ "▁screws",
+ -12.496662139892578
+ ],
+ [
+ "▁Province",
+ -12.496697425842285
+ ],
+ [
+ "▁problematic",
+ -12.496871948242188
+ ],
+ [
+ "▁Vid",
+ -12.496915817260742
+ ],
+ [
+ "▁Lost",
+ -12.496950149536133
+ ],
+ [
+ "▁elegance",
+ -12.497520446777344
+ ],
+ [
+ "▁Elegant",
+ -12.497525215148926
+ ],
+ [
+ "ignant",
+ -12.497573852539062
+ ],
+ [
+ "▁darin",
+ -12.497649192810059
+ ],
+ [
+ "▁anonym",
+ -12.497669219970703
+ ],
+ [
+ "▁vegeta",
+ -12.49767780303955
+ ],
+ [
+ "incoming",
+ -12.497762680053711
+ ],
+ [
+ "▁pills",
+ -12.497846603393555
+ ],
+ [
+ "governing",
+ -12.497893333435059
+ ],
+ [
+ "▁Haven",
+ -12.497920989990234
+ ],
+ [
+ "paper",
+ -12.497947692871094
+ ],
+ [
+ "räume",
+ -12.497979164123535
+ ],
+ [
+ "paw",
+ -12.498099327087402
+ ],
+ [
+ "▁spelling",
+ -12.498283386230469
+ ],
+ [
+ "ambele",
+ -12.498318672180176
+ ],
+ [
+ "▁reprezentat",
+ -12.498371124267578
+ ],
+ [
+ "▁mâ",
+ -12.49853515625
+ ],
+ [
+ "wirtschaftliche",
+ -12.498558044433594
+ ],
+ [
+ "▁valabil",
+ -12.498579025268555
+ ],
+ [
+ "▁konkret",
+ -12.498618125915527
+ ],
+ [
+ "▁financier",
+ -12.498619079589844
+ ],
+ [
+ "▁irre",
+ -12.499135971069336
+ ],
+ [
+ "▁Silicon",
+ -12.499171257019043
+ ],
+ [
+ "Viv",
+ -12.499181747436523
+ ],
+ [
+ "▁viruses",
+ -12.49927043914795
+ ],
+ [
+ "▁CNN",
+ -12.499324798583984
+ ],
+ [
+ "▁erleben",
+ -12.499482154846191
+ ],
+ [
+ "gina",
+ -12.499492645263672
+ ],
+ [
+ "punctul",
+ -12.49951457977295
+ ],
+ [
+ "▁Sfânt",
+ -12.499753952026367
+ ],
+ [
+ "▁Manage",
+ -12.499811172485352
+ ],
+ [
+ "▁payable",
+ -12.499984741210938
+ ],
+ [
+ "▁practitioner",
+ -12.500143051147461
+ ],
+ [
+ "▁conférence",
+ -12.50026798248291
+ ],
+ [
+ "▁drought",
+ -12.50027084350586
+ ],
+ [
+ "▁devote",
+ -12.500361442565918
+ ],
+ [
+ "wertung",
+ -12.500420570373535
+ ],
+ [
+ "stabil",
+ -12.5004301071167
+ ],
+ [
+ "▁balcon",
+ -12.500553131103516
+ ],
+ [
+ "▁Lebensmittel",
+ -12.500603675842285
+ ],
+ [
+ "COL",
+ -12.500950813293457
+ ],
+ [
+ "▁Domnul",
+ -12.501093864440918
+ ],
+ [
+ "carved",
+ -12.501359939575195
+ ],
+ [
+ "▁preparat",
+ -12.5014009475708
+ ],
+ [
+ "101",
+ -12.501537322998047
+ ],
+ [
+ "▁specimen",
+ -12.501580238342285
+ ],
+ [
+ "urgeon",
+ -12.501596450805664
+ ],
+ [
+ "LIC",
+ -12.50163459777832
+ ],
+ [
+ "Plattform",
+ -12.501643180847168
+ ],
+ [
+ "▁ramas",
+ -12.501739501953125
+ ],
+ [
+ "▁copilului",
+ -12.501791954040527
+ ],
+ [
+ "bacter",
+ -12.501812934875488
+ ],
+ [
+ "körper",
+ -12.501940727233887
+ ],
+ [
+ "▁Kru",
+ -12.501981735229492
+ ],
+ [
+ "▁Employ",
+ -12.502055168151855
+ ],
+ [
+ "office",
+ -12.502080917358398
+ ],
+ [
+ "▁simmer",
+ -12.502120018005371
+ ],
+ [
+ "qualität",
+ -12.502137184143066
+ ],
+ [
+ "▁freshly",
+ -12.502215385437012
+ ],
+ [
+ "▁Nine",
+ -12.50223159790039
+ ],
+ [
+ "▁tonnes",
+ -12.50223445892334
+ ],
+ [
+ "boden",
+ -12.502236366271973
+ ],
+ [
+ "enquête",
+ -12.50240707397461
+ ],
+ [
+ "▁Colour",
+ -12.502481460571289
+ ],
+ [
+ "▁Diagram",
+ -12.502495765686035
+ ],
+ [
+ "▁gewählt",
+ -12.502516746520996
+ ],
+ [
+ "▁viitoare",
+ -12.502538681030273
+ ],
+ [
+ "▁reporters",
+ -12.502913475036621
+ ],
+ [
+ "guer",
+ -12.502991676330566
+ ],
+ [
+ "▁Kombination",
+ -12.503021240234375
+ ],
+ [
+ "▁qualitative",
+ -12.50302505493164
+ ],
+ [
+ "Centrul",
+ -12.503131866455078
+ ],
+ [
+ "avy",
+ -12.503170013427734
+ ],
+ [
+ "▁Eng",
+ -12.503175735473633
+ ],
+ [
+ "▁sufletul",
+ -12.50327205657959
+ ],
+ [
+ "▁germ",
+ -12.503412246704102
+ ],
+ [
+ "▁prevented",
+ -12.503448486328125
+ ],
+ [
+ "appelle",
+ -12.503533363342285
+ ],
+ [
+ "gins",
+ -12.503556251525879
+ ],
+ [
+ "▁Skype",
+ -12.503585815429688
+ ],
+ [
+ "conditioned",
+ -12.503617286682129
+ ],
+ [
+ "▁clutch",
+ -12.503641128540039
+ ],
+ [
+ "environ",
+ -12.503694534301758
+ ],
+ [
+ "3.3",
+ -12.503774642944336
+ ],
+ [
+ "▁webinar",
+ -12.503866195678711
+ ],
+ [
+ "▁forty",
+ -12.504104614257812
+ ],
+ [
+ "▁Medicaid",
+ -12.504127502441406
+ ],
+ [
+ "▁dismissed",
+ -12.504167556762695
+ ],
+ [
+ "▁siblings",
+ -12.504168510437012
+ ],
+ [
+ "▁Jaw",
+ -12.504196166992188
+ ],
+ [
+ "guiding",
+ -12.504220962524414
+ ],
+ [
+ "cigarette",
+ -12.504374504089355
+ ],
+ [
+ "▁Shah",
+ -12.504681587219238
+ ],
+ [
+ "▁Lehrer",
+ -12.504684448242188
+ ],
+ [
+ "▁muscular",
+ -12.504694938659668
+ ],
+ [
+ "spatele",
+ -12.504796981811523
+ ],
+ [
+ "▁réduction",
+ -12.504836082458496
+ ],
+ [
+ "▁fixes",
+ -12.504851341247559
+ ],
+ [
+ "Span",
+ -12.50511646270752
+ ],
+ [
+ "▁Hudson",
+ -12.505231857299805
+ ],
+ [
+ "development",
+ -12.505250930786133
+ ],
+ [
+ "▁excluded",
+ -12.50525951385498
+ ],
+ [
+ "Democrat",
+ -12.505260467529297
+ ],
+ [
+ "▁nominal",
+ -12.505317687988281
+ ],
+ [
+ "purpose",
+ -12.50540828704834
+ ],
+ [
+ "▁bored",
+ -12.505500793457031
+ ],
+ [
+ "espèce",
+ -12.50550651550293
+ ],
+ [
+ "▁(30",
+ -12.5055570602417
+ ],
+ [
+ "Neither",
+ -12.505608558654785
+ ],
+ [
+ "hänge",
+ -12.505610466003418
+ ],
+ [
+ "square",
+ -12.505728721618652
+ ],
+ [
+ "voller",
+ -12.505736351013184
+ ],
+ [
+ "▁pertinent",
+ -12.505783081054688
+ ],
+ [
+ "▁Wool",
+ -12.50595474243164
+ ],
+ [
+ "settling",
+ -12.50607681274414
+ ],
+ [
+ "fangen",
+ -12.506148338317871
+ ],
+ [
+ "▁Testing",
+ -12.506152153015137
+ ],
+ [
+ "distin",
+ -12.506196022033691
+ ],
+ [
+ "▁Marken",
+ -12.506227493286133
+ ],
+ [
+ "▁Beta",
+ -12.506300926208496
+ ],
+ [
+ "▁fulfilling",
+ -12.506339073181152
+ ],
+ [
+ "Leider",
+ -12.506357192993164
+ ],
+ [
+ "black",
+ -12.506389617919922
+ ],
+ [
+ "occupe",
+ -12.50658893585205
+ ],
+ [
+ "itățile",
+ -12.506688117980957
+ ],
+ [
+ "Pay",
+ -12.506887435913086
+ ],
+ [
+ "▁bandwidth",
+ -12.506890296936035
+ ],
+ [
+ "▁neighbourhood",
+ -12.506918907165527
+ ],
+ [
+ "▁Gutschein",
+ -12.506922721862793
+ ],
+ [
+ "degree",
+ -12.507055282592773
+ ],
+ [
+ "ivité",
+ -12.507116317749023
+ ],
+ [
+ "4.1",
+ -12.507169723510742
+ ],
+ [
+ "▁tätig",
+ -12.507170677185059
+ ],
+ [
+ "topic",
+ -12.507242202758789
+ ],
+ [
+ "ätz",
+ -12.507243156433105
+ ],
+ [
+ "these",
+ -12.50733470916748
+ ],
+ [
+ "▁propriété",
+ -12.507438659667969
+ ],
+ [
+ "▁innings",
+ -12.507458686828613
+ ],
+ [
+ "▁Prevention",
+ -12.50754165649414
+ ],
+ [
+ "▁Saw",
+ -12.507585525512695
+ ],
+ [
+ "▁opener",
+ -12.507752418518066
+ ],
+ [
+ "entwicklung",
+ -12.507824897766113
+ ],
+ [
+ "▁Johann",
+ -12.507865905761719
+ ],
+ [
+ "▁statistic",
+ -12.507881164550781
+ ],
+ [
+ "oids",
+ -12.507966995239258
+ ],
+ [
+ "▁Delaware",
+ -12.508000373840332
+ ],
+ [
+ "▁Isle",
+ -12.508001327514648
+ ],
+ [
+ "▁accompagn",
+ -12.508028984069824
+ ],
+ [
+ "▁Risiko",
+ -12.508079528808594
+ ],
+ [
+ "▁Conform",
+ -12.508268356323242
+ ],
+ [
+ "zeichnen",
+ -12.508395195007324
+ ],
+ [
+ "▁acuz",
+ -12.508479118347168
+ ],
+ [
+ "▁Mort",
+ -12.508524894714355
+ ],
+ [
+ "Fällen",
+ -12.50853157043457
+ ],
+ [
+ "▁blended",
+ -12.50871467590332
+ ],
+ [
+ "found",
+ -12.50872802734375
+ ],
+ [
+ "▁gestalten",
+ -12.50874137878418
+ ],
+ [
+ "▁Découvrez",
+ -12.508830070495605
+ ],
+ [
+ "▁Wett",
+ -12.508956909179688
+ ],
+ [
+ "▁débat",
+ -12.508990287780762
+ ],
+ [
+ "▁Tire",
+ -12.509007453918457
+ ],
+ [
+ "benz",
+ -12.509037017822266
+ ],
+ [
+ "Yes",
+ -12.509074211120605
+ ],
+ [
+ "▁pierde",
+ -12.509110450744629
+ ],
+ [
+ "▁niciodata",
+ -12.509121894836426
+ ],
+ [
+ "▁precipit",
+ -12.509145736694336
+ ],
+ [
+ "▁lazy",
+ -12.509334564208984
+ ],
+ [
+ "▁creature",
+ -12.509370803833008
+ ],
+ [
+ "Wettbewerb",
+ -12.509385108947754
+ ],
+ [
+ "▁Explo",
+ -12.509496688842773
+ ],
+ [
+ "wolf",
+ -12.509657859802246
+ ],
+ [
+ "▁conséquence",
+ -12.509662628173828
+ ],
+ [
+ "▁jewellery",
+ -12.509662628173828
+ ],
+ [
+ "▁Extension",
+ -12.509735107421875
+ ],
+ [
+ "▁transmitted",
+ -12.509872436523438
+ ],
+ [
+ "▁darker",
+ -12.509973526000977
+ ],
+ [
+ "▁simbol",
+ -12.510065078735352
+ ],
+ [
+ "kim",
+ -12.510069847106934
+ ],
+ [
+ "▁proteja",
+ -12.510098457336426
+ ],
+ [
+ "▁Copper",
+ -12.510189056396484
+ ],
+ [
+ "mitglied",
+ -12.510218620300293
+ ],
+ [
+ "▁explosive",
+ -12.510222434997559
+ ],
+ [
+ "▁Nicolae",
+ -12.510223388671875
+ ],
+ [
+ "▁intricate",
+ -12.510231971740723
+ ],
+ [
+ "lati",
+ -12.510313034057617
+ ],
+ [
+ "Mark",
+ -12.510334014892578
+ ],
+ [
+ "▁Porsche",
+ -12.510339736938477
+ ],
+ [
+ "▁Revenue",
+ -12.510479927062988
+ ],
+ [
+ "4.2",
+ -12.510613441467285
+ ],
+ [
+ "certain",
+ -12.510836601257324
+ ],
+ [
+ "▁Coaching",
+ -12.510879516601562
+ ],
+ [
+ "▁allocated",
+ -12.510879516601562
+ ],
+ [
+ "▁optimiz",
+ -12.511017799377441
+ ],
+ [
+ "▁heel",
+ -12.511205673217773
+ ],
+ [
+ "▁indigenous",
+ -12.511330604553223
+ ],
+ [
+ "▁vineri",
+ -12.511396408081055
+ ],
+ [
+ "▁Inspector",
+ -12.51145076751709
+ ],
+ [
+ "▁colleague",
+ -12.5115327835083
+ ],
+ [
+ "ANG",
+ -12.511649131774902
+ ],
+ [
+ "éducation",
+ -12.511887550354004
+ ],
+ [
+ "▁Geschenk",
+ -12.51188850402832
+ ],
+ [
+ "channel",
+ -12.511899948120117
+ ],
+ [
+ "▁trapped",
+ -12.511954307556152
+ ],
+ [
+ "BF",
+ -12.511974334716797
+ ],
+ [
+ "▁firing",
+ -12.512086868286133
+ ],
+ [
+ "▁chlor",
+ -12.512103080749512
+ ],
+ [
+ "▁Carlos",
+ -12.512115478515625
+ ],
+ [
+ "▁proxy",
+ -12.512128829956055
+ ],
+ [
+ "▁pinch",
+ -12.512167930603027
+ ],
+ [
+ "▁Pete",
+ -12.512201309204102
+ ],
+ [
+ "phospho",
+ -12.512458801269531
+ ],
+ [
+ "▁waiver",
+ -12.51246452331543
+ ],
+ [
+ "▁Croatia",
+ -12.512480735778809
+ ],
+ [
+ "▁behave",
+ -12.51258373260498
+ ],
+ [
+ "▁frig",
+ -12.512676239013672
+ ],
+ [
+ "▁Vorteil",
+ -12.51279067993164
+ ],
+ [
+ "▁wichtiger",
+ -12.512837409973145
+ ],
+ [
+ "........",
+ -12.512929916381836
+ ],
+ [
+ "▁flick",
+ -12.513007164001465
+ ],
+ [
+ "▁Stanford",
+ -12.51306438446045
+ ],
+ [
+ "öse",
+ -12.513096809387207
+ ],
+ [
+ "▁Fernseh",
+ -12.513099670410156
+ ],
+ [
+ "▁vélo",
+ -12.51322078704834
+ ],
+ [
+ "reisen",
+ -12.513304710388184
+ ],
+ [
+ "residing",
+ -12.513504981994629
+ ],
+ [
+ "▁Taste",
+ -12.513580322265625
+ ],
+ [
+ "▁disappeared",
+ -12.513630867004395
+ ],
+ [
+ "▁Hood",
+ -12.513776779174805
+ ],
+ [
+ "▁fabriqu",
+ -12.514046669006348
+ ],
+ [
+ "▁Jake",
+ -12.514470100402832
+ ],
+ [
+ "Lastly",
+ -12.51462173461914
+ ],
+ [
+ "▁furnace",
+ -12.514673233032227
+ ],
+ [
+ "▁Ottawa",
+ -12.51473331451416
+ ],
+ [
+ "▁dictate",
+ -12.514742851257324
+ ],
+ [
+ "zece",
+ -12.514817237854004
+ ],
+ [
+ "protect",
+ -12.514932632446289
+ ],
+ [
+ "FU",
+ -12.51495361328125
+ ],
+ [
+ "Stack",
+ -12.514954566955566
+ ],
+ [
+ "▁teilweise",
+ -12.515018463134766
+ ],
+ [
+ "▁Publisher",
+ -12.51506233215332
+ ],
+ [
+ "▁lutte",
+ -12.515159606933594
+ ],
+ [
+ "202",
+ -12.515178680419922
+ ],
+ [
+ "psy",
+ -12.515190124511719
+ ],
+ [
+ "▁wünschen",
+ -12.515238761901855
+ ],
+ [
+ "▁pathways",
+ -12.515356063842773
+ ],
+ [
+ "ivitate",
+ -12.515559196472168
+ ],
+ [
+ "▁continuă",
+ -12.515658378601074
+ ],
+ [
+ "ziemlich",
+ -12.515791893005371
+ ],
+ [
+ "verted",
+ -12.515812873840332
+ ],
+ [
+ "▁sequel",
+ -12.515839576721191
+ ],
+ [
+ "tinct",
+ -12.51599407196045
+ ],
+ [
+ "vette",
+ -12.516020774841309
+ ],
+ [
+ "▁exceeding",
+ -12.516032218933105
+ ],
+ [
+ "▁Yorkshire",
+ -12.51607608795166
+ ],
+ [
+ "▁cleanse",
+ -12.51613998413086
+ ],
+ [
+ "Sadly",
+ -12.516159057617188
+ ],
+ [
+ "▁präsentiert",
+ -12.516164779663086
+ ],
+ [
+ "angled",
+ -12.516311645507812
+ ],
+ [
+ "tude",
+ -12.516339302062988
+ ],
+ [
+ "chain",
+ -12.516371726989746
+ ],
+ [
+ "▁Oakland",
+ -12.51639175415039
+ ],
+ [
+ "xia",
+ -12.516514778137207
+ ],
+ [
+ "▁foremost",
+ -12.51653003692627
+ ],
+ [
+ "▁incomplete",
+ -12.516786575317383
+ ],
+ [
+ "▁restriction",
+ -12.516905784606934
+ ],
+ [
+ "▁whatsoever",
+ -12.516908645629883
+ ],
+ [
+ "▁shipment",
+ -12.517017364501953
+ ],
+ [
+ "**",
+ -12.517059326171875
+ ],
+ [
+ "Aici",
+ -12.517110824584961
+ ],
+ [
+ "PART",
+ -12.517247200012207
+ ],
+ [
+ "▁grams",
+ -12.517251014709473
+ ],
+ [
+ "▁Folk",
+ -12.517457008361816
+ ],
+ [
+ "▁encryption",
+ -12.517467498779297
+ ],
+ [
+ "▁Alfred",
+ -12.517748832702637
+ ],
+ [
+ "▁Veränderung",
+ -12.517749786376953
+ ],
+ [
+ "▁privately",
+ -12.517817497253418
+ ],
+ [
+ "£",
+ -12.517909049987793
+ ],
+ [
+ "▁Sonne",
+ -12.51799201965332
+ ],
+ [
+ "kow",
+ -12.518117904663086
+ ],
+ [
+ "▁CBS",
+ -12.518172264099121
+ ],
+ [
+ "▁Feuer",
+ -12.518198013305664
+ ],
+ [
+ "▁crushed",
+ -12.518230438232422
+ ],
+ [
+ "▁cazare",
+ -12.518270492553711
+ ],
+ [
+ "▁beraten",
+ -12.518401145935059
+ ],
+ [
+ "envoi",
+ -12.518423080444336
+ ],
+ [
+ "▁genannt",
+ -12.51843547821045
+ ],
+ [
+ "▁Lok",
+ -12.518472671508789
+ ],
+ [
+ "nox",
+ -12.518569946289062
+ ],
+ [
+ "wishing",
+ -12.518759727478027
+ ],
+ [
+ "▁freak",
+ -12.518759727478027
+ ],
+ [
+ "rasi",
+ -12.51879596710205
+ ],
+ [
+ "▁calculations",
+ -12.518888473510742
+ ],
+ [
+ "▁sprechen",
+ -12.51890754699707
+ ],
+ [
+ "5:00",
+ -12.519062042236328
+ ],
+ [
+ "▁Gam",
+ -12.519074440002441
+ ],
+ [
+ "▁invasion",
+ -12.519159317016602
+ ],
+ [
+ "ZA",
+ -12.519230842590332
+ ],
+ [
+ "aiming",
+ -12.519327163696289
+ ],
+ [
+ "▁näher",
+ -12.519404411315918
+ ],
+ [
+ "▁Maßnahmen",
+ -12.519433975219727
+ ],
+ [
+ "▁măsură",
+ -12.519490242004395
+ ],
+ [
+ "▁Bestellung",
+ -12.519610404968262
+ ],
+ [
+ "▁gown",
+ -12.519665718078613
+ ],
+ [
+ "▁oblige",
+ -12.519747734069824
+ ],
+ [
+ "länder",
+ -12.51977825164795
+ ],
+ [
+ "posi",
+ -12.519853591918945
+ ],
+ [
+ "▁Earn",
+ -12.51988410949707
+ ],
+ [
+ "▁dubl",
+ -12.51999282836914
+ ],
+ [
+ "▁sticky",
+ -12.520100593566895
+ ],
+ [
+ "▁litter",
+ -12.520181655883789
+ ],
+ [
+ "▁Salz",
+ -12.520257949829102
+ ],
+ [
+ "▁Matter",
+ -12.520272254943848
+ ],
+ [
+ "▁Driving",
+ -12.520275115966797
+ ],
+ [
+ "▁pursu",
+ -12.520285606384277
+ ],
+ [
+ "ographer",
+ -12.520390510559082
+ ],
+ [
+ "▁touring",
+ -12.520400047302246
+ ],
+ [
+ "opter",
+ -12.520444869995117
+ ],
+ [
+ "▁fierce",
+ -12.520475387573242
+ ],
+ [
+ "▁Audit",
+ -12.520480155944824
+ ],
+ [
+ "▁imperi",
+ -12.520755767822266
+ ],
+ [
+ "▁positiv",
+ -12.520780563354492
+ ],
+ [
+ "règles",
+ -12.520849227905273
+ ],
+ [
+ "▁bouton",
+ -12.520990371704102
+ ],
+ [
+ "▁victorie",
+ -12.520990371704102
+ ],
+ [
+ "▁manuel",
+ -12.521015167236328
+ ],
+ [
+ "▁await",
+ -12.52103042602539
+ ],
+ [
+ "▁transformer",
+ -12.521041870117188
+ ],
+ [
+ "▁cupboard",
+ -12.52108383178711
+ ],
+ [
+ "▁Hag",
+ -12.521117210388184
+ ],
+ [
+ "naj",
+ -12.521214485168457
+ ],
+ [
+ "▁annoncé",
+ -12.52139663696289
+ ],
+ [
+ "▁scolaire",
+ -12.521401405334473
+ ],
+ [
+ "▁étape",
+ -12.521482467651367
+ ],
+ [
+ "▁pirate",
+ -12.521761894226074
+ ],
+ [
+ "▁Rated",
+ -12.521794319152832
+ ],
+ [
+ "LOT",
+ -12.521846771240234
+ ],
+ [
+ "▁natura",
+ -12.521944046020508
+ ],
+ [
+ "oga",
+ -12.522336959838867
+ ],
+ [
+ "Read",
+ -12.522388458251953
+ ],
+ [
+ "idio",
+ -12.522444725036621
+ ],
+ [
+ "▁recession",
+ -12.522698402404785
+ ],
+ [
+ "veţi",
+ -12.522761344909668
+ ],
+ [
+ "▁blossom",
+ -12.523082733154297
+ ],
+ [
+ "▁lunar",
+ -12.523141860961914
+ ],
+ [
+ "▁inhibit",
+ -12.52316951751709
+ ],
+ [
+ "gemein",
+ -12.523219108581543
+ ],
+ [
+ "▁Historic",
+ -12.523262023925781
+ ],
+ [
+ "▁HTTP",
+ -12.523370742797852
+ ],
+ [
+ "misiune",
+ -12.5234956741333
+ ],
+ [
+ "▁Manda",
+ -12.523601531982422
+ ],
+ [
+ "▁Hurricane",
+ -12.523643493652344
+ ],
+ [
+ "Strat",
+ -12.523646354675293
+ ],
+ [
+ "▁populaire",
+ -12.523756980895996
+ ],
+ [
+ "▁useless",
+ -12.523762702941895
+ ],
+ [
+ "▁Leipzig",
+ -12.523924827575684
+ ],
+ [
+ "▁Krankheit",
+ -12.52392578125
+ ],
+ [
+ "▁Bonne",
+ -12.52397346496582
+ ],
+ [
+ "▁tissu",
+ -12.52399730682373
+ ],
+ [
+ "▁Baum",
+ -12.523998260498047
+ ],
+ [
+ "▁BUT",
+ -12.524152755737305
+ ],
+ [
+ "▁Mondial",
+ -12.52423095703125
+ ],
+ [
+ "▁triangle",
+ -12.524242401123047
+ ],
+ [
+ "▁Tesla",
+ -12.524250984191895
+ ],
+ [
+ "▁pământ",
+ -12.52430534362793
+ ],
+ [
+ "▁aminte",
+ -12.524726867675781
+ ],
+ [
+ "▁vehicul",
+ -12.524770736694336
+ ],
+ [
+ "▁cerut",
+ -12.52482795715332
+ ],
+ [
+ "▁respiratory",
+ -12.524836540222168
+ ],
+ [
+ "▁rayon",
+ -12.524993896484375
+ ],
+ [
+ "▁gestaltet",
+ -12.525067329406738
+ ],
+ [
+ "310",
+ -12.525139808654785
+ ],
+ [
+ "pfl",
+ -12.525239944458008
+ ],
+ [
+ "▁shrimp",
+ -12.525337219238281
+ ],
+ [
+ "▁reconnu",
+ -12.525409698486328
+ ],
+ [
+ "ologique",
+ -12.525476455688477
+ ],
+ [
+ "▁unity",
+ -12.525674819946289
+ ],
+ [
+ "Speicher",
+ -12.52569580078125
+ ],
+ [
+ "▁Movement",
+ -12.525794982910156
+ ],
+ [
+ "ddling",
+ -12.52581787109375
+ ],
+ [
+ "OE",
+ -12.525818824768066
+ ],
+ [
+ "▁Resolution",
+ -12.525863647460938
+ ],
+ [
+ "esteem",
+ -12.525898933410645
+ ],
+ [
+ "▁Teen",
+ -12.526288986206055
+ ],
+ [
+ "▁believing",
+ -12.526463508605957
+ ],
+ [
+ "▁Tipps",
+ -12.526481628417969
+ ],
+ [
+ "jpg",
+ -12.526494026184082
+ ],
+ [
+ "▁obs",
+ -12.526519775390625
+ ],
+ [
+ "SHA",
+ -12.526702880859375
+ ],
+ [
+ "▁quietly",
+ -12.526907920837402
+ ],
+ [
+ "setting",
+ -12.52712345123291
+ ],
+ [
+ "▁elevator",
+ -12.527185440063477
+ ],
+ [
+ "phor",
+ -12.527194023132324
+ ],
+ [
+ "Just",
+ -12.52725887298584
+ ],
+ [
+ "▁legatura",
+ -12.52739143371582
+ ],
+ [
+ "elected",
+ -12.527414321899414
+ ],
+ [
+ "▁disclosed",
+ -12.527419090270996
+ ],
+ [
+ "quarter",
+ -12.52743148803711
+ ],
+ [
+ "zzy",
+ -12.527461051940918
+ ],
+ [
+ "▁gata",
+ -12.527491569519043
+ ],
+ [
+ "SAN",
+ -12.527532577514648
+ ],
+ [
+ "▁Cathedral",
+ -12.527592658996582
+ ],
+ [
+ "192",
+ -12.527656555175781
+ ],
+ [
+ "▁RBI",
+ -12.527726173400879
+ ],
+ [
+ "▁Seller",
+ -12.527798652648926
+ ],
+ [
+ "▁urine",
+ -12.527807235717773
+ ],
+ [
+ "▁Hardware",
+ -12.527966499328613
+ ],
+ [
+ "▁steadi",
+ -12.527993202209473
+ ],
+ [
+ "percussion",
+ -12.528158187866211
+ ],
+ [
+ "▁francez",
+ -12.528172492980957
+ ],
+ [
+ "▁rude",
+ -12.528202056884766
+ ],
+ [
+ "bod",
+ -12.528223037719727
+ ],
+ [
+ "cession",
+ -12.528249740600586
+ ],
+ [
+ "▁HTC",
+ -12.528372764587402
+ ],
+ [
+ "HB",
+ -12.528576850891113
+ ],
+ [
+ "▁descent",
+ -12.528644561767578
+ ],
+ [
+ "▁Painting",
+ -12.528681755065918
+ ],
+ [
+ "119",
+ -12.528684616088867
+ ],
+ [
+ "sagen",
+ -12.52877426147461
+ ],
+ [
+ "▁salvation",
+ -12.52880573272705
+ ],
+ [
+ "arro",
+ -12.528814315795898
+ ],
+ [
+ "0.3",
+ -12.52886962890625
+ ],
+ [
+ "▁Duck",
+ -12.52890396118164
+ ],
+ [
+ "Mit",
+ -12.529052734375
+ ],
+ [
+ "да",
+ -12.52927017211914
+ ],
+ [
+ "▁Diesel",
+ -12.529322624206543
+ ],
+ [
+ "▁Medal",
+ -12.529413223266602
+ ],
+ [
+ "▁interim",
+ -12.529439926147461
+ ],
+ [
+ "▁montagne",
+ -12.529439926147461
+ ],
+ [
+ "▁Pixel",
+ -12.529631614685059
+ ],
+ [
+ "LINE",
+ -12.529806137084961
+ ],
+ [
+ "▁dureri",
+ -12.529938697814941
+ ],
+ [
+ "▁Bengal",
+ -12.529990196228027
+ ],
+ [
+ "Legea",
+ -12.530080795288086
+ ],
+ [
+ "▁Strecke",
+ -12.530094146728516
+ ],
+ [
+ "▁schneller",
+ -12.53012752532959
+ ],
+ [
+ "▁Karten",
+ -12.5301513671875
+ ],
+ [
+ "cion",
+ -12.530241966247559
+ ],
+ [
+ "▁Coco",
+ -12.53037166595459
+ ],
+ [
+ "troisième",
+ -12.53052806854248
+ ],
+ [
+ "401",
+ -12.530616760253906
+ ],
+ [
+ "▁sandwiches",
+ -12.530704498291016
+ ],
+ [
+ "▁folosind",
+ -12.530920028686523
+ ],
+ [
+ "▁Folgen",
+ -12.530953407287598
+ ],
+ [
+ "▁triumph",
+ -12.530991554260254
+ ],
+ [
+ "▁Hintergrund",
+ -12.530996322631836
+ ],
+ [
+ "▁revelation",
+ -12.531084060668945
+ ],
+ [
+ "ôme",
+ -12.531222343444824
+ ],
+ [
+ "▁Nex",
+ -12.531245231628418
+ ],
+ [
+ "jährigen",
+ -12.531295776367188
+ ],
+ [
+ "▁militant",
+ -12.531296730041504
+ ],
+ [
+ "▁fabricant",
+ -12.531671524047852
+ ],
+ [
+ "iano",
+ -12.531713485717773
+ ],
+ [
+ "▁formulation",
+ -12.53188705444336
+ ],
+ [
+ "integrating",
+ -12.532050132751465
+ ],
+ [
+ "▁Items",
+ -12.532142639160156
+ ],
+ [
+ "▁contractual",
+ -12.532320976257324
+ ],
+ [
+ "AIDS",
+ -12.532424926757812
+ ],
+ [
+ "▁pitcher",
+ -12.532610893249512
+ ],
+ [
+ "▁Snap",
+ -12.532623291015625
+ ],
+ [
+ "▁systematic",
+ -12.532663345336914
+ ],
+ [
+ "▁referendum",
+ -12.532694816589355
+ ],
+ [
+ "gau",
+ -12.53281021118164
+ ],
+ [
+ "administration",
+ -12.532917022705078
+ ],
+ [
+ "▁speci",
+ -12.532981872558594
+ ],
+ [
+ "ieni",
+ -12.532998085021973
+ ],
+ [
+ "prox",
+ -12.533186912536621
+ ],
+ [
+ "▁bouquet",
+ -12.533241271972656
+ ],
+ [
+ "▁sinnvoll",
+ -12.533270835876465
+ ],
+ [
+ "▁Fleisch",
+ -12.533309936523438
+ ],
+ [
+ "ktuell",
+ -12.533381462097168
+ ],
+ [
+ "▁mushrooms",
+ -12.533408164978027
+ ],
+ [
+ "▁Straf",
+ -12.533470153808594
+ ],
+ [
+ "▁cresc",
+ -12.533491134643555
+ ],
+ [
+ "TEM",
+ -12.533502578735352
+ ],
+ [
+ "▁vindec",
+ -12.53352165222168
+ ],
+ [
+ "▁Drama",
+ -12.533540725708008
+ ],
+ [
+ "chief",
+ -12.533550262451172
+ ],
+ [
+ "▁müsst",
+ -12.533614158630371
+ ],
+ [
+ "▁Warner",
+ -12.533662796020508
+ ],
+ [
+ "118",
+ -12.533761024475098
+ ],
+ [
+ "▁saptamana",
+ -12.533831596374512
+ ],
+ [
+ "▁animaux",
+ -12.53412914276123
+ ],
+ [
+ "▁Directory",
+ -12.534146308898926
+ ],
+ [
+ "▁entgegen",
+ -12.53415584564209
+ ],
+ [
+ "▁deduction",
+ -12.534156799316406
+ ],
+ [
+ "▁Strategic",
+ -12.53426456451416
+ ],
+ [
+ "▁rats",
+ -12.534419059753418
+ ],
+ [
+ "▁Moses",
+ -12.534448623657227
+ ],
+ [
+ "eko",
+ -12.534564971923828
+ ],
+ [
+ "strict",
+ -12.534590721130371
+ ],
+ [
+ "▁Ashley",
+ -12.534603118896484
+ ],
+ [
+ "mik",
+ -12.534622192382812
+ ],
+ [
+ "▁relocate",
+ -12.534668922424316
+ ],
+ [
+ "▁whip",
+ -12.534738540649414
+ ],
+ [
+ "central",
+ -12.534750938415527
+ ],
+ [
+ "mack",
+ -12.534892082214355
+ ],
+ [
+ "stufe",
+ -12.534961700439453
+ ],
+ [
+ "▁Metropolitan",
+ -12.5349702835083
+ ],
+ [
+ "▁croissance",
+ -12.534974098205566
+ ],
+ [
+ "▁celebrities",
+ -12.535021781921387
+ ],
+ [
+ "▁Geh",
+ -12.53507137298584
+ ],
+ [
+ "▁verifica",
+ -12.535196304321289
+ ],
+ [
+ "▁satisfac",
+ -12.535211563110352
+ ],
+ [
+ "▁Julian",
+ -12.535271644592285
+ ],
+ [
+ "▁remotely",
+ -12.535432815551758
+ ],
+ [
+ "▁Safari",
+ -12.535542488098145
+ ],
+ [
+ "▁Chic",
+ -12.53557014465332
+ ],
+ [
+ "▁clamp",
+ -12.535818099975586
+ ],
+ [
+ "▁Schnee",
+ -12.535918235778809
+ ],
+ [
+ "grown",
+ -12.536069869995117
+ ],
+ [
+ "▁Character",
+ -12.536110877990723
+ ],
+ [
+ "▁charities",
+ -12.536137580871582
+ ],
+ [
+ "Thankfully",
+ -12.536625862121582
+ ],
+ [
+ "▁țară",
+ -12.53681468963623
+ ],
+ [
+ "IZ",
+ -12.536816596984863
+ ],
+ [
+ "Vielleicht",
+ -12.536999702453613
+ ],
+ [
+ "▁Pon",
+ -12.537108421325684
+ ],
+ [
+ "gegen",
+ -12.53711986541748
+ ],
+ [
+ "chez",
+ -12.537185668945312
+ ],
+ [
+ "Black",
+ -12.537544250488281
+ ],
+ [
+ "▁alimentare",
+ -12.537555694580078
+ ],
+ [
+ "▁verloren",
+ -12.537562370300293
+ ],
+ [
+ "▁predictions",
+ -12.537657737731934
+ ],
+ [
+ "Founded",
+ -12.53795337677002
+ ],
+ [
+ "▁femeie",
+ -12.538022994995117
+ ],
+ [
+ "wahrscheinlich",
+ -12.538107872009277
+ ],
+ [
+ "▁squeeze",
+ -12.53819465637207
+ ],
+ [
+ "▁verfügbar",
+ -12.538259506225586
+ ],
+ [
+ "▁hygiene",
+ -12.538393020629883
+ ],
+ [
+ "voire",
+ -12.538667678833008
+ ],
+ [
+ "▁birou",
+ -12.538901329040527
+ ],
+ [
+ "▁initiate",
+ -12.538921356201172
+ ],
+ [
+ "▁Patriot",
+ -12.539009094238281
+ ],
+ [
+ "▁Income",
+ -12.539159774780273
+ ],
+ [
+ "▁marry",
+ -12.539310455322266
+ ],
+ [
+ "lokal",
+ -12.539336204528809
+ ],
+ [
+ "logic",
+ -12.53940486907959
+ ],
+ [
+ "▁Abstract",
+ -12.53966236114502
+ ],
+ [
+ "▁grundsätzlich",
+ -12.539822578430176
+ ],
+ [
+ "▁tariff",
+ -12.539886474609375
+ ],
+ [
+ "▁definitiv",
+ -12.539892196655273
+ ],
+ [
+ "paz",
+ -12.53989315032959
+ ],
+ [
+ "Result",
+ -12.539921760559082
+ ],
+ [
+ "1:30",
+ -12.54005241394043
+ ],
+ [
+ "▁Latest",
+ -12.540075302124023
+ ],
+ [
+ "▁Dauer",
+ -12.540155410766602
+ ],
+ [
+ "Med",
+ -12.540275573730469
+ ],
+ [
+ "gewicht",
+ -12.540348052978516
+ ],
+ [
+ "▁Gaza",
+ -12.540430068969727
+ ],
+ [
+ "▁Newton",
+ -12.540769577026367
+ ],
+ [
+ "Dokument",
+ -12.540897369384766
+ ],
+ [
+ "formular",
+ -12.540945053100586
+ ],
+ [
+ "ILE",
+ -12.540964126586914
+ ],
+ [
+ "▁surse",
+ -12.541040420532227
+ ],
+ [
+ "MH",
+ -12.54116153717041
+ ],
+ [
+ "▁Arctic",
+ -12.541255950927734
+ ],
+ [
+ "▁ISBN",
+ -12.541274070739746
+ ],
+ [
+ "▁quarterback",
+ -12.541315078735352
+ ],
+ [
+ "▁absurd",
+ -12.541555404663086
+ ],
+ [
+ "▁Zusammenhang",
+ -12.541561126708984
+ ],
+ [
+ "▁Module",
+ -12.54156494140625
+ ],
+ [
+ "mented",
+ -12.541667938232422
+ ],
+ [
+ "worthy",
+ -12.541797637939453
+ ],
+ [
+ "▁célèbre",
+ -12.541828155517578
+ ],
+ [
+ "▁maritime",
+ -12.541836738586426
+ ],
+ [
+ "▁Reed",
+ -12.541938781738281
+ ],
+ [
+ "▁threaten",
+ -12.542037010192871
+ ],
+ [
+ "▁Satz",
+ -12.542095184326172
+ ],
+ [
+ "▁sticking",
+ -12.542203903198242
+ ],
+ [
+ "▁transcript",
+ -12.542372703552246
+ ],
+ [
+ "▁Morgen",
+ -12.542425155639648
+ ],
+ [
+ "▁Förder",
+ -12.542435646057129
+ ],
+ [
+ "▁Gottes",
+ -12.542572021484375
+ ],
+ [
+ "▁Coordinator",
+ -12.542648315429688
+ ],
+ [
+ "LOG",
+ -12.54265022277832
+ ],
+ [
+ "EAN",
+ -12.542677879333496
+ ],
+ [
+ "▁préparation",
+ -12.54273509979248
+ ],
+ [
+ "▁Brass",
+ -12.542799949645996
+ ],
+ [
+ "Așa",
+ -12.542853355407715
+ ],
+ [
+ "▁Utiliz",
+ -12.54294490814209
+ ],
+ [
+ "framed",
+ -12.542973518371582
+ ],
+ [
+ "▁asphalt",
+ -12.543050765991211
+ ],
+ [
+ "116",
+ -12.543061256408691
+ ],
+ [
+ "▁historically",
+ -12.54310417175293
+ ],
+ [
+ "▁doamn",
+ -12.543176651000977
+ ],
+ [
+ "Air",
+ -12.543293952941895
+ ],
+ [
+ "▁economist",
+ -12.543838500976562
+ ],
+ [
+ "fresh",
+ -12.54384994506836
+ ],
+ [
+ "engine",
+ -12.543906211853027
+ ],
+ [
+ "▁Rücken",
+ -12.543919563293457
+ ],
+ [
+ "▁worthwhile",
+ -12.544124603271484
+ ],
+ [
+ "▁Therapie",
+ -12.544140815734863
+ ],
+ [
+ "▁Joshua",
+ -12.544151306152344
+ ],
+ [
+ "sicherheit",
+ -12.544175148010254
+ ],
+ [
+ "▁scena",
+ -12.544254302978516
+ ],
+ [
+ "ifiant",
+ -12.54433822631836
+ ],
+ [
+ "/20",
+ -12.54442024230957
+ ],
+ [
+ "fehl",
+ -12.544469833374023
+ ],
+ [
+ "karten",
+ -12.544515609741211
+ ],
+ [
+ "501",
+ -12.544656753540039
+ ],
+ [
+ "▁vide",
+ -12.544673919677734
+ ],
+ [
+ "▁miliarde",
+ -12.544699668884277
+ ],
+ [
+ "▁trillion",
+ -12.54470157623291
+ ],
+ [
+ "oudre",
+ -12.544761657714844
+ ],
+ [
+ "nderung",
+ -12.544803619384766
+ ],
+ [
+ "▁inquiries",
+ -12.544992446899414
+ ],
+ [
+ "▁echipe",
+ -12.545034408569336
+ ],
+ [
+ "▁investiga",
+ -12.545040130615234
+ ],
+ [
+ "▁detailing",
+ -12.545042991638184
+ ],
+ [
+ "VIS",
+ -12.545086860656738
+ ],
+ [
+ "▁geographical",
+ -12.545157432556152
+ ],
+ [
+ "▁authentication",
+ -12.54519271850586
+ ],
+ [
+ "▁Schwa",
+ -12.545201301574707
+ ],
+ [
+ "▁Scri",
+ -12.545230865478516
+ ],
+ [
+ "▁discourage",
+ -12.54527473449707
+ ],
+ [
+ "Pass",
+ -12.54529094696045
+ ],
+ [
+ "▁scattered",
+ -12.54529857635498
+ ],
+ [
+ "▁langsam",
+ -12.545300483703613
+ ],
+ [
+ "telles",
+ -12.545380592346191
+ ],
+ [
+ "▁ramane",
+ -12.5454740524292
+ ],
+ [
+ "▁inhibitor",
+ -12.545486450195312
+ ],
+ [
+ "▁Habit",
+ -12.54556941986084
+ ],
+ [
+ "▁10:00",
+ -12.545577049255371
+ ],
+ [
+ "▁rezultat",
+ -12.545595169067383
+ ],
+ [
+ "äck",
+ -12.545943260192871
+ ],
+ [
+ ",000.",
+ -12.545979499816895
+ ],
+ [
+ "▁remedies",
+ -12.546103477478027
+ ],
+ [
+ "▁comportament",
+ -12.546195983886719
+ ],
+ [
+ "namen",
+ -12.546229362487793
+ ],
+ [
+ "▁#3",
+ -12.546327590942383
+ ],
+ [
+ "enstein",
+ -12.546493530273438
+ ],
+ [
+ "▁relevance",
+ -12.546516418457031
+ ],
+ [
+ "▁présentation",
+ -12.54655933380127
+ ],
+ [
+ "MHz",
+ -12.546648979187012
+ ],
+ [
+ "EMA",
+ -12.546661376953125
+ ],
+ [
+ "▁palace",
+ -12.546709060668945
+ ],
+ [
+ "▁vizibil",
+ -12.546723365783691
+ ],
+ [
+ "▁griev",
+ -12.546820640563965
+ ],
+ [
+ "▁severely",
+ -12.54688549041748
+ ],
+ [
+ "expert",
+ -12.546942710876465
+ ],
+ [
+ "▁ravi",
+ -12.54696273803711
+ ],
+ [
+ "▁feasible",
+ -12.547002792358398
+ ],
+ [
+ "▁Wholesale",
+ -12.547009468078613
+ ],
+ [
+ "▁graduat",
+ -12.547077178955078
+ ],
+ [
+ "Kü",
+ -12.547094345092773
+ ],
+ [
+ "▁quotation",
+ -12.547157287597656
+ ],
+ [
+ "/11",
+ -12.54716968536377
+ ],
+ [
+ "lutter",
+ -12.547415733337402
+ ],
+ [
+ "▁dice",
+ -12.547467231750488
+ ],
+ [
+ "modal",
+ -12.547749519348145
+ ],
+ [
+ "ggling",
+ -12.547819137573242
+ ],
+ [
+ "▁considér",
+ -12.547986030578613
+ ],
+ [
+ "▁Insel",
+ -12.548097610473633
+ ],
+ [
+ "▁Database",
+ -12.5483980178833
+ ],
+ [
+ "icism",
+ -12.548508644104004
+ ],
+ [
+ "▁quarterly",
+ -12.54851245880127
+ ],
+ [
+ "▁formule",
+ -12.548558235168457
+ ],
+ [
+ "▁renouvel",
+ -12.54873275756836
+ ],
+ [
+ "▁Treasure",
+ -12.548737525939941
+ ],
+ [
+ "▁1962",
+ -12.548844337463379
+ ],
+ [
+ "▁republic",
+ -12.549111366271973
+ ],
+ [
+ "▁États",
+ -12.549254417419434
+ ],
+ [
+ "▁salut",
+ -12.549356460571289
+ ],
+ [
+ "HK",
+ -12.54941463470459
+ ],
+ [
+ "▁Bali",
+ -12.549427032470703
+ ],
+ [
+ "▁Rechnung",
+ -12.549447059631348
+ ],
+ [
+ "fruit",
+ -12.54945182800293
+ ],
+ [
+ "lays",
+ -12.549467086791992
+ ],
+ [
+ "LAS",
+ -12.54951000213623
+ ],
+ [
+ "inclin",
+ -12.549708366394043
+ ],
+ [
+ "▁Cré",
+ -12.549813270568848
+ ],
+ [
+ "▁compt",
+ -12.54985237121582
+ ],
+ [
+ "țiilor",
+ -12.550056457519531
+ ],
+ [
+ "heft",
+ -12.550111770629883
+ ],
+ [
+ "▁Comisi",
+ -12.55024242401123
+ ],
+ [
+ "▁Nurse",
+ -12.550516128540039
+ ],
+ [
+ "loid",
+ -12.550540924072266
+ ],
+ [
+ "grove",
+ -12.550761222839355
+ ],
+ [
+ "▁Copy",
+ -12.550867080688477
+ ],
+ [
+ "▁Kampf",
+ -12.550873756408691
+ ],
+ [
+ "izată",
+ -12.550945281982422
+ ],
+ [
+ "würdig",
+ -12.551244735717773
+ ],
+ [
+ "-2018",
+ -12.551305770874023
+ ],
+ [
+ "ozo",
+ -12.551350593566895
+ ],
+ [
+ "▁integriert",
+ -12.551397323608398
+ ],
+ [
+ "▁réunion",
+ -12.551448822021484
+ ],
+ [
+ "▁mică",
+ -12.551520347595215
+ ],
+ [
+ "▁Chau",
+ -12.551595687866211
+ ],
+ [
+ "▁allegations",
+ -12.551626205444336
+ ],
+ [
+ "▁shaping",
+ -12.551640510559082
+ ],
+ [
+ "▁transcription",
+ -12.551671981811523
+ ],
+ [
+ "▁Monica",
+ -12.551711082458496
+ ],
+ [
+ "▁torture",
+ -12.551795959472656
+ ],
+ [
+ "▁cooperative",
+ -12.551962852478027
+ ],
+ [
+ "▁invité",
+ -12.551987648010254
+ ],
+ [
+ "▁bamboo",
+ -12.552204132080078
+ ],
+ [
+ "▁Thinking",
+ -12.55232048034668
+ ],
+ [
+ "▁gratis",
+ -12.552392959594727
+ ],
+ [
+ "117",
+ -12.55267333984375
+ ],
+ [
+ "renz",
+ -12.55279541015625
+ ],
+ [
+ "▁Fußball",
+ -12.552823066711426
+ ],
+ [
+ "▁Gram",
+ -12.552873611450195
+ ],
+ [
+ "sprung",
+ -12.55290412902832
+ ],
+ [
+ "▁Schluss",
+ -12.55308723449707
+ ],
+ [
+ "▁Diploma",
+ -12.553345680236816
+ ],
+ [
+ "▁apparatus",
+ -12.553363800048828
+ ],
+ [
+ "notably",
+ -12.553483963012695
+ ],
+ [
+ "▁exercit",
+ -12.553532600402832
+ ],
+ [
+ "ământ",
+ -12.553536415100098
+ ],
+ [
+ "▁masses",
+ -12.553610801696777
+ ],
+ [
+ "▁preuve",
+ -12.553642272949219
+ ],
+ [
+ "great",
+ -12.553754806518555
+ ],
+ [
+ "▁Drink",
+ -12.553792953491211
+ ],
+ [
+ "islam",
+ -12.553828239440918
+ ],
+ [
+ "ARM",
+ -12.553914070129395
+ ],
+ [
+ "indre",
+ -12.554404258728027
+ ],
+ [
+ "DW",
+ -12.554410934448242
+ ],
+ [
+ "▁Flowers",
+ -12.554500579833984
+ ],
+ [
+ "▁pill",
+ -12.554574966430664
+ ],
+ [
+ "▁objectifs",
+ -12.554594039916992
+ ],
+ [
+ "▁Bezug",
+ -12.554659843444824
+ ],
+ [
+ "▁assumptions",
+ -12.55466365814209
+ ],
+ [
+ "▁vesti",
+ -12.554742813110352
+ ],
+ [
+ "route",
+ -12.554783821105957
+ ],
+ [
+ "▁Bangkok",
+ -12.554815292358398
+ ],
+ [
+ "▁seamlessly",
+ -12.55482006072998
+ ],
+ [
+ "config",
+ -12.554882049560547
+ ],
+ [
+ "▁username",
+ -12.554890632629395
+ ],
+ [
+ "unsure",
+ -12.555024147033691
+ ],
+ [
+ "▁poser",
+ -12.555129051208496
+ ],
+ [
+ "▁impozit",
+ -12.555246353149414
+ ],
+ [
+ "▁metode",
+ -12.555333137512207
+ ],
+ [
+ "defending",
+ -12.555347442626953
+ ],
+ [
+ "▁Nic",
+ -12.555431365966797
+ ],
+ [
+ "▁Vertrag",
+ -12.555508613586426
+ ],
+ [
+ "▁plăcut",
+ -12.55552864074707
+ ],
+ [
+ "▁Pou",
+ -12.555675506591797
+ ],
+ [
+ "UCH",
+ -12.555785179138184
+ ],
+ [
+ "▁Fein",
+ -12.555903434753418
+ ],
+ [
+ "reading",
+ -12.555994987487793
+ ],
+ [
+ "snip",
+ -12.55604076385498
+ ],
+ [
+ "▁Livre",
+ -12.556401252746582
+ ],
+ [
+ "lander",
+ -12.556509971618652
+ ],
+ [
+ "▁hydraulic",
+ -12.556559562683105
+ ],
+ [
+ "veiled",
+ -12.556563377380371
+ ],
+ [
+ "intr",
+ -12.556609153747559
+ ],
+ [
+ "▁Domnului",
+ -12.556641578674316
+ ],
+ [
+ "▁$0.",
+ -12.556713104248047
+ ],
+ [
+ "▁kilometers",
+ -12.556753158569336
+ ],
+ [
+ "spann",
+ -12.556870460510254
+ ],
+ [
+ "▁credibility",
+ -12.556892395019531
+ ],
+ [
+ "▁eBook",
+ -12.556953430175781
+ ],
+ [
+ "VERY",
+ -12.556994438171387
+ ],
+ [
+ "▁Charm",
+ -12.557122230529785
+ ],
+ [
+ "Evangeli",
+ -12.557193756103516
+ ],
+ [
+ "▁anderer",
+ -12.557193756103516
+ ],
+ [
+ "▁Entry",
+ -12.557195663452148
+ ],
+ [
+ "ffy",
+ -12.5573148727417
+ ],
+ [
+ "▁Exc",
+ -12.55737018585205
+ ],
+ [
+ "▁Omega",
+ -12.557446479797363
+ ],
+ [
+ "▁Funktionen",
+ -12.557455062866211
+ ],
+ [
+ "▁Gay",
+ -12.55752182006836
+ ],
+ [
+ "▁acht",
+ -12.557608604431152
+ ],
+ [
+ "colored",
+ -12.557615280151367
+ ],
+ [
+ "itude",
+ -12.557634353637695
+ ],
+ [
+ "▁accompagné",
+ -12.557645797729492
+ ],
+ [
+ "▁unfortunate",
+ -12.557981491088867
+ ],
+ [
+ "▁DIN",
+ -12.558091163635254
+ ],
+ [
+ "▁installment",
+ -12.558252334594727
+ ],
+ [
+ "▁indépendant",
+ -12.558307647705078
+ ],
+ [
+ "These",
+ -12.558364868164062
+ ],
+ [
+ "mitten",
+ -12.558394432067871
+ ],
+ [
+ "thank",
+ -12.558470726013184
+ ],
+ [
+ "▁Trek",
+ -12.558721542358398
+ ],
+ [
+ "üchte",
+ -12.55874252319336
+ ],
+ [
+ "▁cuir",
+ -12.55875015258789
+ ],
+ [
+ "▁turbo",
+ -12.558802604675293
+ ],
+ [
+ "Table",
+ -12.558847427368164
+ ],
+ [
+ "▁Extrem",
+ -12.558866500854492
+ ],
+ [
+ "▁advertisements",
+ -12.55915355682373
+ ],
+ [
+ "▁chaîne",
+ -12.559206008911133
+ ],
+ [
+ "▁corridor",
+ -12.559473991394043
+ ],
+ [
+ "▁râ",
+ -12.559651374816895
+ ],
+ [
+ "▁Opening",
+ -12.559718132019043
+ ],
+ [
+ "Get",
+ -12.559747695922852
+ ],
+ [
+ "▁storytelling",
+ -12.55976676940918
+ ],
+ [
+ "▁severity",
+ -12.559771537780762
+ ],
+ [
+ "4\"",
+ -12.559956550598145
+ ],
+ [
+ "▁parasit",
+ -12.559967994689941
+ ],
+ [
+ "angebot",
+ -12.56002426147461
+ ],
+ [
+ "Data",
+ -12.56005573272705
+ ],
+ [
+ "listen",
+ -12.560086250305176
+ ],
+ [
+ "▁vârstă",
+ -12.560094833374023
+ ],
+ [
+ "▁swallow",
+ -12.56025505065918
+ ],
+ [
+ "TRE",
+ -12.560321807861328
+ ],
+ [
+ "▁daunting",
+ -12.56035041809082
+ ],
+ [
+ "▁Oli",
+ -12.560481071472168
+ ],
+ [
+ "▁definitive",
+ -12.56066608428955
+ ],
+ [
+ "▁rezerva",
+ -12.560667037963867
+ ],
+ [
+ "/15",
+ -12.560807228088379
+ ],
+ [
+ "▁Landschaft",
+ -12.560887336730957
+ ],
+ [
+ "▁Automotive",
+ -12.560934066772461
+ ],
+ [
+ "▁convers",
+ -12.56113052368164
+ ],
+ [
+ "▁thru",
+ -12.561139106750488
+ ],
+ [
+ "▁Township",
+ -12.561140060424805
+ ],
+ [
+ "▁tilt",
+ -12.56119441986084
+ ],
+ [
+ "▁Criminal",
+ -12.561227798461914
+ ],
+ [
+ "riez",
+ -12.561407089233398
+ ],
+ [
+ "▁Parking",
+ -12.561440467834473
+ ],
+ [
+ "▁humanitarian",
+ -12.561518669128418
+ ],
+ [
+ "▁Kilometer",
+ -12.561529159545898
+ ],
+ [
+ "controlled",
+ -12.56189250946045
+ ],
+ [
+ "▁Klick",
+ -12.561910629272461
+ ],
+ [
+ "support",
+ -12.56199836730957
+ ],
+ [
+ "handed",
+ -12.562005996704102
+ ],
+ [
+ "ämtliche",
+ -12.562104225158691
+ ],
+ [
+ "access",
+ -12.562232971191406
+ ],
+ [
+ "▁eleven",
+ -12.562232971191406
+ ],
+ [
+ "▁ferry",
+ -12.56229305267334
+ ],
+ [
+ "zieren",
+ -12.562620162963867
+ ],
+ [
+ "▁Gebrauch",
+ -12.562688827514648
+ ],
+ [
+ "▁vigoare",
+ -12.562689781188965
+ ],
+ [
+ "MON",
+ -12.562756538391113
+ ],
+ [
+ "fox",
+ -12.562886238098145
+ ],
+ [
+ "bestimmten",
+ -12.562894821166992
+ ],
+ [
+ "▁Gur",
+ -12.563069343566895
+ ],
+ [
+ "▁Mannschaft",
+ -12.563146591186523
+ ],
+ [
+ "▁patrol",
+ -12.563173294067383
+ ],
+ [
+ "▁casă",
+ -12.563376426696777
+ ],
+ [
+ "▁Stories",
+ -12.563380241394043
+ ],
+ [
+ "▁robotic",
+ -12.563425064086914
+ ],
+ [
+ "tiri",
+ -12.563576698303223
+ ],
+ [
+ "gewiesen",
+ -12.5636568069458
+ ],
+ [
+ "CV",
+ -12.563722610473633
+ ],
+ [
+ "▁parinti",
+ -12.563899040222168
+ ],
+ [
+ "▁Owen",
+ -12.563931465148926
+ ],
+ [
+ "▁Katie",
+ -12.564116477966309
+ ],
+ [
+ "▁Combine",
+ -12.56422233581543
+ ],
+ [
+ "enfalls",
+ -12.56442928314209
+ ],
+ [
+ "▁financière",
+ -12.564447402954102
+ ],
+ [
+ "▁parliament",
+ -12.564549446105957
+ ],
+ [
+ "▁Weekend",
+ -12.564616203308105
+ ],
+ [
+ "▁Sonic",
+ -12.564757347106934
+ ],
+ [
+ "▁fixture",
+ -12.56479263305664
+ ],
+ [
+ "majorité",
+ -12.56497573852539
+ ],
+ [
+ "▁gravel",
+ -12.565028190612793
+ ],
+ [
+ "realizate",
+ -12.565109252929688
+ ],
+ [
+ "examining",
+ -12.565113067626953
+ ],
+ [
+ "▁grim",
+ -12.5653657913208
+ ],
+ [
+ "▁stabili",
+ -12.565458297729492
+ ],
+ [
+ "▁Wochenende",
+ -12.56551456451416
+ ],
+ [
+ "▁Hebrew",
+ -12.565597534179688
+ ],
+ [
+ "▁Harrison",
+ -12.565799713134766
+ ],
+ [
+ "▁boundary",
+ -12.565858840942383
+ ],
+ [
+ "40,000",
+ -12.565902709960938
+ ],
+ [
+ "▁Ambassador",
+ -12.566208839416504
+ ],
+ [
+ "▁scoate",
+ -12.566229820251465
+ ],
+ [
+ "ffin",
+ -12.56623363494873
+ ],
+ [
+ "▁crème",
+ -12.566269874572754
+ ],
+ [
+ "▁obiecte",
+ -12.566378593444824
+ ],
+ [
+ "enţa",
+ -12.566763877868652
+ ],
+ [
+ "▁subsidiary",
+ -12.566797256469727
+ ],
+ [
+ "▁Franco",
+ -12.56688404083252
+ ],
+ [
+ "▁visuel",
+ -12.567042350769043
+ ],
+ [
+ "▁uitat",
+ -12.56708812713623
+ ],
+ [
+ "▁revisit",
+ -12.567122459411621
+ ],
+ [
+ "▁Camping",
+ -12.567150115966797
+ ],
+ [
+ "▁Divine",
+ -12.567304611206055
+ ],
+ [
+ "4-6",
+ -12.567323684692383
+ ],
+ [
+ "▁Brandon",
+ -12.567378997802734
+ ],
+ [
+ "ма",
+ -12.567450523376465
+ ],
+ [
+ "sofern",
+ -12.56745433807373
+ ],
+ [
+ "ntweder",
+ -12.56748104095459
+ ],
+ [
+ "▁Shoot",
+ -12.567618370056152
+ ],
+ [
+ "étais",
+ -12.56771183013916
+ ],
+ [
+ "SPEC",
+ -12.567930221557617
+ ],
+ [
+ "▁dreapta",
+ -12.567973136901855
+ ],
+ [
+ "▁repaired",
+ -12.568055152893066
+ ],
+ [
+ "pyr",
+ -12.568136215209961
+ ],
+ [
+ "▁warranties",
+ -12.568175315856934
+ ],
+ [
+ "▁représent",
+ -12.568263053894043
+ ],
+ [
+ "ADE",
+ -12.568293571472168
+ ],
+ [
+ "▁selective",
+ -12.56836223602295
+ ],
+ [
+ "▁Banking",
+ -12.568441390991211
+ ],
+ [
+ "▁ergonomic",
+ -12.568562507629395
+ ],
+ [
+ "...”",
+ -12.568602561950684
+ ],
+ [
+ "▁willingness",
+ -12.56867790222168
+ ],
+ [
+ "isser",
+ -12.568784713745117
+ ],
+ [
+ "▁confection",
+ -12.568961143493652
+ ],
+ [
+ "admi",
+ -12.569009780883789
+ ],
+ [
+ "▁Freizeit",
+ -12.569023132324219
+ ],
+ [
+ "▁illuminate",
+ -12.569151878356934
+ ],
+ [
+ "▁Repeat",
+ -12.569170951843262
+ ],
+ [
+ "▁Zeitpunkt",
+ -12.56933879852295
+ ],
+ [
+ "claimed",
+ -12.569439888000488
+ ],
+ [
+ "▁erhältlich",
+ -12.569480895996094
+ ],
+ [
+ "▁paysage",
+ -12.569537162780762
+ ],
+ [
+ "▁Atom",
+ -12.569890022277832
+ ],
+ [
+ "▁Graf",
+ -12.570086479187012
+ ],
+ [
+ "▁firmware",
+ -12.570093154907227
+ ],
+ [
+ "▁Swift",
+ -12.570180892944336
+ ],
+ [
+ "▁cercetare",
+ -12.57018756866455
+ ],
+ [
+ "▁internațional",
+ -12.570330619812012
+ ],
+ [
+ "▁zombie",
+ -12.570330619812012
+ ],
+ [
+ "▁Spread",
+ -12.57050609588623
+ ],
+ [
+ "ECO",
+ -12.57056999206543
+ ],
+ [
+ "▁Gestaltung",
+ -12.570758819580078
+ ],
+ [
+ "rast",
+ -12.570858001708984
+ ],
+ [
+ "▁perfume",
+ -12.5709228515625
+ ],
+ [
+ "▁roulette",
+ -12.570924758911133
+ ],
+ [
+ "▁distill",
+ -12.57096004486084
+ ],
+ [
+ "▁Produkten",
+ -12.570992469787598
+ ],
+ [
+ "225",
+ -12.571310043334961
+ ],
+ [
+ "facing",
+ -12.571371078491211
+ ],
+ [
+ "▁paradigm",
+ -12.571514129638672
+ ],
+ [
+ "▁Rah",
+ -12.571532249450684
+ ],
+ [
+ "▁Renault",
+ -12.571846961975098
+ ],
+ [
+ "willig",
+ -12.571864128112793
+ ],
+ [
+ "▁Vet",
+ -12.571890830993652
+ ],
+ [
+ "▁reprezenta",
+ -12.572126388549805
+ ],
+ [
+ "stoß",
+ -12.572185516357422
+ ],
+ [
+ "▁Weiß",
+ -12.5722074508667
+ ],
+ [
+ "▁Solo",
+ -12.572210311889648
+ ],
+ [
+ "▁Jin",
+ -12.572646141052246
+ ],
+ [
+ "▁Brussels",
+ -12.572693824768066
+ ],
+ [
+ "▁Tournament",
+ -12.572693824768066
+ ],
+ [
+ "▁proced",
+ -12.572710037231445
+ ],
+ [
+ "▁Rabbi",
+ -12.572835922241211
+ ],
+ [
+ "▁gameplay",
+ -12.572851181030273
+ ],
+ [
+ "▁ATM",
+ -12.572901725769043
+ ],
+ [
+ "▁firearm",
+ -12.572906494140625
+ ],
+ [
+ "revealing",
+ -12.573003768920898
+ ],
+ [
+ "schütz",
+ -12.57310676574707
+ ],
+ [
+ "▁Absolutely",
+ -12.573288917541504
+ ],
+ [
+ "▁interference",
+ -12.573433876037598
+ ],
+ [
+ "▁Employment",
+ -12.573558807373047
+ ],
+ [
+ "▁chord",
+ -12.57356071472168
+ ],
+ [
+ "▁oportun",
+ -12.573585510253906
+ ],
+ [
+ "▁frontier",
+ -12.573770523071289
+ ],
+ [
+ "▁Lunch",
+ -12.573891639709473
+ ],
+ [
+ "bread",
+ -12.57397174835205
+ ],
+ [
+ "▁rendered",
+ -12.573976516723633
+ ],
+ [
+ "5.1",
+ -12.573984146118164
+ ],
+ [
+ "▁motif",
+ -12.574066162109375
+ ],
+ [
+ "▁Schlag",
+ -12.574227333068848
+ ],
+ [
+ "113",
+ -12.574264526367188
+ ],
+ [
+ "▁Deux",
+ -12.574288368225098
+ ],
+ [
+ "▁surplus",
+ -12.574309349060059
+ ],
+ [
+ "ALS",
+ -12.574417114257812
+ ],
+ [
+ "▁abortion",
+ -12.574472427368164
+ ],
+ [
+ "▁airplane",
+ -12.574475288391113
+ ],
+ [
+ "▁migrants",
+ -12.574501991271973
+ ],
+ [
+ "kli",
+ -12.574539184570312
+ ],
+ [
+ "▁crochet",
+ -12.57454776763916
+ ],
+ [
+ "fahrer",
+ -12.574671745300293
+ ],
+ [
+ "▁reconstruction",
+ -12.57471752166748
+ ],
+ [
+ "▁difer",
+ -12.574752807617188
+ ],
+ [
+ "▁Conserv",
+ -12.57478141784668
+ ],
+ [
+ "▁NSW",
+ -12.57479476928711
+ ],
+ [
+ "▁regim",
+ -12.574844360351562
+ ],
+ [
+ "▁Except",
+ -12.574904441833496
+ ],
+ [
+ "▁trage",
+ -12.574978828430176
+ ],
+ [
+ "▁Consiliul",
+ -12.575058937072754
+ ],
+ [
+ "▁Bedarf",
+ -12.575064659118652
+ ],
+ [
+ "▁additive",
+ -12.5750732421875
+ ],
+ [
+ "know",
+ -12.5751371383667
+ ],
+ [
+ "▁sauna",
+ -12.57517147064209
+ ],
+ [
+ "▁mortality",
+ -12.575201034545898
+ ],
+ [
+ "kräftig",
+ -12.575358390808105
+ ],
+ [
+ "▁Own",
+ -12.575445175170898
+ ],
+ [
+ "nzo",
+ -12.575519561767578
+ ],
+ [
+ "▁villes",
+ -12.575543403625488
+ ],
+ [
+ "▁recette",
+ -12.575749397277832
+ ],
+ [
+ "▁attacking",
+ -12.575799942016602
+ ],
+ [
+ "beruf",
+ -12.57608699798584
+ ],
+ [
+ "▁integrat",
+ -12.57612419128418
+ ],
+ [
+ "realizarea",
+ -12.576201438903809
+ ],
+ [
+ "▁exemption",
+ -12.57628345489502
+ ],
+ [
+ "GW",
+ -12.576285362243652
+ ],
+ [
+ "▁Nano",
+ -12.576395034790039
+ ],
+ [
+ "SCH",
+ -12.576440811157227
+ ],
+ [
+ "▁honesty",
+ -12.576457023620605
+ ],
+ [
+ "▁Arriv",
+ -12.576515197753906
+ ],
+ [
+ "▁gland",
+ -12.576542854309082
+ ],
+ [
+ "▁proactive",
+ -12.576746940612793
+ ],
+ [
+ "▁agile",
+ -12.576837539672852
+ ],
+ [
+ "▁kernel",
+ -12.576844215393066
+ ],
+ [
+ "▁nurture",
+ -12.576860427856445
+ ],
+ [
+ "▁Patent",
+ -12.576963424682617
+ ],
+ [
+ "▁excursi",
+ -12.577189445495605
+ ],
+ [
+ "pulsion",
+ -12.577326774597168
+ ],
+ [
+ "stellte",
+ -12.577351570129395
+ ],
+ [
+ "ständige",
+ -12.577421188354492
+ ],
+ [
+ "▁Rebecca",
+ -12.577436447143555
+ ],
+ [
+ "▁Securities",
+ -12.577436447143555
+ ],
+ [
+ "mètre",
+ -12.577446937561035
+ ],
+ [
+ "LOW",
+ -12.577469825744629
+ ],
+ [
+ "▁consilier",
+ -12.577537536621094
+ ],
+ [
+ "▁Architekt",
+ -12.577733993530273
+ ],
+ [
+ "▁china",
+ -12.57777214050293
+ ],
+ [
+ "älfte",
+ -12.577778816223145
+ ],
+ [
+ "▁Combin",
+ -12.577795028686523
+ ],
+ [
+ "480",
+ -12.577999114990234
+ ],
+ [
+ "liv",
+ -12.578021049499512
+ ],
+ [
+ "▁peur",
+ -12.578067779541016
+ ],
+ [
+ "keep",
+ -12.57822322845459
+ ],
+ [
+ "▁Verhalten",
+ -12.578324317932129
+ ],
+ [
+ "▁peek",
+ -12.578446388244629
+ ],
+ [
+ "▁dient",
+ -12.578550338745117
+ ],
+ [
+ "▁prevazut",
+ -12.578625679016113
+ ],
+ [
+ "Emmanuel",
+ -12.57862663269043
+ ],
+ [
+ "▁incidence",
+ -12.57862663269043
+ ],
+ [
+ "▁Framework",
+ -12.578715324401855
+ ],
+ [
+ "dass",
+ -12.578816413879395
+ ],
+ [
+ "artiste",
+ -12.578874588012695
+ ],
+ [
+ "▁Accept",
+ -12.578971862792969
+ ],
+ [
+ "▁plunge",
+ -12.579073905944824
+ ],
+ [
+ "chauff",
+ -12.579118728637695
+ ],
+ [
+ "▁guilt",
+ -12.579156875610352
+ ],
+ [
+ "▁senator",
+ -12.57945442199707
+ ],
+ [
+ "▁disable",
+ -12.579776763916016
+ ],
+ [
+ "▁partout",
+ -12.579901695251465
+ ],
+ [
+ "JC",
+ -12.580045700073242
+ ],
+ [
+ "▁Highly",
+ -12.580150604248047
+ ],
+ [
+ "▁beneficii",
+ -12.58021068572998
+ ],
+ [
+ "fibro",
+ -12.580347061157227
+ ],
+ [
+ "interpreted",
+ -12.580550193786621
+ ],
+ [
+ "▁genauso",
+ -12.58056354522705
+ ],
+ [
+ "▁basil",
+ -12.580601692199707
+ ],
+ [
+ "▁Angst",
+ -12.580697059631348
+ ],
+ [
+ "rzte",
+ -12.580933570861816
+ ],
+ [
+ "Master",
+ -12.58112907409668
+ ],
+ [
+ "▁french",
+ -12.581324577331543
+ ],
+ [
+ "▁Duration",
+ -12.581343650817871
+ ],
+ [
+ "HM",
+ -12.581402778625488
+ ],
+ [
+ "▁Bert",
+ -12.581518173217773
+ ],
+ [
+ "▁1963",
+ -12.581534385681152
+ ],
+ [
+ "▁warrior",
+ -12.581604957580566
+ ],
+ [
+ "2007",
+ -12.581696510314941
+ ],
+ [
+ "▁recycle",
+ -12.581722259521484
+ ],
+ [
+ "▁fertiliz",
+ -12.581808090209961
+ ],
+ [
+ "▁hatch",
+ -12.581809997558594
+ ],
+ [
+ "ISH",
+ -12.581811904907227
+ ],
+ [
+ "luft",
+ -12.582321166992188
+ ],
+ [
+ "▁crying",
+ -12.582452774047852
+ ],
+ [
+ "▁activist",
+ -12.5824613571167
+ ],
+ [
+ "schränkt",
+ -12.582500457763672
+ ],
+ [
+ "▁diff",
+ -12.582500457763672
+ ],
+ [
+ "▁Demand",
+ -12.58262825012207
+ ],
+ [
+ "▁transported",
+ -12.582669258117676
+ ],
+ [
+ "▁Remodel",
+ -12.582686424255371
+ ],
+ [
+ "▁Etats",
+ -12.582704544067383
+ ],
+ [
+ "ANI",
+ -12.582777976989746
+ ],
+ [
+ "▁spéciale",
+ -12.582804679870605
+ ],
+ [
+ "▁Konzert",
+ -12.582805633544922
+ ],
+ [
+ "▁Bedürfnisse",
+ -12.58281135559082
+ ],
+ [
+ "▁overlooked",
+ -12.582864761352539
+ ],
+ [
+ "▁cutter",
+ -12.582974433898926
+ ],
+ [
+ "klär",
+ -12.58311939239502
+ ],
+ [
+ "▁Materialien",
+ -12.583135604858398
+ ],
+ [
+ "▁gewisse",
+ -12.583388328552246
+ ],
+ [
+ "bull",
+ -12.583499908447266
+ ],
+ [
+ "Good",
+ -12.583513259887695
+ ],
+ [
+ "Gig",
+ -12.583616256713867
+ ],
+ [
+ "Logic",
+ -12.583736419677734
+ ],
+ [
+ "▁Schlaf",
+ -12.583970069885254
+ ],
+ [
+ "▁Yankee",
+ -12.583996772766113
+ ],
+ [
+ "▁Batman",
+ -12.584020614624023
+ ],
+ [
+ "▁funcție",
+ -12.584166526794434
+ ],
+ [
+ "▁partenariat",
+ -12.584294319152832
+ ],
+ [
+ "▁Antrag",
+ -12.584348678588867
+ ],
+ [
+ "▁Pill",
+ -12.584519386291504
+ ],
+ [
+ "▁tram",
+ -12.584637641906738
+ ],
+ [
+ "▁Minor",
+ -12.58465576171875
+ ],
+ [
+ "pertaining",
+ -12.584678649902344
+ ],
+ [
+ "▁apropiere",
+ -12.584843635559082
+ ],
+ [
+ "▁Barack",
+ -12.584965705871582
+ ],
+ [
+ "schön",
+ -12.585174560546875
+ ],
+ [
+ "▁Sandy",
+ -12.585182189941406
+ ],
+ [
+ "kilometre",
+ -12.585192680358887
+ ],
+ [
+ "▁diy",
+ -12.585234642028809
+ ],
+ [
+ "▁1966",
+ -12.585453987121582
+ ],
+ [
+ "gelassen",
+ -12.585485458374023
+ ],
+ [
+ "▁Trial",
+ -12.585592269897461
+ ],
+ [
+ "▁Bauer",
+ -12.585603713989258
+ ],
+ [
+ "▁assumption",
+ -12.585648536682129
+ ],
+ [
+ "birth",
+ -12.585668563842773
+ ],
+ [
+ "rechnen",
+ -12.585861206054688
+ ],
+ [
+ "▁meci",
+ -12.585867881774902
+ ],
+ [
+ "▁gloss",
+ -12.585906982421875
+ ],
+ [
+ "▁sewer",
+ -12.58593463897705
+ ],
+ [
+ "▁Stimme",
+ -12.585955619812012
+ ],
+ [
+ "▁Fortune",
+ -12.585967063903809
+ ],
+ [
+ "▁Lösungen",
+ -12.586007118225098
+ ],
+ [
+ "▁impresi",
+ -12.586074829101562
+ ],
+ [
+ "schlaf",
+ -12.586089134216309
+ ],
+ [
+ "prüfung",
+ -12.586097717285156
+ ],
+ [
+ "▁instalat",
+ -12.586198806762695
+ ],
+ [
+ "▁picturesque",
+ -12.586233139038086
+ ],
+ [
+ "vait",
+ -12.586240768432617
+ ],
+ [
+ "8.1",
+ -12.58629035949707
+ ],
+ [
+ "▁călători",
+ -12.586392402648926
+ ],
+ [
+ "▁dix",
+ -12.586400032043457
+ ],
+ [
+ "▁furnished",
+ -12.586411476135254
+ ],
+ [
+ "▁dolari",
+ -12.586445808410645
+ ],
+ [
+ "▁regener",
+ -12.586562156677246
+ ],
+ [
+ "▁astazi",
+ -12.586621284484863
+ ],
+ [
+ "▁Sprach",
+ -12.586750030517578
+ ],
+ [
+ "delà",
+ -12.586846351623535
+ ],
+ [
+ "avec",
+ -12.58694076538086
+ ],
+ [
+ "▁Buddhist",
+ -12.586990356445312
+ ],
+ [
+ "▁alphabet",
+ -12.586990356445312
+ ],
+ [
+ "▁berichtet",
+ -12.587201118469238
+ ],
+ [
+ "ideally",
+ -12.587209701538086
+ ],
+ [
+ "▁annuel",
+ -12.587421417236328
+ ],
+ [
+ "▁laughing",
+ -12.587532997131348
+ ],
+ [
+ "▁Zustand",
+ -12.587639808654785
+ ],
+ [
+ "cini",
+ -12.587692260742188
+ ],
+ [
+ "solid",
+ -12.587724685668945
+ ],
+ [
+ "▁Broker",
+ -12.587868690490723
+ ],
+ [
+ "▁developmental",
+ -12.5879545211792
+ ],
+ [
+ "▁Summary",
+ -12.588191032409668
+ ],
+ [
+ "▁Trinity",
+ -12.58819580078125
+ ],
+ [
+ "▁sucre",
+ -12.58821964263916
+ ],
+ [
+ "▁sandal",
+ -12.588231086730957
+ ],
+ [
+ "PEN",
+ -12.588274955749512
+ ],
+ [
+ "gewinn",
+ -12.588486671447754
+ ],
+ [
+ "olé",
+ -12.588555335998535
+ ],
+ [
+ "matric",
+ -12.58865737915039
+ ],
+ [
+ "xton",
+ -12.588695526123047
+ ],
+ [
+ "werten",
+ -12.588740348815918
+ ],
+ [
+ "▁Dust",
+ -12.588765144348145
+ ],
+ [
+ "▁Journey",
+ -12.588791847229004
+ ],
+ [
+ "▁Rush",
+ -12.588793754577637
+ ],
+ [
+ "▁NCAA",
+ -12.588839530944824
+ ],
+ [
+ "▁allgemeine",
+ -12.588926315307617
+ ],
+ [
+ "▁Universe",
+ -12.589007377624512
+ ],
+ [
+ "▁connais",
+ -12.589099884033203
+ ],
+ [
+ "▁quantité",
+ -12.58912467956543
+ ],
+ [
+ "▁Kab",
+ -12.589150428771973
+ ],
+ [
+ "▁purse",
+ -12.589150428771973
+ ],
+ [
+ "Health",
+ -12.589210510253906
+ ],
+ [
+ "▁apărut",
+ -12.589288711547852
+ ],
+ [
+ "▁bypass",
+ -12.589313507080078
+ ],
+ [
+ "pronounced",
+ -12.58936595916748
+ ],
+ [
+ "▁magnitude",
+ -12.589393615722656
+ ],
+ [
+ "▁Walmart",
+ -12.589394569396973
+ ],
+ [
+ "ède",
+ -12.589409828186035
+ ],
+ [
+ "▁serum",
+ -12.589590072631836
+ ],
+ [
+ "▁baseline",
+ -12.589765548706055
+ ],
+ [
+ "STER",
+ -12.589932441711426
+ ],
+ [
+ "▁ONLY",
+ -12.590052604675293
+ ],
+ [
+ "▁individuell",
+ -12.590086936950684
+ ],
+ [
+ "▁Ghi",
+ -12.590139389038086
+ ],
+ [
+ "▁Ruby",
+ -12.59020709991455
+ ],
+ [
+ "▁Chal",
+ -12.590241432189941
+ ],
+ [
+ "▁Vier",
+ -12.590261459350586
+ ],
+ [
+ "5.0",
+ -12.5903902053833
+ ],
+ [
+ "▁fog",
+ -12.590519905090332
+ ],
+ [
+ "esel",
+ -12.590557098388672
+ ],
+ [
+ "▁Python",
+ -12.590598106384277
+ ],
+ [
+ "▁urmează",
+ -12.590608596801758
+ ],
+ [
+ "▁trustworthy",
+ -12.590639114379883
+ ],
+ [
+ "hört",
+ -12.590729713439941
+ ],
+ [
+ "▁tâche",
+ -12.59078311920166
+ ],
+ [
+ "Patri",
+ -12.590799331665039
+ ],
+ [
+ "▁grind",
+ -12.590928077697754
+ ],
+ [
+ "▁Raven",
+ -12.590934753417969
+ ],
+ [
+ "▁poursuiv",
+ -12.590951919555664
+ ],
+ [
+ "▁simpli",
+ -12.591140747070312
+ ],
+ [
+ "▁echo",
+ -12.591165542602539
+ ],
+ [
+ "▁Attention",
+ -12.591313362121582
+ ],
+ [
+ "Against",
+ -12.591402053833008
+ ],
+ [
+ "GET",
+ -12.59148120880127
+ ],
+ [
+ "▁turistic",
+ -12.591535568237305
+ ],
+ [
+ "▁tenure",
+ -12.59158992767334
+ ],
+ [
+ "▁alimentaire",
+ -12.591651916503906
+ ],
+ [
+ "Who",
+ -12.59172248840332
+ ],
+ [
+ "▁ändern",
+ -12.591729164123535
+ ],
+ [
+ "▁rebound",
+ -12.591778755187988
+ ],
+ [
+ "grenze",
+ -12.591849327087402
+ ],
+ [
+ "▁Fame",
+ -12.592093467712402
+ ],
+ [
+ "▁Kick",
+ -12.592215538024902
+ ],
+ [
+ "▁Detail",
+ -12.59228801727295
+ ],
+ [
+ "▁Push",
+ -12.592308044433594
+ ],
+ [
+ "production",
+ -12.592430114746094
+ ],
+ [
+ "▁Candidates",
+ -12.59244441986084
+ ],
+ [
+ "▁reușit",
+ -12.592484474182129
+ ],
+ [
+ "istischen",
+ -12.592525482177734
+ ],
+ [
+ "lassung",
+ -12.592649459838867
+ ],
+ [
+ "▁Hann",
+ -12.592713356018066
+ ],
+ [
+ "espère",
+ -12.592965126037598
+ ],
+ [
+ "▁vergessen",
+ -12.593008041381836
+ ],
+ [
+ "▁smiling",
+ -12.593010902404785
+ ],
+ [
+ "▁devotion",
+ -12.593016624450684
+ ],
+ [
+ "▁pastry",
+ -12.593071937561035
+ ],
+ [
+ "Add",
+ -12.593390464782715
+ ],
+ [
+ "▁authorization",
+ -12.593494415283203
+ ],
+ [
+ "▁Suisse",
+ -12.593568801879883
+ ],
+ [
+ "▁Berkeley",
+ -12.593611717224121
+ ],
+ [
+ "▁Guild",
+ -12.593660354614258
+ ],
+ [
+ "▁choir",
+ -12.593748092651367
+ ],
+ [
+ "learning",
+ -12.593802452087402
+ ],
+ [
+ "▁Tanz",
+ -12.593894004821777
+ ],
+ [
+ "mardi",
+ -12.594076156616211
+ ],
+ [
+ "▁rezultatele",
+ -12.594191551208496
+ ],
+ [
+ "▁earrings",
+ -12.594218254089355
+ ],
+ [
+ "▁turbine",
+ -12.594223976135254
+ ],
+ [
+ "▁jeudi",
+ -12.594284057617188
+ ],
+ [
+ "terapie",
+ -12.594576835632324
+ ],
+ [
+ "regain",
+ -12.59461498260498
+ ],
+ [
+ "SET",
+ -12.594643592834473
+ ],
+ [
+ "▁Hände",
+ -12.594681739807129
+ ],
+ [
+ "▁Globe",
+ -12.594683647155762
+ ],
+ [
+ "frag",
+ -12.594775199890137
+ ],
+ [
+ "▁Treasury",
+ -12.594820976257324
+ ],
+ [
+ "▁hazardous",
+ -12.594820976257324
+ ],
+ [
+ "▁Fahrt",
+ -12.594928741455078
+ ],
+ [
+ "▁fulfilled",
+ -12.594966888427734
+ ],
+ [
+ "▁manga",
+ -12.594987869262695
+ ],
+ [
+ "▁composé",
+ -12.595067977905273
+ ],
+ [
+ "▁ABS",
+ -12.595132827758789
+ ],
+ [
+ "▁preced",
+ -12.595197677612305
+ ],
+ [
+ "▁beauté",
+ -12.595233917236328
+ ],
+ [
+ "▁interessant",
+ -12.59526252746582
+ ],
+ [
+ "▁lieber",
+ -12.595324516296387
+ ],
+ [
+ "▁Kö",
+ -12.595378875732422
+ ],
+ [
+ "EMS",
+ -12.595410346984863
+ ],
+ [
+ "FER",
+ -12.595413208007812
+ ],
+ [
+ "▁eure",
+ -12.595427513122559
+ ],
+ [
+ "▁plumber",
+ -12.595427513122559
+ ],
+ [
+ "Love",
+ -12.595463752746582
+ ],
+ [
+ "▁Marcus",
+ -12.595635414123535
+ ],
+ [
+ "▁registry",
+ -12.595637321472168
+ ],
+ [
+ "▁uncle",
+ -12.595696449279785
+ ],
+ [
+ "▁neuf",
+ -12.595728874206543
+ ],
+ [
+ "▁Fläche",
+ -12.59575080871582
+ ],
+ [
+ "▁restaur",
+ -12.595815658569336
+ ],
+ [
+ "▁noticeable",
+ -12.595833778381348
+ ],
+ [
+ "▁riches",
+ -12.595871925354004
+ ],
+ [
+ "occupy",
+ -12.596031188964844
+ ],
+ [
+ "▁hurricane",
+ -12.596031188964844
+ ],
+ [
+ "▁gespeichert",
+ -12.596033096313477
+ ],
+ [
+ "▁Bordeaux",
+ -12.596039772033691
+ ],
+ [
+ "▁Maj",
+ -12.59637451171875
+ ],
+ [
+ "Applied",
+ -12.596439361572266
+ ],
+ [
+ "▁compter",
+ -12.596575736999512
+ ],
+ [
+ "impact",
+ -12.59663200378418
+ ],
+ [
+ "▁Improve",
+ -12.596758842468262
+ ],
+ [
+ "▁Calif",
+ -12.596832275390625
+ ],
+ [
+ "▁desfășur",
+ -12.596939086914062
+ ],
+ [
+ "▁packaged",
+ -12.597001075744629
+ ],
+ [
+ "180",
+ -12.59703540802002
+ ],
+ [
+ "devenu",
+ -12.597042083740234
+ ],
+ [
+ "▁Battery",
+ -12.597243309020996
+ ],
+ [
+ "▁objection",
+ -12.597254753112793
+ ],
+ [
+ "▁anual",
+ -12.597305297851562
+ ],
+ [
+ "▁Landscape",
+ -12.59731674194336
+ ],
+ [
+ "IQ",
+ -12.597403526306152
+ ],
+ [
+ "grès",
+ -12.597586631774902
+ ],
+ [
+ "▁witnesses",
+ -12.597750663757324
+ ],
+ [
+ "enţial",
+ -12.597764015197754
+ ],
+ [
+ "▁plateau",
+ -12.597779273986816
+ ],
+ [
+ "▁bilete",
+ -12.59783935546875
+ ],
+ [
+ "▁Bronze",
+ -12.59786605834961
+ ],
+ [
+ "▁Kiss",
+ -12.597946166992188
+ ],
+ [
+ "▁Serge",
+ -12.598093032836914
+ ],
+ [
+ "atomic",
+ -12.598145484924316
+ ],
+ [
+ "▁renovated",
+ -12.59817886352539
+ ],
+ [
+ "player",
+ -12.598212242126465
+ ],
+ [
+ "▁dirig",
+ -12.598291397094727
+ ],
+ [
+ "▁Îm",
+ -12.598296165466309
+ ],
+ [
+ "▁plimb",
+ -12.59843635559082
+ ],
+ [
+ "▁ambassador",
+ -12.598455429077148
+ ],
+ [
+ "▁apropiat",
+ -12.598455429077148
+ ],
+ [
+ "▁adaug",
+ -12.598602294921875
+ ],
+ [
+ "ogenic",
+ -12.59872055053711
+ ],
+ [
+ "kämpfe",
+ -12.598779678344727
+ ],
+ [
+ "▁Hillary",
+ -12.598907470703125
+ ],
+ [
+ "yak",
+ -12.598942756652832
+ ],
+ [
+ "General",
+ -12.59925365447998
+ ],
+ [
+ "▁Zugang",
+ -12.599400520324707
+ ],
+ [
+ "▁fertil",
+ -12.599457740783691
+ ],
+ [
+ "incat",
+ -12.599536895751953
+ ],
+ [
+ "assessing",
+ -12.599587440490723
+ ],
+ [
+ "▁Cincinnati",
+ -12.59967041015625
+ ],
+ [
+ "▁convincing",
+ -12.599685668945312
+ ],
+ [
+ "sadly",
+ -12.59974479675293
+ ],
+ [
+ "kunde",
+ -12.599801063537598
+ ],
+ [
+ "ambul",
+ -12.599913597106934
+ ],
+ [
+ "▁familii",
+ -12.599974632263184
+ ],
+ [
+ "juri",
+ -12.60007095336914
+ ],
+ [
+ "ionen",
+ -12.600102424621582
+ ],
+ [
+ "▁Wirtschaft",
+ -12.600130081176758
+ ],
+ [
+ "contract",
+ -12.600135803222656
+ ],
+ [
+ "punem",
+ -12.600151062011719
+ ],
+ [
+ "handlung",
+ -12.600394248962402
+ ],
+ [
+ "▁fournir",
+ -12.600455284118652
+ ],
+ [
+ "▁Ambi",
+ -12.600663185119629
+ ],
+ [
+ "▁Isaac",
+ -12.600663185119629
+ ],
+ [
+ "▁praying",
+ -12.6007719039917
+ ],
+ [
+ "▁Italien",
+ -12.600848197937012
+ ],
+ [
+ "233",
+ -12.600850105285645
+ ],
+ [
+ "spawn",
+ -12.600913047790527
+ ],
+ [
+ "▁legii",
+ -12.60092544555664
+ ],
+ [
+ "▁zuvor",
+ -12.601018905639648
+ ],
+ [
+ "▁comune",
+ -12.601030349731445
+ ],
+ [
+ "official",
+ -12.601165771484375
+ ],
+ [
+ "144",
+ -12.601290702819824
+ ],
+ [
+ "izeaza",
+ -12.601329803466797
+ ],
+ [
+ "▁Keller",
+ -12.601372718811035
+ ],
+ [
+ "ORE",
+ -12.601378440856934
+ ],
+ [
+ "122",
+ -12.601485252380371
+ ],
+ [
+ "incurred",
+ -12.60150146484375
+ ],
+ [
+ "CHA",
+ -12.601579666137695
+ ],
+ [
+ "▁Herzen",
+ -12.601590156555176
+ ],
+ [
+ "▁reasoning",
+ -12.6016263961792
+ ],
+ [
+ "affaire",
+ -12.601849555969238
+ ],
+ [
+ "ooth",
+ -12.601890563964844
+ ],
+ [
+ "155",
+ -12.601998329162598
+ ],
+ [
+ "▁invented",
+ -12.602113723754883
+ ],
+ [
+ "▁Comun",
+ -12.602140426635742
+ ],
+ [
+ "zähl",
+ -12.602179527282715
+ ],
+ [
+ "geliefert",
+ -12.602212905883789
+ ],
+ [
+ "explorer",
+ -12.602213859558105
+ ],
+ [
+ "nect",
+ -12.602326393127441
+ ],
+ [
+ "▁mercredi",
+ -12.602408409118652
+ ],
+ [
+ "▁volonté",
+ -12.602408409118652
+ ],
+ [
+ "easy",
+ -12.602453231811523
+ ],
+ [
+ "▁feat",
+ -12.602490425109863
+ ],
+ [
+ "rented",
+ -12.602580070495605
+ ],
+ [
+ "▁converter",
+ -12.602592468261719
+ ],
+ [
+ "Verhältnis",
+ -12.602713584899902
+ ],
+ [
+ "▁Iceland",
+ -12.602792739868164
+ ],
+ [
+ "▁pretul",
+ -12.602933883666992
+ ],
+ [
+ "▁Vorstellung",
+ -12.602960586547852
+ ],
+ [
+ "▁hydrogen",
+ -12.603096008300781
+ ],
+ [
+ "▁pouvai",
+ -12.603097915649414
+ ],
+ [
+ "▁dawn",
+ -12.603153228759766
+ ],
+ [
+ "▁Georg",
+ -12.603269577026367
+ ],
+ [
+ "▁cautious",
+ -12.603367805480957
+ ],
+ [
+ "▁Pattern",
+ -12.603464126586914
+ ],
+ [
+ "▁Ox",
+ -12.603602409362793
+ ],
+ [
+ "▁decizie",
+ -12.603676795959473
+ ],
+ [
+ "REC",
+ -12.603889465332031
+ ],
+ [
+ "▁Mortgage",
+ -12.60393238067627
+ ],
+ [
+ "attributed",
+ -12.603973388671875
+ ],
+ [
+ "floor",
+ -12.603992462158203
+ ],
+ [
+ "▁Wichtig",
+ -12.604207992553711
+ ],
+ [
+ "enseignant",
+ -12.604265213012695
+ ],
+ [
+ "▁civilization",
+ -12.604302406311035
+ ],
+ [
+ "▁dispozitie",
+ -12.60450553894043
+ ],
+ [
+ "▁geographic",
+ -12.604543685913086
+ ],
+ [
+ "▁Kun",
+ -12.604607582092285
+ ],
+ [
+ "LIN",
+ -12.604679107666016
+ ],
+ [
+ "▁auzit",
+ -12.604707717895508
+ ],
+ [
+ "except",
+ -12.604761123657227
+ ],
+ [
+ "▁superbe",
+ -12.604904174804688
+ ],
+ [
+ "▁installé",
+ -12.605000495910645
+ ],
+ [
+ "▁Peninsula",
+ -12.605154037475586
+ ],
+ [
+ "▁norme",
+ -12.605164527893066
+ ],
+ [
+ "elul",
+ -12.60517406463623
+ ],
+ [
+ "▁Experten",
+ -12.605256080627441
+ ],
+ [
+ "expression",
+ -12.605295181274414
+ ],
+ [
+ "Christ",
+ -12.605320930480957
+ ],
+ [
+ "▁Fuel",
+ -12.605369567871094
+ ],
+ [
+ "▁muffin",
+ -12.605485916137695
+ ],
+ [
+ "▁lecteur",
+ -12.605521202087402
+ ],
+ [
+ "▁gifted",
+ -12.605589866638184
+ ],
+ [
+ "▁Japon",
+ -12.605602264404297
+ ],
+ [
+ "▁SSD",
+ -12.605644226074219
+ ],
+ [
+ "▁Calgary",
+ -12.605765342712402
+ ],
+ [
+ "▁hooked",
+ -12.605876922607422
+ ],
+ [
+ "▁Joan",
+ -12.605896949768066
+ ],
+ [
+ "▁tangible",
+ -12.606083869934082
+ ],
+ [
+ "FW",
+ -12.606225967407227
+ ],
+ [
+ "olli",
+ -12.6062593460083
+ ],
+ [
+ "▁Platinum",
+ -12.606376647949219
+ ],
+ [
+ "▁miniature",
+ -12.606392860412598
+ ],
+ [
+ "▁lump",
+ -12.606608390808105
+ ],
+ [
+ "ologische",
+ -12.60689926147461
+ ],
+ [
+ "▁Istanbul",
+ -12.606987953186035
+ ],
+ [
+ "▁Compar",
+ -12.607060432434082
+ ],
+ [
+ "tropic",
+ -12.607256889343262
+ ],
+ [
+ "KING",
+ -12.607279777526855
+ ],
+ [
+ "Präsident",
+ -12.607297897338867
+ ],
+ [
+ "▁fotografii",
+ -12.607303619384766
+ ],
+ [
+ "hoped",
+ -12.607451438903809
+ ],
+ [
+ "▁pâte",
+ -12.607601165771484
+ ],
+ [
+ "▁mercy",
+ -12.60760498046875
+ ],
+ [
+ "▁quiz",
+ -12.607619285583496
+ ],
+ [
+ "demonstrating",
+ -12.607678413391113
+ ],
+ [
+ "▁douce",
+ -12.607832908630371
+ ],
+ [
+ "▁Vest",
+ -12.607841491699219
+ ],
+ [
+ "▁Harvey",
+ -12.6082181930542
+ ],
+ [
+ "▁breit",
+ -12.608227729797363
+ ],
+ [
+ "▁Bereits",
+ -12.608291625976562
+ ],
+ [
+ "▁breakthrough",
+ -12.608316421508789
+ ],
+ [
+ "▁masterpiece",
+ -12.608320236206055
+ ],
+ [
+ "▁Chester",
+ -12.60838794708252
+ ],
+ [
+ "▁indiqué",
+ -12.608451843261719
+ ],
+ [
+ "hook",
+ -12.60857105255127
+ ],
+ [
+ "statutory",
+ -12.608596801757812
+ ],
+ [
+ "▁Direkt",
+ -12.608617782592773
+ ],
+ [
+ "▁specs",
+ -12.608708381652832
+ ],
+ [
+ "Drive",
+ -12.608725547790527
+ ],
+ [
+ "▁survivors",
+ -12.608826637268066
+ ],
+ [
+ "▁jackpot",
+ -12.608840942382812
+ ],
+ [
+ "▁garder",
+ -12.608872413635254
+ ],
+ [
+ "▁Geburtstag",
+ -12.60887336730957
+ ],
+ [
+ "145",
+ -12.608963966369629
+ ],
+ [
+ "▁Clay",
+ -12.609028816223145
+ ],
+ [
+ "▁WHO",
+ -12.60906982421875
+ ],
+ [
+ "▁Ellen",
+ -12.609393119812012
+ ],
+ [
+ "▁bonheur",
+ -12.609440803527832
+ ],
+ [
+ "▁hazards",
+ -12.609440803527832
+ ],
+ [
+ "▁Kaiser",
+ -12.609488487243652
+ ],
+ [
+ "▁tightly",
+ -12.609506607055664
+ ],
+ [
+ "Universitatea",
+ -12.609529495239258
+ ],
+ [
+ "▁rinse",
+ -12.609533309936523
+ ],
+ [
+ "▁passant",
+ -12.609640121459961
+ ],
+ [
+ "▁sânge",
+ -12.609832763671875
+ ],
+ [
+ "▁peuple",
+ -12.60983657836914
+ ],
+ [
+ "jungen",
+ -12.609975814819336
+ ],
+ [
+ "▁inappropriate",
+ -12.610054969787598
+ ],
+ [
+ "▁mitigate",
+ -12.610066413879395
+ ],
+ [
+ "MID",
+ -12.610221862792969
+ ],
+ [
+ "▁telecom",
+ -12.610297203063965
+ ],
+ [
+ "▁plaj",
+ -12.610316276550293
+ ],
+ [
+ "▁presupune",
+ -12.610361099243164
+ ],
+ [
+ "acco",
+ -12.61038875579834
+ ],
+ [
+ "expressing",
+ -12.610654830932617
+ ],
+ [
+ "▁Symphony",
+ -12.61066722869873
+ ],
+ [
+ "temperatur",
+ -12.610710144042969
+ ],
+ [
+ "▁activităţi",
+ -12.610800743103027
+ ],
+ [
+ "▁amended",
+ -12.610847473144531
+ ],
+ [
+ "▁rehab",
+ -12.610909461975098
+ ],
+ [
+ "▁sportiv",
+ -12.611004829406738
+ ],
+ [
+ "hotel",
+ -12.611031532287598
+ ],
+ [
+ "branche",
+ -12.61103630065918
+ ],
+ [
+ "▁Noch",
+ -12.611079216003418
+ ],
+ [
+ "▁1961",
+ -12.611238479614258
+ ],
+ [
+ "release",
+ -12.611359596252441
+ ],
+ [
+ "blaze",
+ -12.611381530761719
+ ],
+ [
+ "Adv",
+ -12.61139965057373
+ ],
+ [
+ "Line",
+ -12.611671447753906
+ ],
+ [
+ "▁financiare",
+ -12.61184310913086
+ ],
+ [
+ "▁chauffage",
+ -12.611919403076172
+ ],
+ [
+ "мо",
+ -12.61192512512207
+ ],
+ [
+ "schuhe",
+ -12.612035751342773
+ ],
+ [
+ "blé",
+ -12.612040519714355
+ ],
+ [
+ "▁Echo",
+ -12.612468719482422
+ ],
+ [
+ "▁remarks",
+ -12.61253547668457
+ ],
+ [
+ "scriu",
+ -12.612629890441895
+ ],
+ [
+ "Vir",
+ -12.612701416015625
+ ],
+ [
+ "War",
+ -12.61271858215332
+ ],
+ [
+ "atifs",
+ -12.613006591796875
+ ],
+ [
+ "RING",
+ -12.613082885742188
+ ],
+ [
+ "▁Instruction",
+ -12.613150596618652
+ ],
+ [
+ "▁verlassen",
+ -12.613155364990234
+ ],
+ [
+ "▁ergänz",
+ -12.613234519958496
+ ],
+ [
+ "▁Emil",
+ -12.613248825073242
+ ],
+ [
+ "▁empire",
+ -12.613263130187988
+ ],
+ [
+ "▁Einkauf",
+ -12.613306999206543
+ ],
+ [
+ "utigen",
+ -12.613329887390137
+ ],
+ [
+ "▁audition",
+ -12.613390922546387
+ ],
+ [
+ "travelled",
+ -12.61347484588623
+ ],
+ [
+ "ло",
+ -12.613579750061035
+ ],
+ [
+ "▁infinite",
+ -12.613720893859863
+ ],
+ [
+ "▁Lieblings",
+ -12.613749504089355
+ ],
+ [
+ "▁vân",
+ -12.613754272460938
+ ],
+ [
+ "▁spinning",
+ -12.613778114318848
+ ],
+ [
+ "converting",
+ -12.614031791687012
+ ],
+ [
+ "▁uncertain",
+ -12.61415958404541
+ ],
+ [
+ "restul",
+ -12.614168167114258
+ ],
+ [
+ "▁colourful",
+ -12.61420726776123
+ ],
+ [
+ "▁accountant",
+ -12.614338874816895
+ ],
+ [
+ "bourg",
+ -12.614532470703125
+ ],
+ [
+ "▁structuri",
+ -12.614538192749023
+ ],
+ [
+ "▁Booking",
+ -12.61465835571289
+ ],
+ [
+ "intéresse",
+ -12.614683151245117
+ ],
+ [
+ "▁coordinated",
+ -12.614753723144531
+ ],
+ [
+ "▁precaution",
+ -12.61497688293457
+ ],
+ [
+ "▁Cheese",
+ -12.615015983581543
+ ],
+ [
+ "▁surfing",
+ -12.615192413330078
+ ],
+ [
+ "▁souffr",
+ -12.61524486541748
+ ],
+ [
+ "▁Menu",
+ -12.615447998046875
+ ],
+ [
+ "▁arthritis",
+ -12.615593910217285
+ ],
+ [
+ "▁headphones",
+ -12.615601539611816
+ ],
+ [
+ "▁upgrading",
+ -12.615602493286133
+ ],
+ [
+ "▁apparel",
+ -12.615653038024902
+ ],
+ [
+ "▁Haushalt",
+ -12.61572551727295
+ ],
+ [
+ "▁Personally",
+ -12.615815162658691
+ ],
+ [
+ "▁insane",
+ -12.615950584411621
+ ],
+ [
+ "▁fonduri",
+ -12.616083145141602
+ ],
+ [
+ "▁entier",
+ -12.616239547729492
+ ],
+ [
+ "▁Herbst",
+ -12.616264343261719
+ ],
+ [
+ "▁cyclist",
+ -12.616331100463867
+ ],
+ [
+ "▁filmmaker",
+ -12.616741180419922
+ ],
+ [
+ "▁Portuguese",
+ -12.616829872131348
+ ],
+ [
+ "▁nominee",
+ -12.616851806640625
+ ],
+ [
+ "▁Yang",
+ -12.616857528686523
+ ],
+ [
+ "▁slate",
+ -12.616943359375
+ ],
+ [
+ "▁entièrement",
+ -12.616974830627441
+ ],
+ [
+ "▁Umgang",
+ -12.617049217224121
+ ],
+ [
+ "shifted",
+ -12.617135047912598
+ ],
+ [
+ "▁défaut",
+ -12.617138862609863
+ ],
+ [
+ "heiz",
+ -12.617246627807617
+ ],
+ [
+ "▁Seal",
+ -12.617379188537598
+ ],
+ [
+ "▁servicing",
+ -12.617451667785645
+ ],
+ [
+ "marketing",
+ -12.617562294006348
+ ],
+ [
+ "▁demandé",
+ -12.617755889892578
+ ],
+ [
+ "TING",
+ -12.617841720581055
+ ],
+ [
+ "▁modifier",
+ -12.617907524108887
+ ],
+ [
+ "lysis",
+ -12.617966651916504
+ ],
+ [
+ "▁suplimentare",
+ -12.618117332458496
+ ],
+ [
+ "OTHER",
+ -12.618359565734863
+ ],
+ [
+ "Graph",
+ -12.618379592895508
+ ],
+ [
+ "▁coincide",
+ -12.618448257446289
+ ],
+ [
+ "governed",
+ -12.618598937988281
+ ],
+ [
+ "▁locking",
+ -12.618638038635254
+ ],
+ [
+ "▁Properties",
+ -12.618685722351074
+ ],
+ [
+ "▁Panama",
+ -12.61876392364502
+ ],
+ [
+ "▁Coupe",
+ -12.618846893310547
+ ],
+ [
+ "songwriter",
+ -12.618978500366211
+ ],
+ [
+ "exhibited",
+ -12.618988990783691
+ ],
+ [
+ "▁semnificativ",
+ -12.618995666503906
+ ],
+ [
+ "▁purchaser",
+ -12.619004249572754
+ ],
+ [
+ "▁puff",
+ -12.619097709655762
+ ],
+ [
+ "Back",
+ -12.619105339050293
+ ],
+ [
+ "fragt",
+ -12.61919116973877
+ ],
+ [
+ "▁deputy",
+ -12.619362831115723
+ ],
+ [
+ "▁revien",
+ -12.619556427001953
+ ],
+ [
+ "▁Christine",
+ -12.619558334350586
+ ],
+ [
+ "▁Cities",
+ -12.619573593139648
+ ],
+ [
+ "▁Charakter",
+ -12.61961555480957
+ ],
+ [
+ "atteindre",
+ -12.619625091552734
+ ],
+ [
+ "▁fou",
+ -12.619635581970215
+ ],
+ [
+ "▁obligatoire",
+ -12.619643211364746
+ ],
+ [
+ "INA",
+ -12.619791030883789
+ ],
+ [
+ "etc",
+ -12.6198148727417
+ ],
+ [
+ "▁newborn",
+ -12.620091438293457
+ ],
+ [
+ "▁explicitly",
+ -12.620116233825684
+ ],
+ [
+ "simplest",
+ -12.620203018188477
+ ],
+ [
+ "▁plateforme",
+ -12.62023639678955
+ ],
+ [
+ "ordinate",
+ -12.620291709899902
+ ],
+ [
+ "displaying",
+ -12.620346069335938
+ ],
+ [
+ "▁messy",
+ -12.620464324951172
+ ],
+ [
+ "gespielt",
+ -12.620466232299805
+ ],
+ [
+ "▁electron",
+ -12.62061882019043
+ ],
+ [
+ "▁Dreh",
+ -12.620796203613281
+ ],
+ [
+ "▁ambient",
+ -12.620976448059082
+ ],
+ [
+ "340",
+ -12.620979309082031
+ ],
+ [
+ "▁directive",
+ -12.62109375
+ ],
+ [
+ "▁Vall",
+ -12.621152877807617
+ ],
+ [
+ "ookie",
+ -12.621206283569336
+ ],
+ [
+ "▁wasted",
+ -12.621304512023926
+ ],
+ [
+ "CIS",
+ -12.621367454528809
+ ],
+ [
+ "lude",
+ -12.621378898620605
+ ],
+ [
+ "rach",
+ -12.621472358703613
+ ],
+ [
+ "▁gasest",
+ -12.62150764465332
+ ],
+ [
+ "▁miros",
+ -12.62150764465332
+ ],
+ [
+ "transforming",
+ -12.621536254882812
+ ],
+ [
+ "▁Milwaukee",
+ -12.621787071228027
+ ],
+ [
+ "▁uncommon",
+ -12.621789932250977
+ ],
+ [
+ "▁tableau",
+ -12.621841430664062
+ ],
+ [
+ "geräte",
+ -12.621952056884766
+ ],
+ [
+ "ophil",
+ -12.622139930725098
+ ],
+ [
+ "▁Jeep",
+ -12.62220287322998
+ ],
+ [
+ "▁wreck",
+ -12.622422218322754
+ ],
+ [
+ "LAND",
+ -12.622434616088867
+ ],
+ [
+ "attach",
+ -12.622566223144531
+ ],
+ [
+ "▁Panther",
+ -12.622634887695312
+ ],
+ [
+ "9:30",
+ -12.622777938842773
+ ],
+ [
+ "▁induce",
+ -12.622974395751953
+ ],
+ [
+ "▁privest",
+ -12.623006820678711
+ ],
+ [
+ "Ident",
+ -12.623047828674316
+ ],
+ [
+ "▁illnesses",
+ -12.623076438903809
+ ],
+ [
+ "▁inhabitants",
+ -12.623138427734375
+ ],
+ [
+ "▁fehlen",
+ -12.623357772827148
+ ],
+ [
+ "obtenu",
+ -12.623391151428223
+ ],
+ [
+ "▁gegründet",
+ -12.623655319213867
+ ],
+ [
+ "ARA",
+ -12.623711585998535
+ ],
+ [
+ "3-2",
+ -12.623835563659668
+ ],
+ [
+ "▁milliards",
+ -12.623968124389648
+ ],
+ [
+ "▁Bü",
+ -12.624001502990723
+ ],
+ [
+ "▁angegeben",
+ -12.624102592468262
+ ],
+ [
+ "TUR",
+ -12.624143600463867
+ ],
+ [
+ "▁arab",
+ -12.624166488647461
+ ],
+ [
+ "▁Scientist",
+ -12.624275207519531
+ ],
+ [
+ "▁minut",
+ -12.624394416809082
+ ],
+ [
+ "▁beast",
+ -12.624481201171875
+ ],
+ [
+ "▁accidentally",
+ -12.624573707580566
+ ],
+ [
+ "WN",
+ -12.624579429626465
+ ],
+ [
+ "▁Ralph",
+ -12.624588966369629
+ ],
+ [
+ "hängt",
+ -12.62462329864502
+ ],
+ [
+ "▁Erik",
+ -12.624639511108398
+ ],
+ [
+ "▁différent",
+ -12.624711990356445
+ ],
+ [
+ "▁conformitate",
+ -12.624842643737793
+ ],
+ [
+ "thriving",
+ -12.624900817871094
+ ],
+ [
+ "▁Piece",
+ -12.625123023986816
+ ],
+ [
+ "plasm",
+ -12.625152587890625
+ ],
+ [
+ "▁erwarten",
+ -12.62520980834961
+ ],
+ [
+ "owski",
+ -12.62523365020752
+ ],
+ [
+ "prayed",
+ -12.625293731689453
+ ],
+ [
+ "three",
+ -12.625542640686035
+ ],
+ [
+ "▁soundtrack",
+ -12.625651359558105
+ ],
+ [
+ "guru",
+ -12.625709533691406
+ ],
+ [
+ "▁cracked",
+ -12.625710487365723
+ ],
+ [
+ "▁adh",
+ -12.625823020935059
+ ],
+ [
+ "▁maître",
+ -12.625834465026855
+ ],
+ [
+ "▁Oberfläche",
+ -12.62585735321045
+ ],
+ [
+ "▁crab",
+ -12.625886917114258
+ ],
+ [
+ "▁Foster",
+ -12.625944137573242
+ ],
+ [
+ "▁gemütlich",
+ -12.626145362854004
+ ],
+ [
+ "SIC",
+ -12.626226425170898
+ ],
+ [
+ "ième",
+ -12.626298904418945
+ ],
+ [
+ "▁Few",
+ -12.626330375671387
+ ],
+ [
+ "gérer",
+ -12.626360893249512
+ ],
+ [
+ "2006",
+ -12.626456260681152
+ ],
+ [
+ "cool",
+ -12.626498222351074
+ ],
+ [
+ "▁dispune",
+ -12.626523971557617
+ ],
+ [
+ "recevoir",
+ -12.626577377319336
+ ],
+ [
+ "▁Bak",
+ -12.626585960388184
+ ],
+ [
+ "▁steer",
+ -12.62659740447998
+ ],
+ [
+ "ICS",
+ -12.626733779907227
+ ],
+ [
+ "▁Brett",
+ -12.626733779907227
+ ],
+ [
+ "▁downside",
+ -12.626751899719238
+ ],
+ [
+ "▁residency",
+ -12.62678050994873
+ ],
+ [
+ "important",
+ -12.626991271972656
+ ],
+ [
+ "ubb",
+ -12.627073287963867
+ ],
+ [
+ "mony",
+ -12.627259254455566
+ ],
+ [
+ "▁leasing",
+ -12.627341270446777
+ ],
+ [
+ "▁Gir",
+ -12.62735366821289
+ ],
+ [
+ "▁Biology",
+ -12.627364158630371
+ ],
+ [
+ "▁Colin",
+ -12.627463340759277
+ ],
+ [
+ "▁complicat",
+ -12.627775192260742
+ ],
+ [
+ "▁regroup",
+ -12.627899169921875
+ ],
+ [
+ "SPA",
+ -12.627950668334961
+ ],
+ [
+ "▁Veranstaltungen",
+ -12.627986907958984
+ ],
+ [
+ "convicted",
+ -12.628019332885742
+ ],
+ [
+ "▁Wonderful",
+ -12.628636360168457
+ ],
+ [
+ "züge",
+ -12.628799438476562
+ ],
+ [
+ "yton",
+ -12.628813743591309
+ ],
+ [
+ "EMENT",
+ -12.628887176513672
+ ],
+ [
+ "▁bent",
+ -12.62893009185791
+ ],
+ [
+ "heben",
+ -12.629231452941895
+ ],
+ [
+ "▁Sustainable",
+ -12.62926959991455
+ ],
+ [
+ "▁Newcastle",
+ -12.629276275634766
+ ],
+ [
+ "mother",
+ -12.629507064819336
+ ],
+ [
+ "▁eighth",
+ -12.629572868347168
+ ],
+ [
+ "▁atmosfer",
+ -12.629582405090332
+ ],
+ [
+ "expériment",
+ -12.629584312438965
+ ],
+ [
+ "▁Interest",
+ -12.629608154296875
+ ],
+ [
+ "▁successes",
+ -12.62964153289795
+ ],
+ [
+ "▁preschool",
+ -12.629802703857422
+ ],
+ [
+ "▁Funeral",
+ -12.629900932312012
+ ],
+ [
+ "blast",
+ -12.630083084106445
+ ],
+ [
+ "▁dimensiuni",
+ -12.630125999450684
+ ],
+ [
+ "▁Dow",
+ -12.630167007446289
+ ],
+ [
+ "▁pulp",
+ -12.63022518157959
+ ],
+ [
+ "▁Heather",
+ -12.630356788635254
+ ],
+ [
+ "▁erstellen",
+ -12.63044261932373
+ ],
+ [
+ "locating",
+ -12.630470275878906
+ ],
+ [
+ "direct",
+ -12.630475997924805
+ ],
+ [
+ "▁tractor",
+ -12.630494117736816
+ ],
+ [
+ "growing",
+ -12.630576133728027
+ ],
+ [
+ "▁inventor",
+ -12.630587577819824
+ ],
+ [
+ "ASA",
+ -12.63060188293457
+ ],
+ [
+ "insta",
+ -12.630732536315918
+ ],
+ [
+ "yana",
+ -12.63082504272461
+ ],
+ [
+ "▁squash",
+ -12.630839347839355
+ ],
+ [
+ "▁Basketball",
+ -12.630853652954102
+ ],
+ [
+ "AMA",
+ -12.631041526794434
+ ],
+ [
+ "insel",
+ -12.631093978881836
+ ],
+ [
+ "▁Fisch",
+ -12.631138801574707
+ ],
+ [
+ "▁metaphor",
+ -12.631221771240234
+ ],
+ [
+ "TES",
+ -12.631304740905762
+ ],
+ [
+ "▁conduce",
+ -12.631308555603027
+ ],
+ [
+ "stehende",
+ -12.631370544433594
+ ],
+ [
+ "▁FAQ",
+ -12.631475448608398
+ ],
+ [
+ "▁bezeichnet",
+ -12.631658554077148
+ ],
+ [
+ "wendung",
+ -12.631706237792969
+ ],
+ [
+ "▁Commonwealth",
+ -12.631776809692383
+ ],
+ [
+ "▁bait",
+ -12.631793975830078
+ ],
+ [
+ "▁Umsetzung",
+ -12.631834030151367
+ ],
+ [
+ "▁Equi",
+ -12.632063865661621
+ ],
+ [
+ "▁validity",
+ -12.632109642028809
+ ],
+ [
+ "Off",
+ -12.63222599029541
+ ],
+ [
+ "▁produsul",
+ -12.632314682006836
+ ],
+ [
+ "▁sensory",
+ -12.632363319396973
+ ],
+ [
+ "▁Imperial",
+ -12.632501602172852
+ ],
+ [
+ "▁Dick",
+ -12.632542610168457
+ ],
+ [
+ "kampf",
+ -12.632596969604492
+ ],
+ [
+ "▁Arzt",
+ -12.63267993927002
+ ],
+ [
+ "▁Reason",
+ -12.63267993927002
+ ],
+ [
+ "ITS",
+ -12.63270092010498
+ ],
+ [
+ "URL",
+ -12.632720947265625
+ ],
+ [
+ "demonstrates",
+ -12.632725715637207
+ ],
+ [
+ "▁dépend",
+ -12.632753372192383
+ ],
+ [
+ "NAS",
+ -12.632970809936523
+ ],
+ [
+ "▁funcți",
+ -12.633031845092773
+ ],
+ [
+ "▁vulnerability",
+ -12.633085250854492
+ ],
+ [
+ "2.7",
+ -12.633143424987793
+ ],
+ [
+ "layered",
+ -12.633152961730957
+ ],
+ [
+ "escence",
+ -12.633206367492676
+ ],
+ [
+ "▁République",
+ -12.633346557617188
+ ],
+ [
+ "▁Lust",
+ -12.633377075195312
+ ],
+ [
+ "▁sute",
+ -12.633381843566895
+ ],
+ [
+ "▁autonomous",
+ -12.633661270141602
+ ],
+ [
+ "Biserica",
+ -12.633662223815918
+ ],
+ [
+ "▁Chuck",
+ -12.633749961853027
+ ],
+ [
+ "▁protéger",
+ -12.6339750289917
+ ],
+ [
+ "rrell",
+ -12.634061813354492
+ ],
+ [
+ "▁Schaden",
+ -12.634062767028809
+ ],
+ [
+ "prennent",
+ -12.634100914001465
+ ],
+ [
+ "maß",
+ -12.6343412399292
+ ],
+ [
+ "OV",
+ -12.634453773498535
+ ],
+ [
+ "▁Wake",
+ -12.63450813293457
+ ],
+ [
+ "produire",
+ -12.634635925292969
+ ],
+ [
+ "▁Elder",
+ -12.634749412536621
+ ],
+ [
+ "Max",
+ -12.634839057922363
+ ],
+ [
+ "▁Chemistry",
+ -12.634918212890625
+ ],
+ [
+ "▁gourmet",
+ -12.634918212890625
+ ],
+ [
+ "erri",
+ -12.634967803955078
+ ],
+ [
+ "ени",
+ -12.635085105895996
+ ],
+ [
+ "▁Gru",
+ -12.635147094726562
+ ],
+ [
+ "▁vorbit",
+ -12.635408401489258
+ ],
+ [
+ "▁precede",
+ -12.635455131530762
+ ],
+ [
+ "▁randomly",
+ -12.635489463806152
+ ],
+ [
+ "▁efecte",
+ -12.63563060760498
+ ],
+ [
+ "▁calatori",
+ -12.635668754577637
+ ],
+ [
+ "▁Poor",
+ -12.635765075683594
+ ],
+ [
+ "List",
+ -12.635781288146973
+ ],
+ [
+ "▁regula",
+ -12.635964393615723
+ ],
+ [
+ "▁organisé",
+ -12.636028289794922
+ ],
+ [
+ "Div",
+ -12.636076927185059
+ ],
+ [
+ "▁volunteering",
+ -12.636423110961914
+ ],
+ [
+ "▁horr",
+ -12.636449813842773
+ ],
+ [
+ "9.99",
+ -12.636487007141113
+ ],
+ [
+ "▁UPS",
+ -12.636513710021973
+ ],
+ [
+ "▁englez",
+ -12.63652229309082
+ ],
+ [
+ "▁Eden",
+ -12.636523246765137
+ ],
+ [
+ "GG",
+ -12.63659954071045
+ ],
+ [
+ "▁typing",
+ -12.63664722442627
+ ],
+ [
+ "Likewise",
+ -12.636700630187988
+ ],
+ [
+ "▁stabilize",
+ -12.636737823486328
+ ],
+ [
+ "physio",
+ -12.636747360229492
+ ],
+ [
+ "ми",
+ -12.636785507202148
+ ],
+ [
+ "▁protagonist",
+ -12.636808395385742
+ ],
+ [
+ "▁velvet",
+ -12.636812210083008
+ ],
+ [
+ "schrank",
+ -12.636861801147461
+ ],
+ [
+ "▁Allah",
+ -12.63693618774414
+ ],
+ [
+ "▁forefront",
+ -12.636968612670898
+ ],
+ [
+ "▁salaries",
+ -12.637001037597656
+ ],
+ [
+ "▁prediction",
+ -12.637041091918945
+ ],
+ [
+ "▁Advent",
+ -12.637182235717773
+ ],
+ [
+ "politik",
+ -12.637280464172363
+ ],
+ [
+ "▁Heimat",
+ -12.637350082397461
+ ],
+ [
+ "ducted",
+ -12.637380599975586
+ ],
+ [
+ "ASH",
+ -12.637386322021484
+ ],
+ [
+ "▁Mold",
+ -12.637773513793945
+ ],
+ [
+ "▁publi",
+ -12.63784122467041
+ ],
+ [
+ "▁Vil",
+ -12.637892723083496
+ ],
+ [
+ "▁stu",
+ -12.637925148010254
+ ],
+ [
+ "INTE",
+ -12.638032913208008
+ ],
+ [
+ "▁fave",
+ -12.638151168823242
+ ],
+ [
+ "▁grounded",
+ -12.638175010681152
+ ],
+ [
+ "▁Anything",
+ -12.638184547424316
+ ],
+ [
+ "vik",
+ -12.638481140136719
+ ],
+ [
+ "Bank",
+ -12.63853645324707
+ ],
+ [
+ "deserved",
+ -12.638550758361816
+ ],
+ [
+ "machen",
+ -12.63874626159668
+ ],
+ [
+ "▁rugged",
+ -12.638751029968262
+ ],
+ [
+ "▁Nest",
+ -12.638901710510254
+ ],
+ [
+ "▁profund",
+ -12.639043807983398
+ ],
+ [
+ "▁quantum",
+ -12.639067649841309
+ ],
+ [
+ "▁funcționa",
+ -12.639118194580078
+ ],
+ [
+ "klu",
+ -12.639158248901367
+ ],
+ [
+ "▁consulter",
+ -12.63917350769043
+ ],
+ [
+ "MED",
+ -12.639286994934082
+ ],
+ [
+ "▁câştig",
+ -12.639334678649902
+ ],
+ [
+ "▁săptămâni",
+ -12.639334678649902
+ ],
+ [
+ "questioned",
+ -12.639517784118652
+ ],
+ [
+ "▁Trop",
+ -12.639530181884766
+ ],
+ [
+ "▁convo",
+ -12.639533042907715
+ ],
+ [
+ "▁sparkling",
+ -12.639533996582031
+ ],
+ [
+ "▁specialise",
+ -12.639566421508789
+ ],
+ [
+ "▁pancake",
+ -12.639726638793945
+ ],
+ [
+ "habitude",
+ -12.639727592468262
+ ],
+ [
+ "phal",
+ -12.640009880065918
+ ],
+ [
+ "▁Roche",
+ -12.640158653259277
+ ],
+ [
+ "▁personalities",
+ -12.640250205993652
+ ],
+ [
+ "▁Venice",
+ -12.640308380126953
+ ],
+ [
+ "▁comerciale",
+ -12.640379905700684
+ ],
+ [
+ "▁wounded",
+ -12.64075756072998
+ ],
+ [
+ "▁oraş",
+ -12.640864372253418
+ ],
+ [
+ "▁Pepper",
+ -12.641044616699219
+ ],
+ [
+ "▁Tourist",
+ -12.641094207763672
+ ],
+ [
+ "▁Mull",
+ -12.64116382598877
+ ],
+ [
+ "▁dignity",
+ -12.641234397888184
+ ],
+ [
+ "▁Fixed",
+ -12.641291618347168
+ ],
+ [
+ "çant",
+ -12.64130687713623
+ ],
+ [
+ "▁spectator",
+ -12.641402244567871
+ ],
+ [
+ "▁somn",
+ -12.641685485839844
+ ],
+ [
+ "▁ständig",
+ -12.641820907592773
+ ],
+ [
+ "▁resilience",
+ -12.641866683959961
+ ],
+ [
+ "▁Malta",
+ -12.642251014709473
+ ],
+ [
+ "▁problemele",
+ -12.642253875732422
+ ],
+ [
+ "▁Martha",
+ -12.642254829406738
+ ],
+ [
+ "▁extern",
+ -12.642267227172852
+ ],
+ [
+ "embre",
+ -12.642379760742188
+ ],
+ [
+ "▁médical",
+ -12.642526626586914
+ ],
+ [
+ "fordern",
+ -12.64256477355957
+ ],
+ [
+ "nji",
+ -12.642592430114746
+ ],
+ [
+ "▁aboard",
+ -12.642740249633789
+ ],
+ [
+ "▁sidewalk",
+ -12.642759323120117
+ ],
+ [
+ "WIN",
+ -12.642775535583496
+ ],
+ [
+ "▁Bobby",
+ -12.642842292785645
+ ],
+ [
+ "▁umfangreiche",
+ -12.642876625061035
+ ],
+ [
+ "leid",
+ -12.64292049407959
+ ],
+ [
+ "▁compens",
+ -12.642967224121094
+ ],
+ [
+ "▁juge",
+ -12.64299488067627
+ ],
+ [
+ "gerufen",
+ -12.64311408996582
+ ],
+ [
+ "▁médicament",
+ -12.643135070800781
+ ],
+ [
+ "▁1918",
+ -12.643155097961426
+ ],
+ [
+ "▁blanche",
+ -12.643163681030273
+ ],
+ [
+ "▁pleasing",
+ -12.643220901489258
+ ],
+ [
+ "▁propria",
+ -12.643471717834473
+ ],
+ [
+ "ergebnisse",
+ -12.643503189086914
+ ],
+ [
+ "▁retrouv",
+ -12.643571853637695
+ ],
+ [
+ "urteil",
+ -12.643592834472656
+ ],
+ [
+ "▁Draft",
+ -12.64361572265625
+ ],
+ [
+ "▁concluzi",
+ -12.643671035766602
+ ],
+ [
+ "centralized",
+ -12.643789291381836
+ ],
+ [
+ "▁Hannah",
+ -12.64382266998291
+ ],
+ [
+ "grija",
+ -12.64392375946045
+ ],
+ [
+ "▁Exercise",
+ -12.643972396850586
+ ],
+ [
+ "RAL",
+ -12.644001960754395
+ ],
+ [
+ "creme",
+ -12.64408016204834
+ ],
+ [
+ "High",
+ -12.644126892089844
+ ],
+ [
+ "clude",
+ -12.644131660461426
+ ],
+ [
+ "Considering",
+ -12.644208908081055
+ ],
+ [
+ "▁Guarantee",
+ -12.644404411315918
+ ],
+ [
+ "▁cuptor",
+ -12.644436836242676
+ ],
+ [
+ "ivität",
+ -12.64468002319336
+ ],
+ [
+ "▁Southwest",
+ -12.644882202148438
+ ],
+ [
+ "▁vivant",
+ -12.644890785217285
+ ],
+ [
+ "Your",
+ -12.64498519897461
+ ],
+ [
+ "▁Stunde",
+ -12.645003318786621
+ ],
+ [
+ "▁Ethernet",
+ -12.645040512084961
+ ],
+ [
+ "angebote",
+ -12.645078659057617
+ ],
+ [
+ "▁Sage",
+ -12.645271301269531
+ ],
+ [
+ "▁Boeing",
+ -12.645295143127441
+ ],
+ [
+ "▁$300",
+ -12.645381927490234
+ ],
+ [
+ "2-4",
+ -12.64546012878418
+ ],
+ [
+ "▁nécessit",
+ -12.645516395568848
+ ],
+ [
+ "▁ferment",
+ -12.645599365234375
+ ],
+ [
+ "▁Anmeldung",
+ -12.64567756652832
+ ],
+ [
+ "▁exhausted",
+ -12.645758628845215
+ ],
+ [
+ "▁Schloss",
+ -12.645772933959961
+ ],
+ [
+ "▁Replacement",
+ -12.645859718322754
+ ],
+ [
+ "▁Aussi",
+ -12.645933151245117
+ ],
+ [
+ "jection",
+ -12.646127700805664
+ ],
+ [
+ "978",
+ -12.64615535736084
+ ],
+ [
+ "▁siège",
+ -12.646258354187012
+ ],
+ [
+ "crest",
+ -12.646310806274414
+ ],
+ [
+ "▁jumatate",
+ -12.646312713623047
+ ],
+ [
+ "effizient",
+ -12.646317481994629
+ ],
+ [
+ "▁colaborare",
+ -12.6464262008667
+ ],
+ [
+ "HQ",
+ -12.646615028381348
+ ],
+ [
+ "130",
+ -12.646695137023926
+ ],
+ [
+ "culaire",
+ -12.646907806396484
+ ],
+ [
+ "▁Jamaica",
+ -12.646952629089355
+ ],
+ [
+ "▁cardboard",
+ -12.64731216430664
+ ],
+ [
+ "▁technische",
+ -12.64731502532959
+ ],
+ [
+ "▁cereri",
+ -12.647507667541504
+ ],
+ [
+ "▁contradict",
+ -12.647570610046387
+ ],
+ [
+ "▁irrigation",
+ -12.647586822509766
+ ],
+ [
+ "Nume",
+ -12.64765739440918
+ ],
+ [
+ "▁Bier",
+ -12.647714614868164
+ ],
+ [
+ "▁livrare",
+ -12.647903442382812
+ ],
+ [
+ "▁reservoir",
+ -12.647906303405762
+ ],
+ [
+ "vâr",
+ -12.648130416870117
+ ],
+ [
+ "▁galben",
+ -12.648213386535645
+ ],
+ [
+ "▁Geneva",
+ -12.648303985595703
+ ],
+ [
+ "▁lightning",
+ -12.648418426513672
+ ],
+ [
+ "wished",
+ -12.64842414855957
+ ],
+ [
+ "▁Blind",
+ -12.648481369018555
+ ],
+ [
+ "Interested",
+ -12.648499488830566
+ ],
+ [
+ "▁Primări",
+ -12.648627281188965
+ ],
+ [
+ "anthropo",
+ -12.648954391479492
+ ],
+ [
+ "▁Transaction",
+ -12.648961067199707
+ ],
+ [
+ "▁marcat",
+ -12.648971557617188
+ ],
+ [
+ "▁gelegen",
+ -12.649077415466309
+ ],
+ [
+ "▁contemporain",
+ -12.649182319641113
+ ],
+ [
+ "▁politică",
+ -12.649182319641113
+ ],
+ [
+ "▁1948",
+ -12.64928150177002
+ ],
+ [
+ "▁Mik",
+ -12.649287223815918
+ ],
+ [
+ "▁preţ",
+ -12.649310111999512
+ ],
+ [
+ "moor",
+ -12.649312973022461
+ ],
+ [
+ "ANN",
+ -12.649432182312012
+ ],
+ [
+ "▁constructive",
+ -12.649454116821289
+ ],
+ [
+ "konzept",
+ -12.649502754211426
+ ],
+ [
+ "▁entendu",
+ -12.649511337280273
+ ],
+ [
+ "▁Genesis",
+ -12.649541854858398
+ ],
+ [
+ "arzt",
+ -12.649581909179688
+ ],
+ [
+ "▁Allgemein",
+ -12.64970874786377
+ ],
+ [
+ "▁Derby",
+ -12.649725914001465
+ ],
+ [
+ "Class",
+ -12.649762153625488
+ ],
+ [
+ "▁$12",
+ -12.649770736694336
+ ],
+ [
+ "▁Tube",
+ -12.6498441696167
+ ],
+ [
+ "▁Contribu",
+ -12.649847030639648
+ ],
+ [
+ "▁HAVE",
+ -12.649860382080078
+ ],
+ [
+ "▁oxide",
+ -12.64986515045166
+ ],
+ [
+ "▁producator",
+ -12.649941444396973
+ ],
+ [
+ "▁Bench",
+ -12.650132179260254
+ ],
+ [
+ "▁comprehend",
+ -12.650139808654785
+ ],
+ [
+ "▁Damen",
+ -12.650494575500488
+ ],
+ [
+ "▁Garant",
+ -12.65056037902832
+ ],
+ [
+ "▁disappointing",
+ -12.650614738464355
+ ],
+ [
+ "▁réalisée",
+ -12.650693893432617
+ ],
+ [
+ "▁comportement",
+ -12.65072250366211
+ ],
+ [
+ "▁clash",
+ -12.650753021240234
+ ],
+ [
+ "▁curry",
+ -12.65076732635498
+ ],
+ [
+ "▁Lebanon",
+ -12.65078067779541
+ ],
+ [
+ "▁Romaniei",
+ -12.650784492492676
+ ],
+ [
+ "▁reprise",
+ -12.650840759277344
+ ],
+ [
+ "▁perceive",
+ -12.65095329284668
+ ],
+ [
+ "▁weaknesses",
+ -12.65101146697998
+ ],
+ [
+ "▁aminti",
+ -12.651057243347168
+ ],
+ [
+ "▁Concern",
+ -12.651103973388672
+ ],
+ [
+ "shadow",
+ -12.651310920715332
+ ],
+ [
+ "▁basin",
+ -12.651311874389648
+ ],
+ [
+ "moral",
+ -12.652063369750977
+ ],
+ [
+ "▁Hughes",
+ -12.652101516723633
+ ],
+ [
+ "Psych",
+ -12.652266502380371
+ ],
+ [
+ "▁Lieferung",
+ -12.65227222442627
+ ],
+ [
+ "▁serrurier",
+ -12.652379035949707
+ ],
+ [
+ "ussi",
+ -12.652386665344238
+ ],
+ [
+ "▁timpului",
+ -12.6524658203125
+ ],
+ [
+ "üm",
+ -12.652629852294922
+ ],
+ [
+ "▁Vladimir",
+ -12.652701377868652
+ ],
+ [
+ "▁Jag",
+ -12.65279483795166
+ ],
+ [
+ "▁verific",
+ -12.652849197387695
+ ],
+ [
+ "▁Pru",
+ -12.652894020080566
+ ],
+ [
+ "▁Laut",
+ -12.653285026550293
+ ],
+ [
+ "ITA",
+ -12.653287887573242
+ ],
+ [
+ "usually",
+ -12.653294563293457
+ ],
+ [
+ "▁carrière",
+ -12.65341854095459
+ ],
+ [
+ "▁extracted",
+ -12.653663635253906
+ ],
+ [
+ "kultur",
+ -12.653679847717285
+ ],
+ [
+ "öpfe",
+ -12.653932571411133
+ ],
+ [
+ "▁rejection",
+ -12.654016494750977
+ ],
+ [
+ "▁Hydr",
+ -12.654062271118164
+ ],
+ [
+ "▁informaţii",
+ -12.654098510742188
+ ],
+ [
+ "▁tolerate",
+ -12.654122352600098
+ ],
+ [
+ "▁cinéma",
+ -12.654302597045898
+ ],
+ [
+ "traumatic",
+ -12.654305458068848
+ ],
+ [
+ "produkt",
+ -12.654450416564941
+ ],
+ [
+ "▁Contest",
+ -12.654560089111328
+ ],
+ [
+ "lotte",
+ -12.654570579528809
+ ],
+ [
+ "▁Pension",
+ -12.65461254119873
+ ],
+ [
+ "▁Advertising",
+ -12.654623985290527
+ ],
+ [
+ "▁payout",
+ -12.654772758483887
+ ],
+ [
+ "▁Amanda",
+ -12.65481185913086
+ ],
+ [
+ "Elect",
+ -12.65485668182373
+ ],
+ [
+ "▁interiorul",
+ -12.654996871948242
+ ],
+ [
+ "stay",
+ -12.655348777770996
+ ],
+ [
+ "▁feminine",
+ -12.655352592468262
+ ],
+ [
+ "▁întâmplă",
+ -12.655437469482422
+ ],
+ [
+ "▁insult",
+ -12.65562915802002
+ ],
+ [
+ "▁chocolat",
+ -12.65567398071289
+ ],
+ [
+ "▁noroc",
+ -12.655750274658203
+ ],
+ [
+ "▁centr",
+ -12.655781745910645
+ ],
+ [
+ "▁Bühne",
+ -12.655858039855957
+ ],
+ [
+ "mighty",
+ -12.6558837890625
+ ],
+ [
+ "▁Buddha",
+ -12.655908584594727
+ ],
+ [
+ "▁parental",
+ -12.655997276306152
+ ],
+ [
+ "storm",
+ -12.656451225280762
+ ],
+ [
+ "recurring",
+ -12.6565523147583
+ ],
+ [
+ "▁luxe",
+ -12.656588554382324
+ ],
+ [
+ "niște",
+ -12.656728744506836
+ ],
+ [
+ "cuit",
+ -12.656839370727539
+ ],
+ [
+ "▁ausgewählt",
+ -12.656880378723145
+ ],
+ [
+ "▁dumb",
+ -12.657047271728516
+ ],
+ [
+ "IPS",
+ -12.657127380371094
+ ],
+ [
+ "▁Thir",
+ -12.65717887878418
+ ],
+ [
+ "Definitely",
+ -12.657195091247559
+ ],
+ [
+ "▁hilarious",
+ -12.657195091247559
+ ],
+ [
+ "▁rainbow",
+ -12.657231330871582
+ ],
+ [
+ "▁Bravo",
+ -12.657251358032227
+ ],
+ [
+ "▁entstanden",
+ -12.657259941101074
+ ],
+ [
+ "itorul",
+ -12.657269477844238
+ ],
+ [
+ "▁prosperity",
+ -12.657299041748047
+ ],
+ [
+ "▁Bord",
+ -12.657336235046387
+ ],
+ [
+ "▁familiei",
+ -12.657363891601562
+ ],
+ [
+ "▁scade",
+ -12.657425880432129
+ ],
+ [
+ "wöhn",
+ -12.657426834106445
+ ],
+ [
+ "▁ingrediente",
+ -12.65743637084961
+ ],
+ [
+ "RAD",
+ -12.657441139221191
+ ],
+ [
+ "▁tăi",
+ -12.657472610473633
+ ],
+ [
+ "bours",
+ -12.65747356414795
+ ],
+ [
+ "ATI",
+ -12.657540321350098
+ ],
+ [
+ "▁Blake",
+ -12.65761661529541
+ ],
+ [
+ "▁Implement",
+ -12.657712936401367
+ ],
+ [
+ "▁Beziehung",
+ -12.657838821411133
+ ],
+ [
+ "finanz",
+ -12.657953262329102
+ ],
+ [
+ "intestin",
+ -12.658513069152832
+ ],
+ [
+ "ließen",
+ -12.658535957336426
+ ],
+ [
+ "▁récent",
+ -12.658594131469727
+ ],
+ [
+ "▁laminate",
+ -12.658692359924316
+ ],
+ [
+ "▁Hör",
+ -12.65876579284668
+ ],
+ [
+ "▁personnalisé",
+ -12.658804893493652
+ ],
+ [
+ "edel",
+ -12.65890121459961
+ ],
+ [
+ "▁advertisement",
+ -12.658902168273926
+ ],
+ [
+ "▁pinterest",
+ -12.658921241760254
+ ],
+ [
+ "185",
+ -12.659058570861816
+ ],
+ [
+ "identité",
+ -12.65938949584961
+ ],
+ [
+ "▁Brick",
+ -12.659408569335938
+ ],
+ [
+ "Glu",
+ -12.65941047668457
+ ],
+ [
+ "▁attendant",
+ -12.659571647644043
+ ],
+ [
+ "▁Flip",
+ -12.659614562988281
+ ],
+ [
+ "attracting",
+ -12.659662246704102
+ ],
+ [
+ "functional",
+ -12.659703254699707
+ ],
+ [
+ "conceived",
+ -12.659772872924805
+ ],
+ [
+ "▁summarize",
+ -12.659773826599121
+ ],
+ [
+ "adjusting",
+ -12.659809112548828
+ ],
+ [
+ "CAL",
+ -12.660041809082031
+ ],
+ [
+ "▁Operating",
+ -12.660076141357422
+ ],
+ [
+ "zzi",
+ -12.66008472442627
+ ],
+ [
+ "▁Rover",
+ -12.6603364944458
+ ],
+ [
+ "▁versuchen",
+ -12.6603364944458
+ ],
+ [
+ "▁articulate",
+ -12.660600662231445
+ ],
+ [
+ "▁privé",
+ -12.660614013671875
+ ],
+ [
+ "▁consequent",
+ -12.660663604736328
+ ],
+ [
+ "EAT",
+ -12.660690307617188
+ ],
+ [
+ "▁Marsh",
+ -12.660696983337402
+ ],
+ [
+ "▁teenage",
+ -12.660717964172363
+ ],
+ [
+ "▁Renaissance",
+ -12.660740852355957
+ ],
+ [
+ "▁furnizor",
+ -12.660883903503418
+ ],
+ [
+ "▁Desert",
+ -12.660894393920898
+ ],
+ [
+ "unicipiului",
+ -12.66104793548584
+ ],
+ [
+ "▁ulterior",
+ -12.661065101623535
+ ],
+ [
+ "▁Ebene",
+ -12.661280632019043
+ ],
+ [
+ "▁monkey",
+ -12.661351203918457
+ ],
+ [
+ "▁enclosed",
+ -12.661389350891113
+ ],
+ [
+ "▁profitability",
+ -12.66139030456543
+ ],
+ [
+ "▁Evolution",
+ -12.661628723144531
+ ],
+ [
+ "▁adica",
+ -12.661670684814453
+ ],
+ [
+ "▁Structure",
+ -12.661709785461426
+ ],
+ [
+ "▁primer",
+ -12.661761283874512
+ ],
+ [
+ "▁asigură",
+ -12.662001609802246
+ ],
+ [
+ "▁Manuel",
+ -12.662220001220703
+ ],
+ [
+ "polita",
+ -12.662267684936523
+ ],
+ [
+ "▁Portable",
+ -12.662286758422852
+ ],
+ [
+ "fecți",
+ -12.662413597106934
+ ],
+ [
+ "▁obscure",
+ -12.662424087524414
+ ],
+ [
+ "▁Atlas",
+ -12.662436485290527
+ ],
+ [
+ "fährt",
+ -12.662679672241211
+ ],
+ [
+ "▁clinician",
+ -12.662837982177734
+ ],
+ [
+ "fuhr",
+ -12.66310977935791
+ ],
+ [
+ "▁matériaux",
+ -12.663113594055176
+ ],
+ [
+ "écrire",
+ -12.663142204284668
+ ],
+ [
+ "▁suspicious",
+ -12.6632080078125
+ ],
+ [
+ "pore",
+ -12.663263320922852
+ ],
+ [
+ "▁outdated",
+ -12.663304328918457
+ ],
+ [
+ "▁Mädchen",
+ -12.663328170776367
+ ],
+ [
+ "rcis",
+ -12.663420677185059
+ ],
+ [
+ "nicht",
+ -12.663463592529297
+ ],
+ [
+ "holding",
+ -12.663561820983887
+ ],
+ [
+ "▁heavier",
+ -12.66366195678711
+ ],
+ [
+ "ezimal",
+ -12.663960456848145
+ ],
+ [
+ "▁silicone",
+ -12.66397476196289
+ ],
+ [
+ "punerea",
+ -12.664108276367188
+ ],
+ [
+ "▁begeistert",
+ -12.664237976074219
+ ],
+ [
+ "2004",
+ -12.664283752441406
+ ],
+ [
+ "▁predecessor",
+ -12.664299011230469
+ ],
+ [
+ "▁overlap",
+ -12.664369583129883
+ ],
+ [
+ "▁digging",
+ -12.664376258850098
+ ],
+ [
+ "▁Upgrade",
+ -12.664407730102539
+ ],
+ [
+ "▁interesat",
+ -12.664543151855469
+ ],
+ [
+ "▁spinach",
+ -12.66456127166748
+ ],
+ [
+ "▁politice",
+ -12.664626121520996
+ ],
+ [
+ "activity",
+ -12.664831161499023
+ ],
+ [
+ "▁Rating",
+ -12.66484546661377
+ ],
+ [
+ "▁serrure",
+ -12.664846420288086
+ ],
+ [
+ "▁tânăr",
+ -12.664959907531738
+ ],
+ [
+ "▁WHAT",
+ -12.664970397949219
+ ],
+ [
+ "▁railroad",
+ -12.664989471435547
+ ],
+ [
+ "▁avid",
+ -12.665081024169922
+ ],
+ [
+ "▁Sophie",
+ -12.665084838867188
+ ],
+ [
+ "preferably",
+ -12.665173530578613
+ ],
+ [
+ "▁Fourth",
+ -12.665431022644043
+ ],
+ [
+ "kommenden",
+ -12.665452003479004
+ ],
+ [
+ "QUI",
+ -12.665478706359863
+ ],
+ [
+ "lohn",
+ -12.665505409240723
+ ],
+ [
+ "▁promis",
+ -12.665611267089844
+ ],
+ [
+ "▁shrub",
+ -12.665621757507324
+ ],
+ [
+ "nummer",
+ -12.66579818725586
+ ],
+ [
+ "▁dinosaur",
+ -12.665922164916992
+ ],
+ [
+ "▁Lucky",
+ -12.665937423706055
+ ],
+ [
+ "relates",
+ -12.666038513183594
+ ],
+ [
+ "▁FROM",
+ -12.666049003601074
+ ],
+ [
+ "▁racism",
+ -12.66610336303711
+ ],
+ [
+ "physical",
+ -12.66611385345459
+ ],
+ [
+ "alcoholic",
+ -12.666119575500488
+ ],
+ [
+ "▁reef",
+ -12.666126251220703
+ ],
+ [
+ "▁centru",
+ -12.66618824005127
+ ],
+ [
+ "université",
+ -12.66622257232666
+ ],
+ [
+ "▁visage",
+ -12.666232109069824
+ ],
+ [
+ "ităţile",
+ -12.666253089904785
+ ],
+ [
+ "▁Gent",
+ -12.666345596313477
+ ],
+ [
+ "zugeben",
+ -12.66643238067627
+ ],
+ [
+ "▁paradise",
+ -12.66646957397461
+ ],
+ [
+ "fuel",
+ -12.666505813598633
+ ],
+ [
+ "ografie",
+ -12.666568756103516
+ ],
+ [
+ "▁TIP",
+ -12.666730880737305
+ ],
+ [
+ "schreibung",
+ -12.66683292388916
+ ],
+ [
+ "▁bark",
+ -12.666840553283691
+ ],
+ [
+ "accéder",
+ -12.666895866394043
+ ],
+ [
+ "▁contamination",
+ -12.666937828063965
+ ],
+ [
+ "▁swelling",
+ -12.666950225830078
+ ],
+ [
+ "▁optimistic",
+ -12.666974067687988
+ ],
+ [
+ "▁differential",
+ -12.667015075683594
+ ],
+ [
+ "▁Arad",
+ -12.667030334472656
+ ],
+ [
+ "toxins",
+ -12.667075157165527
+ ],
+ [
+ "▁übernehmen",
+ -12.667091369628906
+ ],
+ [
+ "▁anime",
+ -12.667143821716309
+ ],
+ [
+ "actuel",
+ -12.667462348937988
+ ],
+ [
+ "▁bientôt",
+ -12.667525291442871
+ ],
+ [
+ "▁Patio",
+ -12.66761302947998
+ ],
+ [
+ "▁baisse",
+ -12.667630195617676
+ ],
+ [
+ "▁sprint",
+ -12.66773796081543
+ ],
+ [
+ "▁bilden",
+ -12.66811466217041
+ ],
+ [
+ "VAL",
+ -12.668132781982422
+ ],
+ [
+ "▁réflexion",
+ -12.668220520019531
+ ],
+ [
+ "hopping",
+ -12.668242454528809
+ ],
+ [
+ "genesis",
+ -12.66834545135498
+ ],
+ [
+ "achtet",
+ -12.668435096740723
+ ],
+ [
+ "▁chinois",
+ -12.668525695800781
+ ],
+ [
+ "▁dezvoltat",
+ -12.668795585632324
+ ],
+ [
+ "arguably",
+ -12.66884708404541
+ ],
+ [
+ "▁Protocol",
+ -12.66884708404541
+ ],
+ [
+ "▁Sterling",
+ -12.668862342834473
+ ],
+ [
+ "▁Cave",
+ -12.668975830078125
+ ],
+ [
+ "▁Condo",
+ -12.66921615600586
+ ],
+ [
+ "▁erhöht",
+ -12.669235229492188
+ ],
+ [
+ "typische",
+ -12.669416427612305
+ ],
+ [
+ "merged",
+ -12.669439315795898
+ ],
+ [
+ "▁accumulation",
+ -12.669560432434082
+ ],
+ [
+ "sicherlich",
+ -12.669569969177246
+ ],
+ [
+ "kW",
+ -12.669620513916016
+ ],
+ [
+ "▁schriftlich",
+ -12.669757843017578
+ ],
+ [
+ "▁Vorteile",
+ -12.669918060302734
+ ],
+ [
+ "▁Northeast",
+ -12.669922828674316
+ ],
+ [
+ "frunt",
+ -12.669941902160645
+ ],
+ [
+ "istik",
+ -12.670003890991211
+ ],
+ [
+ "erster",
+ -12.670035362243652
+ ],
+ [
+ "▁Assistance",
+ -12.670150756835938
+ ],
+ [
+ "▁Fantastic",
+ -12.670150756835938
+ ],
+ [
+ "▁bărbat",
+ -12.670150756835938
+ ],
+ [
+ "▁Grinding",
+ -12.670151710510254
+ ],
+ [
+ "▁diffusion",
+ -12.670161247253418
+ ],
+ [
+ "▁vreun",
+ -12.670331954956055
+ ],
+ [
+ "▁Butler",
+ -12.670342445373535
+ ],
+ [
+ "▁Cherry",
+ -12.670352935791016
+ ],
+ [
+ "▁visualization",
+ -12.670540809631348
+ ],
+ [
+ "Paket",
+ -12.670572280883789
+ ],
+ [
+ "blin",
+ -12.670619010925293
+ ],
+ [
+ "▁cadou",
+ -12.670705795288086
+ ],
+ [
+ "▁Celtic",
+ -12.670754432678223
+ ],
+ [
+ "alegerea",
+ -12.670894622802734
+ ],
+ [
+ "▁Dorf",
+ -12.671035766601562
+ ],
+ [
+ "▁Noir",
+ -12.671185493469238
+ ],
+ [
+ "payment",
+ -12.67126750946045
+ ],
+ [
+ "▁Caroline",
+ -12.671334266662598
+ ],
+ [
+ "▁Berry",
+ -12.671359062194824
+ ],
+ [
+ "▁professeur",
+ -12.67147445678711
+ ],
+ [
+ "▁gratuitement",
+ -12.671503067016602
+ ],
+ [
+ "Suntem",
+ -12.671523094177246
+ ],
+ [
+ "IAN",
+ -12.671738624572754
+ ],
+ [
+ "▁fingerprint",
+ -12.671780586242676
+ ],
+ [
+ "▁controversy",
+ -12.671781539916992
+ ],
+ [
+ "▁fled",
+ -12.671875
+ ],
+ [
+ "▁Pokémon",
+ -12.67210865020752
+ ],
+ [
+ "excluding",
+ -12.67211627960205
+ ],
+ [
+ "▁friction",
+ -12.672161102294922
+ ],
+ [
+ "therapie",
+ -12.67225456237793
+ ],
+ [
+ "/7",
+ -12.672398567199707
+ ],
+ [
+ "▁designation",
+ -12.672442436218262
+ ],
+ [
+ "▁Belgia",
+ -12.672704696655273
+ ],
+ [
+ "▁cursuri",
+ -12.672836303710938
+ ],
+ [
+ "model",
+ -12.672840118408203
+ ],
+ [
+ "super",
+ -12.672987937927246
+ ],
+ [
+ "▁réduit",
+ -12.673028945922852
+ ],
+ [
+ "▁implicit",
+ -12.673177719116211
+ ],
+ [
+ "athlon",
+ -12.673227310180664
+ ],
+ [
+ "anniversaire",
+ -12.673416137695312
+ ],
+ [
+ "▁teaspoon",
+ -12.673416137695312
+ ],
+ [
+ "▁corrosion",
+ -12.673418998718262
+ ],
+ [
+ "▁überzeugt",
+ -12.673418998718262
+ ],
+ [
+ "▁flawless",
+ -12.673421859741211
+ ],
+ [
+ "▁vegetation",
+ -12.673477172851562
+ ],
+ [
+ "▁iarna",
+ -12.673507690429688
+ ],
+ [
+ "▁psychologist",
+ -12.673591613769531
+ ],
+ [
+ "hora",
+ -12.673625946044922
+ ],
+ [
+ "gab",
+ -12.67387580871582
+ ],
+ [
+ "▁soothing",
+ -12.674084663391113
+ ],
+ [
+ "▁stew",
+ -12.674141883850098
+ ],
+ [
+ "▁wager",
+ -12.674172401428223
+ ],
+ [
+ "▁tinere",
+ -12.674322128295898
+ ],
+ [
+ "▁baut",
+ -12.674323081970215
+ ],
+ [
+ "ecunoscut",
+ -12.674352645874023
+ ],
+ [
+ "gearbeitet",
+ -12.674422264099121
+ ],
+ [
+ "▁functi",
+ -12.674480438232422
+ ],
+ [
+ "▁dürfte",
+ -12.674724578857422
+ ],
+ [
+ "▁média",
+ -12.674724578857422
+ ],
+ [
+ "▁campanie",
+ -12.67475700378418
+ ],
+ [
+ "▁Distribu",
+ -12.674817085266113
+ ],
+ [
+ "▁mentoring",
+ -12.674959182739258
+ ],
+ [
+ "▁criz",
+ -12.675020217895508
+ ],
+ [
+ "findest",
+ -12.675056457519531
+ ],
+ [
+ "▁Vasile",
+ -12.675058364868164
+ ],
+ [
+ "▁compassionate",
+ -12.675115585327148
+ ],
+ [
+ "▁Tudor",
+ -12.675140380859375
+ ],
+ [
+ "▁flare",
+ -12.675260543823242
+ ],
+ [
+ "intreaga",
+ -12.675283432006836
+ ],
+ [
+ "gaz",
+ -12.6753511428833
+ ],
+ [
+ "▁porcelain",
+ -12.675379753112793
+ ],
+ [
+ "▁expedition",
+ -12.675520896911621
+ ],
+ [
+ "▁Azure",
+ -12.67553997039795
+ ],
+ [
+ "räumen",
+ -12.675549507141113
+ ],
+ [
+ "eiro",
+ -12.675567626953125
+ ],
+ [
+ "variante",
+ -12.675804138183594
+ ],
+ [
+ "▁Lucy",
+ -12.675825119018555
+ ],
+ [
+ "ôle",
+ -12.675909996032715
+ ],
+ [
+ "▁revenir",
+ -12.67602252960205
+ ],
+ [
+ "▁stained",
+ -12.676040649414062
+ ],
+ [
+ "▁falsch",
+ -12.676166534423828
+ ],
+ [
+ "▁incorpor",
+ -12.676166534423828
+ ],
+ [
+ "merkt",
+ -12.676187515258789
+ ],
+ [
+ "▁achten",
+ -12.6762056350708
+ ],
+ [
+ "▁hello",
+ -12.676290512084961
+ ],
+ [
+ "selben",
+ -12.676422119140625
+ ],
+ [
+ "ifty",
+ -12.676525115966797
+ ],
+ [
+ "▁Feier",
+ -12.67653751373291
+ ],
+ [
+ "1.000",
+ -12.676557540893555
+ ],
+ [
+ "▁Patch",
+ -12.676583290100098
+ ],
+ [
+ "peptid",
+ -12.676846504211426
+ ],
+ [
+ "▁recovering",
+ -12.676898956298828
+ ],
+ [
+ "Symptom",
+ -12.677020072937012
+ ],
+ [
+ "▁Auckland",
+ -12.677020072937012
+ ],
+ [
+ "▁retrieve",
+ -12.677328109741211
+ ],
+ [
+ "▁800-",
+ -12.67733097076416
+ ],
+ [
+ "schlagen",
+ -12.677473068237305
+ ],
+ [
+ "▁lourd",
+ -12.677562713623047
+ ],
+ [
+ "▁Purple",
+ -12.67760181427002
+ ],
+ [
+ "▁mittels",
+ -12.677776336669922
+ ],
+ [
+ "▁Düsseldorf",
+ -12.67800521850586
+ ],
+ [
+ "▁getaway",
+ -12.67803955078125
+ ],
+ [
+ "▁Cedar",
+ -12.678061485290527
+ ],
+ [
+ "▁Function",
+ -12.678241729736328
+ ],
+ [
+ "▁bizarre",
+ -12.67833423614502
+ ],
+ [
+ "4.3",
+ -12.67849063873291
+ ],
+ [
+ "▁fundraiser",
+ -12.67866325378418
+ ],
+ [
+ "geared",
+ -12.678780555725098
+ ],
+ [
+ "▁privée",
+ -12.678781509399414
+ ],
+ [
+ "▁Bonjour",
+ -12.67894458770752
+ ],
+ [
+ "Gar",
+ -12.67895793914795
+ ],
+ [
+ "▁Lloyd",
+ -12.678991317749023
+ ],
+ [
+ "▁Reinigung",
+ -12.6790132522583
+ ],
+ [
+ "▁Geno",
+ -12.679155349731445
+ ],
+ [
+ "▁Teilnahme",
+ -12.67919635772705
+ ],
+ [
+ "pian",
+ -12.679362297058105
+ ],
+ [
+ "sammelt",
+ -12.679368019104004
+ ],
+ [
+ "Pad",
+ -12.679755210876465
+ ],
+ [
+ "▁Troy",
+ -12.67976188659668
+ ],
+ [
+ "HG",
+ -12.679943084716797
+ ],
+ [
+ "▁klein",
+ -12.679962158203125
+ ],
+ [
+ "▁lettuce",
+ -12.679978370666504
+ ],
+ [
+ "▁patrimoine",
+ -12.679978370666504
+ ],
+ [
+ "▁cooker",
+ -12.680055618286133
+ ],
+ [
+ "▁accesibil",
+ -12.680137634277344
+ ],
+ [
+ "▁Spray",
+ -12.680201530456543
+ ],
+ [
+ "▁negotiation",
+ -12.68047046661377
+ ],
+ [
+ "▁jewel",
+ -12.680480003356934
+ ],
+ [
+ "▁dynamique",
+ -12.68063735961914
+ ],
+ [
+ "▁plastique",
+ -12.68067741394043
+ ],
+ [
+ "▁Limo",
+ -12.680682182312012
+ ],
+ [
+ "▁Funk",
+ -12.68069076538086
+ ],
+ [
+ "▁omului",
+ -12.680702209472656
+ ],
+ [
+ "title",
+ -12.680768013000488
+ ],
+ [
+ "curved",
+ -12.68082046508789
+ ],
+ [
+ "▁Lemon",
+ -12.680851936340332
+ ],
+ [
+ "förder",
+ -12.680891990661621
+ ],
+ [
+ "▁bewusst",
+ -12.681112289428711
+ ],
+ [
+ "inevitably",
+ -12.681296348571777
+ ],
+ [
+ "▁derivative",
+ -12.681297302246094
+ ],
+ [
+ "2:30",
+ -12.681300163269043
+ ],
+ [
+ "komfort",
+ -12.681305885314941
+ ],
+ [
+ "original",
+ -12.681480407714844
+ ],
+ [
+ "sanct",
+ -12.681540489196777
+ ],
+ [
+ "▁matte",
+ -12.6815767288208
+ ],
+ [
+ "empêche",
+ -12.681628227233887
+ ],
+ [
+ "▁jucător",
+ -12.681634902954102
+ ],
+ [
+ "▁attentive",
+ -12.681640625
+ ],
+ [
+ "▁recunoscut",
+ -12.681674003601074
+ ],
+ [
+ "▁Brush",
+ -12.68167495727539
+ ],
+ [
+ "▁consommateur",
+ -12.68183422088623
+ ],
+ [
+ "érence",
+ -12.682063102722168
+ ],
+ [
+ "typical",
+ -12.682084083557129
+ ],
+ [
+ "strategie",
+ -12.682205200195312
+ ],
+ [
+ "Effekt",
+ -12.682290077209473
+ ],
+ [
+ "▁Alcohol",
+ -12.682292938232422
+ ],
+ [
+ "oji",
+ -12.682333946228027
+ ],
+ [
+ "▁ruler",
+ -12.682357788085938
+ ],
+ [
+ "▁Norwegian",
+ -12.682615280151367
+ ],
+ [
+ "▁PlayStation",
+ -12.682615280151367
+ ],
+ [
+ "▁Hook",
+ -12.682747840881348
+ ],
+ [
+ "▁viewpoint",
+ -12.682759284973145
+ ],
+ [
+ "THER",
+ -12.682841300964355
+ ],
+ [
+ "420",
+ -12.682888984680176
+ ],
+ [
+ "Consequently",
+ -12.68294620513916
+ ],
+ [
+ "▁entschieden",
+ -12.68294620513916
+ ],
+ [
+ "▁Trag",
+ -12.68295669555664
+ ],
+ [
+ "▁Dawn",
+ -12.683003425598145
+ ],
+ [
+ "▁fuss",
+ -12.68301773071289
+ ],
+ [
+ "*****",
+ -12.683040618896484
+ ],
+ [
+ "▁Bullet",
+ -12.683140754699707
+ ],
+ [
+ "CAM",
+ -12.683155059814453
+ ],
+ [
+ "▁wonderfully",
+ -12.683201789855957
+ ],
+ [
+ "▁parlamentar",
+ -12.683263778686523
+ ],
+ [
+ "▁geometric",
+ -12.683307647705078
+ ],
+ [
+ "talement",
+ -12.683321952819824
+ ],
+ [
+ "/2018",
+ -12.683577537536621
+ ],
+ [
+ "▁oversight",
+ -12.684036254882812
+ ],
+ [
+ "kindly",
+ -12.684080123901367
+ ],
+ [
+ "therm",
+ -12.684305191040039
+ ],
+ [
+ "▁treaba",
+ -12.6846342086792
+ ],
+ [
+ "▁Trim",
+ -12.68471908569336
+ ],
+ [
+ "▁intelege",
+ -12.684842109680176
+ ],
+ [
+ "cino",
+ -12.685032844543457
+ ],
+ [
+ "▁straw",
+ -12.68508529663086
+ ],
+ [
+ "Tru",
+ -12.685251235961914
+ ],
+ [
+ "▁Television",
+ -12.68530559539795
+ ],
+ [
+ "Trader",
+ -12.68538761138916
+ ],
+ [
+ "▁Passion",
+ -12.685394287109375
+ ],
+ [
+ "rescu",
+ -12.685622215270996
+ ],
+ [
+ "Nicol",
+ -12.685635566711426
+ ],
+ [
+ "luj",
+ -12.685805320739746
+ ],
+ [
+ "▁mijloace",
+ -12.685921669006348
+ ],
+ [
+ "▁Removal",
+ -12.685922622680664
+ ],
+ [
+ "▁1944",
+ -12.686034202575684
+ ],
+ [
+ "▁shortcut",
+ -12.686159133911133
+ ],
+ [
+ "▁Fett",
+ -12.686258316040039
+ ],
+ [
+ "largement",
+ -12.686371803283691
+ ],
+ [
+ "▁altern",
+ -12.686446189880371
+ ],
+ [
+ "▁cleansing",
+ -12.686562538146973
+ ],
+ [
+ "▁Qatar",
+ -12.686692237854004
+ ],
+ [
+ "▁Ceci",
+ -12.686826705932617
+ ],
+ [
+ "▁weave",
+ -12.686848640441895
+ ],
+ [
+ "schmerz",
+ -12.686878204345703
+ ],
+ [
+ "▁dots",
+ -12.686888694763184
+ ],
+ [
+ "Télécharger",
+ -12.68691635131836
+ ],
+ [
+ "▁Conduct",
+ -12.686944007873535
+ ],
+ [
+ "bekannten",
+ -12.687325477600098
+ ],
+ [
+ "▁lungime",
+ -12.687344551086426
+ ],
+ [
+ "▁Ferrari",
+ -12.687390327453613
+ ],
+ [
+ "▁totusi",
+ -12.687605857849121
+ ],
+ [
+ "▁Anniversary",
+ -12.687911033630371
+ ],
+ [
+ "▁wilderness",
+ -12.687911987304688
+ ],
+ [
+ "▁Christoph",
+ -12.687939643859863
+ ],
+ [
+ "▁Nikon",
+ -12.688112258911133
+ ],
+ [
+ "▁Digi",
+ -12.68818473815918
+ ],
+ [
+ "▁Blumen",
+ -12.688190460205078
+ ],
+ [
+ "▁altul",
+ -12.688249588012695
+ ],
+ [
+ "▁Parish",
+ -12.688321113586426
+ ],
+ [
+ "czy",
+ -12.688393592834473
+ ],
+ [
+ "▁temper",
+ -12.688401222229004
+ ],
+ [
+ "▁Powder",
+ -12.688576698303223
+ ],
+ [
+ "▁Arnold",
+ -12.688577651977539
+ ],
+ [
+ "capacitatea",
+ -12.688687324523926
+ ],
+ [
+ "nderungen",
+ -12.688787460327148
+ ],
+ [
+ "▁utilization",
+ -12.688859939575195
+ ],
+ [
+ "99%",
+ -12.688942909240723
+ ],
+ [
+ "▁Fear",
+ -12.689099311828613
+ ],
+ [
+ "JE",
+ -12.689165115356445
+ ],
+ [
+ "▁Simpson",
+ -12.689239501953125
+ ],
+ [
+ "▁Podcast",
+ -12.68924617767334
+ ],
+ [
+ "▁Cardinal",
+ -12.689290046691895
+ ],
+ [
+ "▁Distribution",
+ -12.689315795898438
+ ],
+ [
+ "▁Drawing",
+ -12.689373970031738
+ ],
+ [
+ "▁tint",
+ -12.689412117004395
+ ],
+ [
+ "▁hran",
+ -12.68945598602295
+ ],
+ [
+ "▁Slide",
+ -12.68960189819336
+ ],
+ [
+ "▁Vertrauen",
+ -12.689654350280762
+ ],
+ [
+ "cloth",
+ -12.68971061706543
+ ],
+ [
+ "▁redirect",
+ -12.689728736877441
+ ],
+ [
+ "126",
+ -12.689842224121094
+ ],
+ [
+ "▁constituie",
+ -12.68985652923584
+ ],
+ [
+ "Mai",
+ -12.690070152282715
+ ],
+ [
+ "▁idol",
+ -12.690088272094727
+ ],
+ [
+ "▁tehnice",
+ -12.690163612365723
+ ],
+ [
+ "dip",
+ -12.690393447875977
+ ],
+ [
+ "▁soldier",
+ -12.690400123596191
+ ],
+ [
+ "▁Ordin",
+ -12.690409660339355
+ ],
+ [
+ "wobe",
+ -12.69050407409668
+ ],
+ [
+ "▁Brent",
+ -12.69058895111084
+ ],
+ [
+ "▁Sudan",
+ -12.690597534179688
+ ],
+ [
+ "6000",
+ -12.690619468688965
+ ],
+ [
+ "turism",
+ -12.690689086914062
+ ],
+ [
+ "▁Rocky",
+ -12.690744400024414
+ ],
+ [
+ "naming",
+ -12.69092082977295
+ ],
+ [
+ "▁entrepreneurial",
+ -12.690925598144531
+ ],
+ [
+ "hearted",
+ -12.690962791442871
+ ],
+ [
+ "ayne",
+ -12.69097900390625
+ ],
+ [
+ "▁hover",
+ -12.691081047058105
+ ],
+ [
+ "▁skull",
+ -12.691279411315918
+ ],
+ [
+ "▁tribal",
+ -12.691407203674316
+ ],
+ [
+ "▁crafting",
+ -12.691543579101562
+ ],
+ [
+ "bewertungen",
+ -12.691569328308105
+ ],
+ [
+ "▁decizii",
+ -12.691625595092773
+ ],
+ [
+ "obwohl",
+ -12.691655158996582
+ ],
+ [
+ "▁compromised",
+ -12.691875457763672
+ ],
+ [
+ "▁quelqu",
+ -12.69195556640625
+ ],
+ [
+ "▁Hilton",
+ -12.692075729370117
+ ],
+ [
+ "▁maturity",
+ -12.692095756530762
+ ],
+ [
+ "gelesen",
+ -12.692100524902344
+ ],
+ [
+ "▁harbor",
+ -12.69210433959961
+ ],
+ [
+ "▁maple",
+ -12.692326545715332
+ ],
+ [
+ "▁développ",
+ -12.6924409866333
+ ],
+ [
+ "▁Nobody",
+ -12.692517280578613
+ ],
+ [
+ "équipement",
+ -12.69255542755127
+ ],
+ [
+ "121",
+ -12.69274616241455
+ ],
+ [
+ "140",
+ -12.692827224731445
+ ],
+ [
+ "▁artistes",
+ -12.692914962768555
+ ],
+ [
+ "▁depune",
+ -12.692941665649414
+ ],
+ [
+ "▁erase",
+ -12.693129539489746
+ ],
+ [
+ "▁erzählt",
+ -12.693197250366211
+ ],
+ [
+ "▁Hyundai",
+ -12.69323444366455
+ ],
+ [
+ "▁impairment",
+ -12.69323444366455
+ ],
+ [
+ "▁conving",
+ -12.693279266357422
+ ],
+ [
+ "chasing",
+ -12.693426132202148
+ ],
+ [
+ "▁Claus",
+ -12.693438529968262
+ ],
+ [
+ "▁adaptée",
+ -12.693687438964844
+ ],
+ [
+ "▁Raz",
+ -12.693740844726562
+ ],
+ [
+ "rugs",
+ -12.693796157836914
+ ],
+ [
+ "▁urme",
+ -12.69387435913086
+ ],
+ [
+ "Nonetheless",
+ -12.693902015686035
+ ],
+ [
+ "▁Cemetery",
+ -12.693902969360352
+ ],
+ [
+ "umps",
+ -12.693906784057617
+ ],
+ [
+ "ACA",
+ -12.694003105163574
+ ],
+ [
+ "▁perioade",
+ -12.694235801696777
+ ],
+ [
+ "▁slogan",
+ -12.694263458251953
+ ],
+ [
+ "▁downward",
+ -12.694441795349121
+ ],
+ [
+ "eidig",
+ -12.694446563720703
+ ],
+ [
+ "RAC",
+ -12.69444751739502
+ ],
+ [
+ "▁inaugur",
+ -12.694496154785156
+ ],
+ [
+ "се",
+ -12.694588661193848
+ ],
+ [
+ "▁înțeleg",
+ -12.694608688354492
+ ],
+ [
+ "▁hopeful",
+ -12.694635391235352
+ ],
+ [
+ "▁customization",
+ -12.6946439743042
+ ],
+ [
+ "▁prisoners",
+ -12.694708824157715
+ ],
+ [
+ "▁Rau",
+ -12.695270538330078
+ ],
+ [
+ "▁Pitt",
+ -12.695389747619629
+ ],
+ [
+ "ături",
+ -12.695542335510254
+ ],
+ [
+ "▁metabolic",
+ -12.695842742919922
+ ],
+ [
+ "▁Zach",
+ -12.695868492126465
+ ],
+ [
+ "▁umfassende",
+ -12.695914268493652
+ ],
+ [
+ "▁révél",
+ -12.695950508117676
+ ],
+ [
+ "131",
+ -12.696052551269531
+ ],
+ [
+ "ismului",
+ -12.696062088012695
+ ],
+ [
+ "▁Sac",
+ -12.696076393127441
+ ],
+ [
+ "efficacité",
+ -12.69624137878418
+ ],
+ [
+ "cruci",
+ -12.69625473022461
+ ],
+ [
+ "bisschen",
+ -12.69632339477539
+ ],
+ [
+ "▁Oster",
+ -12.696324348449707
+ ],
+ [
+ "lowered",
+ -12.6964693069458
+ ],
+ [
+ "▁Ausland",
+ -12.69674015045166
+ ],
+ [
+ "▁Pub",
+ -12.696794509887695
+ ],
+ [
+ "▁Marseille",
+ -12.696925163269043
+ ],
+ [
+ "▁Charter",
+ -12.696959495544434
+ ],
+ [
+ "howcasing",
+ -12.697010040283203
+ ],
+ [
+ "risti",
+ -12.6971435546875
+ ],
+ [
+ "▁thermostat",
+ -12.697151184082031
+ ],
+ [
+ "▁Clin",
+ -12.697233200073242
+ ],
+ [
+ "▁entsteht",
+ -12.697246551513672
+ ],
+ [
+ "Choosing",
+ -12.697248458862305
+ ],
+ [
+ "▁Schmerz",
+ -12.697284698486328
+ ],
+ [
+ "▁Till",
+ -12.697307586669922
+ ],
+ [
+ "▁Polo",
+ -12.697399139404297
+ ],
+ [
+ "▁proceduri",
+ -12.697402000427246
+ ],
+ [
+ "▁Believe",
+ -12.697444915771484
+ ],
+ [
+ "▁playful",
+ -12.697514533996582
+ ],
+ [
+ "▁verändert",
+ -12.697588920593262
+ ],
+ [
+ "▁pairing",
+ -12.697654724121094
+ ],
+ [
+ "MAG",
+ -12.69784927368164
+ ],
+ [
+ "leiste",
+ -12.69788932800293
+ ],
+ [
+ "▁testimonial",
+ -12.697916030883789
+ ],
+ [
+ "▁Economy",
+ -12.697916984558105
+ ],
+ [
+ "▁Wechsel",
+ -12.697918891906738
+ ],
+ [
+ "wirkung",
+ -12.69801139831543
+ ],
+ [
+ "▁exceeded",
+ -12.698030471801758
+ ],
+ [
+ "South",
+ -12.698067665100098
+ ],
+ [
+ "create",
+ -12.698221206665039
+ ],
+ [
+ "▁davantage",
+ -12.698270797729492
+ ],
+ [
+ "Log",
+ -12.69831657409668
+ ],
+ [
+ "▁irregular",
+ -12.698587417602539
+ ],
+ [
+ "VB",
+ -12.698691368103027
+ ],
+ [
+ "▁Rö",
+ -12.698741912841797
+ ],
+ [
+ "▁intreb",
+ -12.698881149291992
+ ],
+ [
+ "▁penser",
+ -12.698920249938965
+ ],
+ [
+ "▁déclaré",
+ -12.698923110961914
+ ],
+ [
+ "▁Tommy",
+ -12.699026107788086
+ ],
+ [
+ "2,500",
+ -12.699163436889648
+ ],
+ [
+ "▁Uganda",
+ -12.699260711669922
+ ],
+ [
+ "contacting",
+ -12.699445724487305
+ ],
+ [
+ "▁apreciat",
+ -12.699485778808594
+ ],
+ [
+ "▁beginnen",
+ -12.6995210647583
+ ],
+ [
+ "▁Gain",
+ -12.699580192565918
+ ],
+ [
+ "Office",
+ -12.69969654083252
+ ],
+ [
+ "ermittlung",
+ -12.699710845947266
+ ],
+ [
+ "▁Admission",
+ -12.699727058410645
+ ],
+ [
+ "▁Earl",
+ -12.6997652053833
+ ],
+ [
+ "▁Aviation",
+ -12.699833869934082
+ ],
+ [
+ "▁apologize",
+ -12.699929237365723
+ ],
+ [
+ "▁enclosure",
+ -12.699929237365723
+ ],
+ [
+ "▁Lack",
+ -12.69998836517334
+ ],
+ [
+ "wife",
+ -12.699995994567871
+ ],
+ [
+ "▁rotating",
+ -12.700016975402832
+ ],
+ [
+ "▁hergestellt",
+ -12.700020790100098
+ ],
+ [
+ "▁repository",
+ -12.70002269744873
+ ],
+ [
+ "TK",
+ -12.700149536132812
+ ],
+ [
+ "▁lectur",
+ -12.700190544128418
+ ],
+ [
+ "▁reflex",
+ -12.700286865234375
+ ],
+ [
+ "▁Harmon",
+ -12.700401306152344
+ ],
+ [
+ "▁vrem",
+ -12.700479507446289
+ ],
+ [
+ "▁Strange",
+ -12.70055103302002
+ ],
+ [
+ "▁champagne",
+ -12.700615882873535
+ ],
+ [
+ "▁oscil",
+ -12.700647354125977
+ ],
+ [
+ "sensitive",
+ -12.700677871704102
+ ],
+ [
+ "▁Sheriff",
+ -12.700841903686523
+ ],
+ [
+ "PRES",
+ -12.700956344604492
+ ],
+ [
+ "▁vow",
+ -12.70123291015625
+ ],
+ [
+ "▁dioxide",
+ -12.701276779174805
+ ],
+ [
+ "ен",
+ -12.701374053955078
+ ],
+ [
+ "▁corpului",
+ -12.701376914978027
+ ],
+ [
+ "▁prevăzut",
+ -12.70160961151123
+ ],
+ [
+ "India",
+ -12.701827049255371
+ ],
+ [
+ "hausse",
+ -12.70189094543457
+ ],
+ [
+ "▁clienți",
+ -12.701957702636719
+ ],
+ [
+ "▁entour",
+ -12.70202350616455
+ ],
+ [
+ "▁Sharp",
+ -12.70209789276123
+ ],
+ [
+ "▁teatru",
+ -12.702285766601562
+ ],
+ [
+ "▁Grow",
+ -12.702327728271484
+ ],
+ [
+ "▁caravan",
+ -12.70234203338623
+ ],
+ [
+ "▁sieben",
+ -12.702420234680176
+ ],
+ [
+ "▁cunosc",
+ -12.702502250671387
+ ],
+ [
+ "Bereichen",
+ -12.702527046203613
+ ],
+ [
+ "▁Benutzer",
+ -12.702619552612305
+ ],
+ [
+ "▁Ethiopia",
+ -12.702619552612305
+ ],
+ [
+ "▁Physics",
+ -12.702619552612305
+ ],
+ [
+ "preserving",
+ -12.70263385772705
+ ],
+ [
+ "ал",
+ -12.702712059020996
+ ],
+ [
+ "▁aerial",
+ -12.70272159576416
+ ],
+ [
+ "▁nouvel",
+ -12.702741622924805
+ ],
+ [
+ "▁stamped",
+ -12.702954292297363
+ ],
+ [
+ "▁inaugural",
+ -12.702970504760742
+ ],
+ [
+ "▁medicinal",
+ -12.702999114990234
+ ],
+ [
+ "Quite",
+ -12.703028678894043
+ ],
+ [
+ "accumulated",
+ -12.703165054321289
+ ],
+ [
+ "register",
+ -12.703271865844727
+ ],
+ [
+ "▁Falcon",
+ -12.70327377319336
+ ],
+ [
+ "▁boiling",
+ -12.703301429748535
+ ],
+ [
+ "▁advertised",
+ -12.703339576721191
+ ],
+ [
+ "collect",
+ -12.703362464904785
+ ],
+ [
+ "albeit",
+ -12.703418731689453
+ ],
+ [
+ "▁Organis",
+ -12.703473091125488
+ ],
+ [
+ "luate",
+ -12.703536033630371
+ ],
+ [
+ "▁préféré",
+ -12.70369815826416
+ ],
+ [
+ "▁frumoasa",
+ -12.703968048095703
+ ],
+ [
+ "▁truc",
+ -12.704092979431152
+ ],
+ [
+ "▁Fä",
+ -12.704154968261719
+ ],
+ [
+ "▁dome",
+ -12.704180717468262
+ ],
+ [
+ "Mobile",
+ -12.704191207885742
+ ],
+ [
+ "▁redeem",
+ -12.704198837280273
+ ],
+ [
+ "IONS",
+ -12.70422077178955
+ ],
+ [
+ "▁țări",
+ -12.704235076904297
+ ],
+ [
+ "▁singular",
+ -12.704385757446289
+ ],
+ [
+ "▁livestock",
+ -12.704425811767578
+ ],
+ [
+ "▁démont",
+ -12.704427719116211
+ ],
+ [
+ "clés",
+ -12.704527854919434
+ ],
+ [
+ "music",
+ -12.704561233520508
+ ],
+ [
+ "▁explicat",
+ -12.704602241516113
+ ],
+ [
+ "▁Fellowship",
+ -12.704703330993652
+ ],
+ [
+ "▁electrode",
+ -12.704760551452637
+ ],
+ [
+ "129",
+ -12.704977035522461
+ ],
+ [
+ "▁Rescue",
+ -12.704983711242676
+ ],
+ [
+ "▁Rocket",
+ -12.705159187316895
+ ],
+ [
+ "OSE",
+ -12.705301284790039
+ ],
+ [
+ "▁Sacramento",
+ -12.705317497253418
+ ],
+ [
+ "▁Haiti",
+ -12.705357551574707
+ ],
+ [
+ "▁Erwachsene",
+ -12.705390930175781
+ ],
+ [
+ "▁Terminal",
+ -12.70541000366211
+ ],
+ [
+ "URI",
+ -12.705453872680664
+ ],
+ [
+ "▁Rural",
+ -12.70549201965332
+ ],
+ [
+ "▁achizitiona",
+ -12.70552921295166
+ ],
+ [
+ "▁identifiable",
+ -12.705655097961426
+ ],
+ [
+ "▁gekauft",
+ -12.705659866333008
+ ],
+ [
+ "▁improper",
+ -12.705673217773438
+ ],
+ [
+ "lashes",
+ -12.705751419067383
+ ],
+ [
+ "vorbim",
+ -12.705751419067383
+ ],
+ [
+ "▁hinder",
+ -12.705862045288086
+ ],
+ [
+ "▁Grenz",
+ -12.705878257751465
+ ],
+ [
+ "Nav",
+ -12.705955505371094
+ ],
+ [
+ "alimentation",
+ -12.705972671508789
+ ],
+ [
+ "▁Cottage",
+ -12.7059965133667
+ ],
+ [
+ "▁nötig",
+ -12.706197738647461
+ ],
+ [
+ "▁cuprinde",
+ -12.70622444152832
+ ],
+ [
+ "session",
+ -12.706256866455078
+ ],
+ [
+ "▁Separat",
+ -12.70634651184082
+ ],
+ [
+ "▁besuchen",
+ -12.706672668457031
+ ],
+ [
+ "▁noodles",
+ -12.706684112548828
+ ],
+ [
+ "▁ballet",
+ -12.706696510314941
+ ],
+ [
+ "WG",
+ -12.706731796264648
+ ],
+ [
+ "▁Duty",
+ -12.706871032714844
+ ],
+ [
+ "▁porc",
+ -12.706944465637207
+ ],
+ [
+ "▁booster",
+ -12.70698356628418
+ ],
+ [
+ "galerie",
+ -12.707056045532227
+ ],
+ [
+ "▁Lance",
+ -12.707119941711426
+ ],
+ [
+ "▁déplac",
+ -12.707178115844727
+ ],
+ [
+ "▁rugby",
+ -12.707240104675293
+ ],
+ [
+ "▁upholstery",
+ -12.707345962524414
+ ],
+ [
+ "▁bustl",
+ -12.70736312866211
+ ],
+ [
+ "▁Dealer",
+ -12.70740032196045
+ ],
+ [
+ "▁genome",
+ -12.707414627075195
+ ],
+ [
+ "▁citizenship",
+ -12.707466125488281
+ ],
+ [
+ "rora",
+ -12.707515716552734
+ ],
+ [
+ "ARK",
+ -12.707776069641113
+ ],
+ [
+ "▁Semi",
+ -12.707820892333984
+ ],
+ [
+ "▁Improvement",
+ -12.707892417907715
+ ],
+ [
+ "▁negru",
+ -12.708142280578613
+ ],
+ [
+ "▁Bruxelles",
+ -12.70836067199707
+ ],
+ [
+ "flüge",
+ -12.70837688446045
+ ],
+ [
+ "▁Technique",
+ -12.708392143249512
+ ],
+ [
+ "▁Obst",
+ -12.708413124084473
+ ],
+ [
+ "2020",
+ -12.708560943603516
+ ],
+ [
+ "▁gek",
+ -12.708593368530273
+ ],
+ [
+ "▁drepturi",
+ -12.708600997924805
+ ],
+ [
+ "▁Logan",
+ -12.708605766296387
+ ],
+ [
+ "gelöst",
+ -12.70863151550293
+ ],
+ [
+ "▁grandparents",
+ -12.708702087402344
+ ],
+ [
+ "phin",
+ -12.708950996398926
+ ],
+ [
+ "▁dwell",
+ -12.709037780761719
+ ],
+ [
+ "▁Nobel",
+ -12.709151268005371
+ ],
+ [
+ "dial",
+ -12.70927906036377
+ ],
+ [
+ "▁spontan",
+ -12.709344863891602
+ ],
+ [
+ "advancing",
+ -12.70937728881836
+ ],
+ [
+ "starring",
+ -12.70947551727295
+ ],
+ [
+ "▁astea",
+ -12.709498405456543
+ ],
+ [
+ "igueur",
+ -12.709638595581055
+ ],
+ [
+ "▁Ancient",
+ -12.709700584411621
+ ],
+ [
+ "filter",
+ -12.70971965789795
+ ],
+ [
+ "Doar",
+ -12.709758758544922
+ ],
+ [
+ "▁Workers",
+ -12.709759712219238
+ ],
+ [
+ "Certainly",
+ -12.709906578063965
+ ],
+ [
+ "▁commencé",
+ -12.709914207458496
+ ],
+ [
+ "▁zipper",
+ -12.710001945495605
+ ],
+ [
+ "▁Selection",
+ -12.710070610046387
+ ],
+ [
+ "▁succ",
+ -12.710280418395996
+ ],
+ [
+ "headed",
+ -12.710345268249512
+ ],
+ [
+ "RIA",
+ -12.710350036621094
+ ],
+ [
+ "▁papa",
+ -12.710366249084473
+ ],
+ [
+ "▁profesionale",
+ -12.710394859313965
+ ],
+ [
+ "▁Zeichen",
+ -12.710402488708496
+ ],
+ [
+ "▁artisans",
+ -12.710489273071289
+ ],
+ [
+ "▁Geist",
+ -12.710585594177246
+ ],
+ [
+ "practic",
+ -12.710741996765137
+ ],
+ [
+ "▁ministrul",
+ -12.71076488494873
+ ],
+ [
+ "viens",
+ -12.710912704467773
+ ],
+ [
+ "prezintă",
+ -12.710919380187988
+ ],
+ [
+ "Integrated",
+ -12.710981369018555
+ ],
+ [
+ "▁rooftop",
+ -12.710989952087402
+ ],
+ [
+ "▁successor",
+ -12.710991859436035
+ ],
+ [
+ "OTO",
+ -12.711012840270996
+ ],
+ [
+ "liés",
+ -12.711027145385742
+ ],
+ [
+ "▁Diver",
+ -12.71121597290039
+ ],
+ [
+ "Specifically",
+ -12.711297988891602
+ ],
+ [
+ "▁calibr",
+ -12.711301803588867
+ ],
+ [
+ "KK",
+ -12.711341857910156
+ ],
+ [
+ "▁défense",
+ -12.711414337158203
+ ],
+ [
+ "▁english",
+ -12.711414337158203
+ ],
+ [
+ "verbrauch",
+ -12.711418151855469
+ ],
+ [
+ "▁attire",
+ -12.711433410644531
+ ],
+ [
+ "▁Recipe",
+ -12.711441040039062
+ ],
+ [
+ "équilibre",
+ -12.711457252502441
+ ],
+ [
+ "accumul",
+ -12.71157169342041
+ ],
+ [
+ "▁financement",
+ -12.71169662475586
+ ],
+ [
+ "rij",
+ -12.711962699890137
+ ],
+ [
+ "▁prince",
+ -12.711999893188477
+ ],
+ [
+ "▁préparer",
+ -12.7120361328125
+ ],
+ [
+ "surviving",
+ -12.71211051940918
+ ],
+ [
+ "operation",
+ -12.712233543395996
+ ],
+ [
+ "▁judet",
+ -12.71242904663086
+ ],
+ [
+ "▁Verantwortung",
+ -12.712433815002441
+ ],
+ [
+ "▁Vinyl",
+ -12.712536811828613
+ ],
+ [
+ "DEN",
+ -12.712584495544434
+ ],
+ [
+ "▁Tail",
+ -12.712589263916016
+ ],
+ [
+ "yearly",
+ -12.712590217590332
+ ],
+ [
+ "▁comisi",
+ -12.712613105773926
+ ],
+ [
+ "lava",
+ -12.71261978149414
+ ],
+ [
+ "▁succession",
+ -12.71264934539795
+ ],
+ [
+ "▁Whisk",
+ -12.713030815124512
+ ],
+ [
+ "▁precizat",
+ -12.713096618652344
+ ],
+ [
+ "▁unmittelbar",
+ -12.713117599487305
+ ],
+ [
+ "ICH",
+ -12.713139533996582
+ ],
+ [
+ "▁atteint",
+ -12.713199615478516
+ ],
+ [
+ "▁hometown",
+ -12.713268280029297
+ ],
+ [
+ "▁Zip",
+ -12.71328353881836
+ ],
+ [
+ "▁Weekly",
+ -12.71336841583252
+ ],
+ [
+ "▁crashes",
+ -12.713401794433594
+ ],
+ [
+ "▁Turbo",
+ -12.713421821594238
+ ],
+ [
+ "▁susține",
+ -12.713468551635742
+ ],
+ [
+ "▁Venus",
+ -12.713587760925293
+ ],
+ [
+ "▁finalement",
+ -12.713595390319824
+ ],
+ [
+ "rewarded",
+ -12.713693618774414
+ ],
+ [
+ "▁principau",
+ -12.713899612426758
+ ],
+ [
+ "▁régional",
+ -12.713979721069336
+ ],
+ [
+ "▁1958",
+ -12.714178085327148
+ ],
+ [
+ "▁Musical",
+ -12.714189529418945
+ ],
+ [
+ "▁stylist",
+ -12.714251518249512
+ ],
+ [
+ "cetate",
+ -12.714282035827637
+ ],
+ [
+ "gorge",
+ -12.71433162689209
+ ],
+ [
+ "▁espresso",
+ -12.714493751525879
+ ],
+ [
+ "überall",
+ -12.714576721191406
+ ],
+ [
+ "▁NHL",
+ -12.714593887329102
+ ],
+ [
+ "▁Dock",
+ -12.71472454071045
+ ],
+ [
+ "▁mosquito",
+ -12.71481704711914
+ ],
+ [
+ "▁forthcoming",
+ -12.714852333068848
+ ],
+ [
+ "▁Visitors",
+ -12.714881896972656
+ ],
+ [
+ "kro",
+ -12.714882850646973
+ ],
+ [
+ "_______",
+ -12.715048789978027
+ ],
+ [
+ "▁STEM",
+ -12.715105056762695
+ ],
+ [
+ "9.5",
+ -12.715141296386719
+ ],
+ [
+ "accompagne",
+ -12.715177536010742
+ ],
+ [
+ "▁Trick",
+ -12.715202331542969
+ ],
+ [
+ "▁endorsement",
+ -12.715400695800781
+ ],
+ [
+ "▁amplifier",
+ -12.715498924255371
+ ],
+ [
+ "▁malicious",
+ -12.715499877929688
+ ],
+ [
+ "▁roam",
+ -12.71552848815918
+ ],
+ [
+ "▁kennt",
+ -12.715635299682617
+ ],
+ [
+ "Connor",
+ -12.715690612792969
+ ],
+ [
+ "▁dysfunction",
+ -12.715828895568848
+ ],
+ [
+ "▁zuverlässig",
+ -12.715840339660645
+ ],
+ [
+ "▁corpul",
+ -12.71595573425293
+ ],
+ [
+ "▁boule",
+ -12.715967178344727
+ ],
+ [
+ "otti",
+ -12.715991973876953
+ ],
+ [
+ "440",
+ -12.716050148010254
+ ],
+ [
+ "▁mimic",
+ -12.716056823730469
+ ],
+ [
+ "farben",
+ -12.716129302978516
+ ],
+ [
+ "▁Wagner",
+ -12.716214179992676
+ ],
+ [
+ "Kom",
+ -12.7162504196167
+ ],
+ [
+ "▁miteinander",
+ -12.716269493103027
+ ],
+ [
+ "▁String",
+ -12.716296195983887
+ ],
+ [
+ "▁Ellis",
+ -12.716313362121582
+ ],
+ [
+ "▁Perth",
+ -12.716337203979492
+ ],
+ [
+ "▁temperatura",
+ -12.716381072998047
+ ],
+ [
+ "umbling",
+ -12.716397285461426
+ ],
+ [
+ "▁Medizin",
+ -12.716554641723633
+ ],
+ [
+ "▁KY",
+ -12.71660327911377
+ ],
+ [
+ "apei",
+ -12.716642379760742
+ ],
+ [
+ "counter",
+ -12.716647148132324
+ ],
+ [
+ "strich",
+ -12.71665096282959
+ ],
+ [
+ "▁Între",
+ -12.716652870178223
+ ],
+ [
+ "▁Cliff",
+ -12.716785430908203
+ ],
+ [
+ "▁foreclosure",
+ -12.716864585876465
+ ],
+ [
+ "................",
+ -12.716878890991211
+ ],
+ [
+ "Clearly",
+ -12.717028617858887
+ ],
+ [
+ "AJ",
+ -12.717057228088379
+ ],
+ [
+ "ndro",
+ -12.717180252075195
+ ],
+ [
+ "▁Arsenal",
+ -12.717206001281738
+ ],
+ [
+ "▁Recherche",
+ -12.717216491699219
+ ],
+ [
+ "Guests",
+ -12.717225074768066
+ ],
+ [
+ "▁besucht",
+ -12.717242240905762
+ ],
+ [
+ "wissen",
+ -12.717266082763672
+ ],
+ [
+ "fekt",
+ -12.717414855957031
+ ],
+ [
+ "hottest",
+ -12.717414855957031
+ ],
+ [
+ "▁Tomorrow",
+ -12.717547416687012
+ ],
+ [
+ "▁Signature",
+ -12.717557907104492
+ ],
+ [
+ "127",
+ -12.717583656311035
+ ],
+ [
+ "▁competence",
+ -12.71766471862793
+ ],
+ [
+ "Einige",
+ -12.717686653137207
+ ],
+ [
+ "patented",
+ -12.71782112121582
+ ],
+ [
+ "▁Exhibition",
+ -12.717889785766602
+ ],
+ [
+ "▁verbessern",
+ -12.717889785766602
+ ],
+ [
+ "▁Garcia",
+ -12.718043327331543
+ ],
+ [
+ "▁inquire",
+ -12.718278884887695
+ ],
+ [
+ "coping",
+ -12.718353271484375
+ ],
+ [
+ "▁linguri",
+ -12.71842098236084
+ ],
+ [
+ "▁trivia",
+ -12.718433380126953
+ ],
+ [
+ "▁începutul",
+ -12.718489646911621
+ ],
+ [
+ "▁parteneriat",
+ -12.7186279296875
+ ],
+ [
+ "tagen",
+ -12.718636512756348
+ ],
+ [
+ "▁engagé",
+ -12.718916893005371
+ ],
+ [
+ "▁chalk",
+ -12.718944549560547
+ ],
+ [
+ "▁fashionable",
+ -12.719416618347168
+ ],
+ [
+ "0.8",
+ -12.719635009765625
+ ],
+ [
+ "▁sticker",
+ -12.719751358032227
+ ],
+ [
+ "▁desperately",
+ -12.719765663146973
+ ],
+ [
+ "höhe",
+ -12.719903945922852
+ ],
+ [
+ "▁fericire",
+ -12.71994400024414
+ ],
+ [
+ "évaluation",
+ -12.719948768615723
+ ],
+ [
+ "▁Divide",
+ -12.719959259033203
+ ],
+ [
+ "▁indulge",
+ -12.719979286193848
+ ],
+ [
+ "fett",
+ -12.720014572143555
+ ],
+ [
+ "▁communal",
+ -12.72017765045166
+ ],
+ [
+ "▁mindful",
+ -12.720187187194824
+ ],
+ [
+ "dauert",
+ -12.720192909240723
+ ],
+ [
+ "▁veille",
+ -12.720263481140137
+ ],
+ [
+ "▁vér",
+ -12.720330238342285
+ ],
+ [
+ "▁Baseball",
+ -12.720373153686523
+ ],
+ [
+ "▁succeeded",
+ -12.720418930053711
+ ],
+ [
+ "▁Terrasse",
+ -12.720420837402344
+ ],
+ [
+ "irgend",
+ -12.720500946044922
+ ],
+ [
+ "▁Munich",
+ -12.720556259155273
+ ],
+ [
+ "weisung",
+ -12.72067642211914
+ ],
+ [
+ "metre",
+ -12.720916748046875
+ ],
+ [
+ "▁Raymond",
+ -12.721015930175781
+ ],
+ [
+ "▁chute",
+ -12.72102165222168
+ ],
+ [
+ "▁Accounting",
+ -12.721075057983398
+ ],
+ [
+ "▁pantry",
+ -12.721122741699219
+ ],
+ [
+ "▁underwater",
+ -12.721181869506836
+ ],
+ [
+ "ARI",
+ -12.721222877502441
+ ],
+ [
+ "lowed",
+ -12.721245765686035
+ ],
+ [
+ "numbered",
+ -12.721430778503418
+ ],
+ [
+ "REN",
+ -12.72148609161377
+ ],
+ [
+ "▁industriel",
+ -12.721489906311035
+ ],
+ [
+ "wäh",
+ -12.721531867980957
+ ],
+ [
+ "kenntnis",
+ -12.721631050109863
+ ],
+ [
+ "▁govern",
+ -12.721635818481445
+ ],
+ [
+ "strained",
+ -12.721661567687988
+ ],
+ [
+ "▁rythme",
+ -12.721689224243164
+ ],
+ [
+ "ин",
+ -12.72169303894043
+ ],
+ [
+ "▁burner",
+ -12.721723556518555
+ ],
+ [
+ "▁zählt",
+ -12.721790313720703
+ ],
+ [
+ "▁verte",
+ -12.721883773803711
+ ],
+ [
+ "▁Catalog",
+ -12.721896171569824
+ ],
+ [
+ "▁Bruno",
+ -12.721988677978516
+ ],
+ [
+ "0.7",
+ -12.721997261047363
+ ],
+ [
+ "▁litig",
+ -12.72207260131836
+ ],
+ [
+ "▁greet",
+ -12.722129821777344
+ ],
+ [
+ "▁stool",
+ -12.722393035888672
+ ],
+ [
+ "gression",
+ -12.722457885742188
+ ],
+ [
+ "▁Klassen",
+ -12.722491264343262
+ ],
+ [
+ "▁neon",
+ -12.722661018371582
+ ],
+ [
+ "▁Tall",
+ -12.722734451293945
+ ],
+ [
+ "▁satin",
+ -12.722895622253418
+ ],
+ [
+ "▁Bend",
+ -12.722915649414062
+ ],
+ [
+ "▁soluţi",
+ -12.723077774047852
+ ],
+ [
+ "▁styl",
+ -12.723196983337402
+ ],
+ [
+ "▁Siri",
+ -12.723358154296875
+ ],
+ [
+ "▁Sanders",
+ -12.723464012145996
+ ],
+ [
+ "▁spike",
+ -12.723499298095703
+ ],
+ [
+ "pinion",
+ -12.723854064941406
+ ],
+ [
+ "▁purta",
+ -12.724122047424316
+ ],
+ [
+ "CARE",
+ -12.724224090576172
+ ],
+ [
+ "▁creştere",
+ -12.724311828613281
+ ],
+ [
+ "▁fry",
+ -12.724374771118164
+ ],
+ [
+ "▁Schweizer",
+ -12.724400520324707
+ ],
+ [
+ "durchschnittlich",
+ -12.724411010742188
+ ],
+ [
+ "celaşi",
+ -12.724446296691895
+ ],
+ [
+ "▁deceased",
+ -12.724474906921387
+ ],
+ [
+ "▁Nerv",
+ -12.724668502807617
+ ],
+ [
+ "2-2",
+ -12.7247314453125
+ ],
+ [
+ "▁Stahl",
+ -12.724753379821777
+ ],
+ [
+ "▁workload",
+ -12.724834442138672
+ ],
+ [
+ "erhielt",
+ -12.724984169006348
+ ],
+ [
+ "▁hypothesis",
+ -12.725103378295898
+ ],
+ [
+ "bib",
+ -12.725110054016113
+ ],
+ [
+ "▁ţară",
+ -12.725116729736328
+ ],
+ [
+ "vaut",
+ -12.725122451782227
+ ],
+ [
+ "prehensi",
+ -12.725184440612793
+ ],
+ [
+ "▁Offering",
+ -12.725188255310059
+ ],
+ [
+ "▁dislike",
+ -12.725252151489258
+ ],
+ [
+ "▁firewall",
+ -12.725252151489258
+ ],
+ [
+ "mania",
+ -12.725255966186523
+ ],
+ [
+ "195",
+ -12.725278854370117
+ ],
+ [
+ "▁Champ",
+ -12.725324630737305
+ ],
+ [
+ "▁philosophical",
+ -12.725343704223633
+ ],
+ [
+ "länge",
+ -12.72553539276123
+ ],
+ [
+ "advisable",
+ -12.725785255432129
+ ],
+ [
+ "negotiating",
+ -12.725785255432129
+ ],
+ [
+ "Providing",
+ -12.725791931152344
+ ],
+ [
+ "▁1959",
+ -12.725801467895508
+ ],
+ [
+ "▁spyware",
+ -12.725831031799316
+ ],
+ [
+ "sharing",
+ -12.725837707519531
+ ],
+ [
+ "▁prévoi",
+ -12.725905418395996
+ ],
+ [
+ "▁jaune",
+ -12.7260103225708
+ ],
+ [
+ "schoss",
+ -12.726028442382812
+ ],
+ [
+ "▁obține",
+ -12.726129531860352
+ ],
+ [
+ "▁attraktiv",
+ -12.726489067077637
+ ],
+ [
+ "gemeinschaft",
+ -12.7265043258667
+ ],
+ [
+ "BV",
+ -12.726505279541016
+ ],
+ [
+ "Top",
+ -12.726617813110352
+ ],
+ [
+ "▁Sharon",
+ -12.726625442504883
+ ],
+ [
+ "bok",
+ -12.726675033569336
+ ],
+ [
+ "▁résist",
+ -12.726811408996582
+ ],
+ [
+ "Napoca",
+ -12.726822853088379
+ ],
+ [
+ "▁Uncategorized",
+ -12.726898193359375
+ ],
+ [
+ "▁trustee",
+ -12.726936340332031
+ ],
+ [
+ "▁remise",
+ -12.727025985717773
+ ],
+ [
+ "▁aştept",
+ -12.727165222167969
+ ],
+ [
+ "▁allergic",
+ -12.727206230163574
+ ],
+ [
+ "èvre",
+ -12.727211952209473
+ ],
+ [
+ "LAR",
+ -12.72734546661377
+ ],
+ [
+ "1.9",
+ -12.727497100830078
+ ],
+ [
+ "▁outbreak",
+ -12.727520942687988
+ ],
+ [
+ "▁trocken",
+ -12.727568626403809
+ ],
+ [
+ "▁laughter",
+ -12.727724075317383
+ ],
+ [
+ "▁Attend",
+ -12.727785110473633
+ ],
+ [
+ "jung",
+ -12.727822303771973
+ ],
+ [
+ "racking",
+ -12.727934837341309
+ ],
+ [
+ "ORS",
+ -12.728178024291992
+ ],
+ [
+ "▁rasp",
+ -12.728527069091797
+ ],
+ [
+ "VF",
+ -12.728551864624023
+ ],
+ [
+ "▁Tamil",
+ -12.72860050201416
+ ],
+ [
+ "124",
+ -12.728602409362793
+ ],
+ [
+ "▁Fiber",
+ -12.728714942932129
+ ],
+ [
+ "▁launches",
+ -12.728755950927734
+ ],
+ [
+ "Post",
+ -12.728777885437012
+ ],
+ [
+ "▁bucks",
+ -12.729072570800781
+ ],
+ [
+ "▁Nicholas",
+ -12.72923755645752
+ ],
+ [
+ "▁cărți",
+ -12.729255676269531
+ ],
+ [
+ "emper",
+ -12.729681968688965
+ ],
+ [
+ "Point",
+ -12.729689598083496
+ ],
+ [
+ "fraction",
+ -12.729753494262695
+ ],
+ [
+ "▁BIG",
+ -12.729804992675781
+ ],
+ [
+ "▁lancer",
+ -12.729829788208008
+ ],
+ [
+ "EVER",
+ -12.72997760772705
+ ],
+ [
+ "trend",
+ -12.73000431060791
+ ],
+ [
+ "▁remerci",
+ -12.730076789855957
+ ],
+ [
+ "▁prevalent",
+ -12.730168342590332
+ ],
+ [
+ "370",
+ -12.730290412902832
+ ],
+ [
+ "▁bestellen",
+ -12.730327606201172
+ ],
+ [
+ "Buying",
+ -12.730341911315918
+ ],
+ [
+ "▁Aufbau",
+ -12.730416297912598
+ ],
+ [
+ "▁opini",
+ -12.730416297912598
+ ],
+ [
+ "▁regiune",
+ -12.730663299560547
+ ],
+ [
+ "▁martial",
+ -12.73069953918457
+ ],
+ [
+ "LK",
+ -12.730754852294922
+ ],
+ [
+ "▁Feuerwehr",
+ -12.730974197387695
+ ],
+ [
+ "screened",
+ -12.73099422454834
+ ],
+ [
+ "Blue",
+ -12.73120403289795
+ ],
+ [
+ "▁analize",
+ -12.731237411499023
+ ],
+ [
+ "▁lure",
+ -12.731247901916504
+ ],
+ [
+ "▁internally",
+ -12.731283187866211
+ ],
+ [
+ "father",
+ -12.731322288513184
+ ],
+ [
+ "▁diplomatic",
+ -12.731343269348145
+ ],
+ [
+ "▁Activity",
+ -12.731464385986328
+ ],
+ [
+ "▁cliqu",
+ -12.73156452178955
+ ],
+ [
+ "▁adequately",
+ -12.731809616088867
+ ],
+ [
+ "▁Elena",
+ -12.73183822631836
+ ],
+ [
+ "▁Citizens",
+ -12.732102394104004
+ ],
+ [
+ "▁Länge",
+ -12.732295989990234
+ ],
+ [
+ "▁respectful",
+ -12.732300758361816
+ ],
+ [
+ "▁zuständig",
+ -12.73248291015625
+ ],
+ [
+ "▁réception",
+ -12.732584953308105
+ ],
+ [
+ "▁headset",
+ -12.732686996459961
+ ],
+ [
+ "▁awhile",
+ -12.732705116271973
+ ],
+ [
+ "▁speculation",
+ -12.732707977294922
+ ],
+ [
+ "▁WhatsApp",
+ -12.732714653015137
+ ],
+ [
+ "▁tulbur",
+ -12.732731819152832
+ ],
+ [
+ "▁voluntar",
+ -12.732758522033691
+ ],
+ [
+ "▁Studium",
+ -12.73277473449707
+ ],
+ [
+ "▁protector",
+ -12.732833862304688
+ ],
+ [
+ "▁Wrap",
+ -12.732840538024902
+ ],
+ [
+ "staat",
+ -12.732951164245605
+ ],
+ [
+ "▁judgement",
+ -12.733396530151367
+ ],
+ [
+ "unauthorized",
+ -12.733397483825684
+ ],
+ [
+ "Rank",
+ -12.733487129211426
+ ],
+ [
+ "pră",
+ -12.733503341674805
+ ],
+ [
+ "▁Paw",
+ -12.733627319335938
+ ],
+ [
+ "▁relev",
+ -12.733664512634277
+ ],
+ [
+ "▁arbor",
+ -12.733830451965332
+ ],
+ [
+ "stretches",
+ -12.733885765075684
+ ],
+ [
+ "nook",
+ -12.733906745910645
+ ],
+ [
+ "▁Tunis",
+ -12.733907699584961
+ ],
+ [
+ "▁shocking",
+ -12.734036445617676
+ ],
+ [
+ "▁oppress",
+ -12.73414421081543
+ ],
+ [
+ "10.1",
+ -12.7341890335083
+ ],
+ [
+ "▁ERP",
+ -12.734310150146484
+ ],
+ [
+ "wolle",
+ -12.7343168258667
+ ],
+ [
+ "▁Catch",
+ -12.734352111816406
+ ],
+ [
+ "Plus",
+ -12.734368324279785
+ ],
+ [
+ "Market",
+ -12.734445571899414
+ ],
+ [
+ "scribed",
+ -12.734536170959473
+ ],
+ [
+ "▁décoration",
+ -12.734594345092773
+ ],
+ [
+ "▁chanson",
+ -12.734607696533203
+ ],
+ [
+ "▁Midwest",
+ -12.734763145446777
+ ],
+ [
+ "▁Spencer",
+ -12.734795570373535
+ ],
+ [
+ "▁societate",
+ -12.734807968139648
+ ],
+ [
+ "curated",
+ -12.735087394714355
+ ],
+ [
+ "▁canopy",
+ -12.735135078430176
+ ],
+ [
+ "ат",
+ -12.735142707824707
+ ],
+ [
+ "Sig",
+ -12.73514461517334
+ ],
+ [
+ "▁witch",
+ -12.735153198242188
+ ],
+ [
+ "envoyer",
+ -12.735175132751465
+ ],
+ [
+ "▁$1,000",
+ -12.735230445861816
+ ],
+ [
+ "▁peripheral",
+ -12.735482215881348
+ ],
+ [
+ "nnouncing",
+ -12.735509872436523
+ ],
+ [
+ "perfect",
+ -12.73559284210205
+ ],
+ [
+ "▁warten",
+ -12.735748291015625
+ ],
+ [
+ "ELI",
+ -12.735822677612305
+ ],
+ [
+ "▁recap",
+ -12.735912322998047
+ ],
+ [
+ "dün",
+ -12.735978126525879
+ ],
+ [
+ "▁Spre",
+ -12.736029624938965
+ ],
+ [
+ "2005",
+ -12.736153602600098
+ ],
+ [
+ "▁réparation",
+ -12.73617935180664
+ ],
+ [
+ "▁extraordinar",
+ -12.736196517944336
+ ],
+ [
+ "existence",
+ -12.736337661743164
+ ],
+ [
+ "oanele",
+ -12.736467361450195
+ ],
+ [
+ "▁reprezentant",
+ -12.736474990844727
+ ],
+ [
+ "▁attacker",
+ -12.736490249633789
+ ],
+ [
+ "▁Berliner",
+ -12.73657512664795
+ ],
+ [
+ "experience",
+ -12.736649513244629
+ ],
+ [
+ "▁Monde",
+ -12.736800193786621
+ ],
+ [
+ "intervention",
+ -12.736956596374512
+ ],
+ [
+ "▁Einstellung",
+ -12.736977577209473
+ ],
+ [
+ "▁Valentin",
+ -12.737011909484863
+ ],
+ [
+ "▁zonă",
+ -12.737200736999512
+ ],
+ [
+ "occupant",
+ -12.737223625183105
+ ],
+ [
+ "▁mobilis",
+ -12.737260818481445
+ ],
+ [
+ "metall",
+ -12.737261772155762
+ ],
+ [
+ "evangeli",
+ -12.73729133605957
+ ],
+ [
+ "Adding",
+ -12.737326622009277
+ ],
+ [
+ "▁Roland",
+ -12.73735237121582
+ ],
+ [
+ "ENCE",
+ -12.737462043762207
+ ],
+ [
+ "▁Insul",
+ -12.737478256225586
+ ],
+ [
+ "tellement",
+ -12.737497329711914
+ ],
+ [
+ "▁Blogger",
+ -12.737499237060547
+ ],
+ [
+ "▁prote",
+ -12.737504005432129
+ ],
+ [
+ "▁Minimum",
+ -12.737574577331543
+ ],
+ [
+ "▁termic",
+ -12.737624168395996
+ ],
+ [
+ "▁Sachen",
+ -12.737859725952148
+ ],
+ [
+ "▁Maschinen",
+ -12.737863540649414
+ ],
+ [
+ "▁Dragnea",
+ -12.737926483154297
+ ],
+ [
+ "▁overtime",
+ -12.737967491149902
+ ],
+ [
+ "calorie",
+ -12.737968444824219
+ ],
+ [
+ "▁jene",
+ -12.73814868927002
+ ],
+ [
+ "▁Satan",
+ -12.738153457641602
+ ],
+ [
+ "▁currencies",
+ -12.73827075958252
+ ],
+ [
+ "▁echipamente",
+ -12.738329887390137
+ ],
+ [
+ "▁forgiveness",
+ -12.73843765258789
+ ],
+ [
+ "▁Pause",
+ -12.738479614257812
+ ],
+ [
+ "▁Witt",
+ -12.738529205322266
+ ],
+ [
+ "STOR",
+ -12.738632202148438
+ ],
+ [
+ "▁actuelle",
+ -12.738703727722168
+ ],
+ [
+ "▁Ard",
+ -12.738853454589844
+ ],
+ [
+ "▁Constitu",
+ -12.738880157470703
+ ],
+ [
+ "ghan",
+ -12.7388916015625
+ ],
+ [
+ "Make",
+ -12.738906860351562
+ ],
+ [
+ "▁garne",
+ -12.738947868347168
+ ],
+ [
+ "▁Hitler",
+ -12.738956451416016
+ ],
+ [
+ "▁rubbish",
+ -12.738973617553711
+ ],
+ [
+ "6.0",
+ -12.739025115966797
+ ],
+ [
+ "▁Giving",
+ -12.739177703857422
+ ],
+ [
+ "▁persever",
+ -12.73937702178955
+ ],
+ [
+ "wirk",
+ -12.7394380569458
+ ],
+ [
+ "liegenden",
+ -12.739455223083496
+ ],
+ [
+ "▁morceau",
+ -12.73946762084961
+ ],
+ [
+ "atty",
+ -12.73961067199707
+ ],
+ [
+ "▁Quebec",
+ -12.739669799804688
+ ],
+ [
+ "harmonie",
+ -12.739705085754395
+ ],
+ [
+ "Nummer",
+ -12.739721298217773
+ ],
+ [
+ "▁splendid",
+ -12.739747047424316
+ ],
+ [
+ "▁halfway",
+ -12.739808082580566
+ ],
+ [
+ "▁periodically",
+ -12.740071296691895
+ ],
+ [
+ "▁Ländern",
+ -12.740077018737793
+ ],
+ [
+ "▁AAA",
+ -12.740083694458008
+ ],
+ [
+ "▁Frost",
+ -12.740198135375977
+ ],
+ [
+ "▁heroin",
+ -12.740289688110352
+ ],
+ [
+ "▁bucurie",
+ -12.7403564453125
+ ],
+ [
+ "▁Pradesh",
+ -12.74036693572998
+ ],
+ [
+ "zusetzen",
+ -12.740405082702637
+ ],
+ [
+ "raising",
+ -12.740425109863281
+ ],
+ [
+ "▁furniz",
+ -12.740567207336426
+ ],
+ [
+ "▁convi",
+ -12.740575790405273
+ ],
+ [
+ "pictured",
+ -12.740911483764648
+ ],
+ [
+ "▁inadequate",
+ -12.741065979003906
+ ],
+ [
+ "▁aprobat",
+ -12.741069793701172
+ ],
+ [
+ "▁exercising",
+ -12.741083145141602
+ ],
+ [
+ "▁faisai",
+ -12.741138458251953
+ ],
+ [
+ "▁prosecution",
+ -12.741231918334961
+ ],
+ [
+ "380",
+ -12.741402626037598
+ ],
+ [
+ "▁Potential",
+ -12.74145793914795
+ ],
+ [
+ "▁Magi",
+ -12.741523742675781
+ ],
+ [
+ "From",
+ -12.741752624511719
+ ],
+ [
+ "batterie",
+ -12.74181079864502
+ ],
+ [
+ "▁poisson",
+ -12.74185562133789
+ ],
+ [
+ "▁Probe",
+ -12.741950988769531
+ ],
+ [
+ "▁pastel",
+ -12.741998672485352
+ ],
+ [
+ "▁tracked",
+ -12.742410659790039
+ ],
+ [
+ "▁advertisers",
+ -12.74251937866211
+ ],
+ [
+ "adevar",
+ -12.742537498474121
+ ],
+ [
+ "ит",
+ -12.742776870727539
+ ],
+ [
+ "▁Herren",
+ -12.742815971374512
+ ],
+ [
+ "EAM",
+ -12.742820739746094
+ ],
+ [
+ "▁scooter",
+ -12.742822647094727
+ ],
+ [
+ "requesting",
+ -12.742841720581055
+ ],
+ [
+ "dynamis",
+ -12.742949485778809
+ ],
+ [
+ "▁dahin",
+ -12.742961883544922
+ ],
+ [
+ "▁tweak",
+ -12.743061065673828
+ ],
+ [
+ "▁hail",
+ -12.743101119995117
+ ],
+ [
+ "▁întotdeauna",
+ -12.743160247802734
+ ],
+ [
+ "▁Publikum",
+ -12.743167877197266
+ ],
+ [
+ "▁panoramic",
+ -12.743167877197266
+ ],
+ [
+ "▁PRE",
+ -12.74331283569336
+ ],
+ [
+ "▁thrill",
+ -12.743361473083496
+ ],
+ [
+ "Open",
+ -12.743366241455078
+ ],
+ [
+ "▁Layer",
+ -12.74345588684082
+ ],
+ [
+ "▁Bosch",
+ -12.743459701538086
+ ],
+ [
+ "hull",
+ -12.743511199951172
+ ],
+ [
+ "▁născut",
+ -12.743518829345703
+ ],
+ [
+ "tausch",
+ -12.743559837341309
+ ],
+ [
+ "▁autoturism",
+ -12.743577003479004
+ ],
+ [
+ "▁crank",
+ -12.743701934814453
+ ],
+ [
+ "CLE",
+ -12.743735313415527
+ ],
+ [
+ "▁Frederick",
+ -12.74386978149414
+ ],
+ [
+ "mog",
+ -12.743887901306152
+ ],
+ [
+ "behalten",
+ -12.74396800994873
+ ],
+ [
+ "▁aunt",
+ -12.744050979614258
+ ],
+ [
+ "▁Triple",
+ -12.744141578674316
+ ],
+ [
+ "▁Ark",
+ -12.744242668151855
+ ],
+ [
+ "AUD",
+ -12.744440078735352
+ ],
+ [
+ "▁Candy",
+ -12.744505882263184
+ ],
+ [
+ "tama",
+ -12.744515419006348
+ ],
+ [
+ "▁Evaluation",
+ -12.744571685791016
+ ],
+ [
+ "▁Memphis",
+ -12.744571685791016
+ ],
+ [
+ "▁stellar",
+ -12.74457836151123
+ ],
+ [
+ "▁fabricat",
+ -12.744632720947266
+ ],
+ [
+ "▁terminat",
+ -12.744868278503418
+ ],
+ [
+ "▁domnul",
+ -12.744913101196289
+ ],
+ [
+ "▁keynote",
+ -12.744925498962402
+ ],
+ [
+ "▁dentistry",
+ -12.744951248168945
+ ],
+ [
+ "rift",
+ -12.745052337646484
+ ],
+ [
+ "▁bilan",
+ -12.745119094848633
+ ],
+ [
+ "2.6",
+ -12.745125770568848
+ ],
+ [
+ "undergoing",
+ -12.745210647583008
+ ],
+ [
+ "▁pseudo",
+ -12.745274543762207
+ ],
+ [
+ "▁maşin",
+ -12.745280265808105
+ ],
+ [
+ "▁munte",
+ -12.74555492401123
+ ],
+ [
+ "▁VW",
+ -12.745932579040527
+ ],
+ [
+ "▁Rab",
+ -12.74593448638916
+ ],
+ [
+ "▁sustine",
+ -12.745972633361816
+ ],
+ [
+ "▁Bedingungen",
+ -12.745977401733398
+ ],
+ [
+ "▁învăţ",
+ -12.745980262756348
+ ],
+ [
+ "▁pyramid",
+ -12.745983123779297
+ ],
+ [
+ "HEN",
+ -12.746020317077637
+ ],
+ [
+ "▁citrus",
+ -12.746058464050293
+ ],
+ [
+ "Code",
+ -12.746064186096191
+ ],
+ [
+ "▁Beginning",
+ -12.746164321899414
+ ],
+ [
+ "▁discourse",
+ -12.746249198913574
+ ],
+ [
+ "▁miercuri",
+ -12.746329307556152
+ ],
+ [
+ "▁producător",
+ -12.74637508392334
+ ],
+ [
+ "▁analys",
+ -12.746397972106934
+ ],
+ [
+ "▁Evan",
+ -12.7467041015625
+ ],
+ [
+ "138",
+ -12.746987342834473
+ ],
+ [
+ "▁târziu",
+ -12.74703311920166
+ ],
+ [
+ "▁relocation",
+ -12.747052192687988
+ ],
+ [
+ "decizia",
+ -12.74708080291748
+ ],
+ [
+ "tollen",
+ -12.74714183807373
+ ],
+ [
+ "TRO",
+ -12.747180938720703
+ ],
+ [
+ "▁runway",
+ -12.74719524383545
+ ],
+ [
+ "illet",
+ -12.747270584106445
+ ],
+ [
+ "▁serveur",
+ -12.747387886047363
+ ],
+ [
+ "bezogen",
+ -12.747427940368652
+ ],
+ [
+ "▁believers",
+ -12.747668266296387
+ ],
+ [
+ "determined",
+ -12.747711181640625
+ ],
+ [
+ "▁reinforced",
+ -12.74791431427002
+ ],
+ [
+ "▁wedge",
+ -12.748006820678711
+ ],
+ [
+ "methyl",
+ -12.74807357788086
+ ],
+ [
+ "MES",
+ -12.748188018798828
+ ],
+ [
+ "vpn",
+ -12.748374938964844
+ ],
+ [
+ "▁consta",
+ -12.74837875366211
+ ],
+ [
+ "▁vizitat",
+ -12.748420715332031
+ ],
+ [
+ "modul",
+ -12.748455047607422
+ ],
+ [
+ "▁routing",
+ -12.748528480529785
+ ],
+ [
+ "tempted",
+ -12.748540878295898
+ ],
+ [
+ "URS",
+ -12.748785018920898
+ ],
+ [
+ "apprentissage",
+ -12.748795509338379
+ ],
+ [
+ "▁Hungary",
+ -12.748796463012695
+ ],
+ [
+ "Previously",
+ -12.74880313873291
+ ],
+ [
+ "▁translator",
+ -12.748804092407227
+ ],
+ [
+ "▁resonate",
+ -12.748830795288086
+ ],
+ [
+ "201",
+ -12.748851776123047
+ ],
+ [
+ "3-0",
+ -12.749029159545898
+ ],
+ [
+ "▁reunion",
+ -12.749090194702148
+ ],
+ [
+ "▁palate",
+ -12.749096870422363
+ ],
+ [
+ "0.4",
+ -12.749171257019043
+ ],
+ [
+ "reheat",
+ -12.74924373626709
+ ],
+ [
+ "Roo",
+ -12.749261856079102
+ ],
+ [
+ "200,000",
+ -12.74940013885498
+ ],
+ [
+ "Bro",
+ -12.749431610107422
+ ],
+ [
+ "▁estimation",
+ -12.749468803405762
+ ],
+ [
+ "schneiden",
+ -12.749499320983887
+ ],
+ [
+ "▁Inspired",
+ -12.749506950378418
+ ],
+ [
+ "▁lottery",
+ -12.749539375305176
+ ],
+ [
+ "▁Friedrich",
+ -12.749887466430664
+ ],
+ [
+ "FIT",
+ -12.749913215637207
+ ],
+ [
+ "0.6",
+ -12.7499418258667
+ ],
+ [
+ "▁dagegen",
+ -12.74997615814209
+ ],
+ [
+ "▁Reb",
+ -12.750115394592285
+ ],
+ [
+ "▁Eigenschaften",
+ -12.75020694732666
+ ],
+ [
+ "▁molding",
+ -12.750361442565918
+ ],
+ [
+ "▁Harper",
+ -12.750548362731934
+ ],
+ [
+ "verwaltung",
+ -12.75055980682373
+ ],
+ [
+ "▁Schlüssel",
+ -12.75055980682373
+ ],
+ [
+ "▁desfasura",
+ -12.75055980682373
+ ],
+ [
+ "▁rencontrer",
+ -12.75055980682373
+ ],
+ [
+ "▁negoci",
+ -12.750581741333008
+ ],
+ [
+ "▁Leading",
+ -12.750615119934082
+ ],
+ [
+ "▁necesita",
+ -12.750652313232422
+ ],
+ [
+ "▁biking",
+ -12.750683784484863
+ ],
+ [
+ "▁jointly",
+ -12.75069808959961
+ ],
+ [
+ "▁crush",
+ -12.750702857971191
+ ],
+ [
+ "Vol",
+ -12.750768661499023
+ ],
+ [
+ "▁ebay",
+ -12.750836372375488
+ ],
+ [
+ "▁Shri",
+ -12.750991821289062
+ ],
+ [
+ "▁AMD",
+ -12.751029968261719
+ ],
+ [
+ "FG",
+ -12.751032829284668
+ ],
+ [
+ "Argentin",
+ -12.75120735168457
+ ],
+ [
+ "▁incercat",
+ -12.751431465148926
+ ],
+ [
+ "▁tidy",
+ -12.751628875732422
+ ],
+ [
+ "▁provoqu",
+ -12.751635551452637
+ ],
+ [
+ "▁Written",
+ -12.751649856567383
+ ],
+ [
+ "▁Kooperation",
+ -12.751666069030762
+ ],
+ [
+ "▁scripture",
+ -12.751952171325684
+ ],
+ [
+ "▁Pflicht",
+ -12.751974105834961
+ ],
+ [
+ "ficial",
+ -12.752013206481934
+ ],
+ [
+ "vremea",
+ -12.752013206481934
+ ],
+ [
+ "▁Growing",
+ -12.752115249633789
+ ],
+ [
+ "▁redesign",
+ -12.752119064331055
+ ],
+ [
+ "▁obstacle",
+ -12.752214431762695
+ ],
+ [
+ "▁rugam",
+ -12.752235412597656
+ ],
+ [
+ "▁SPD",
+ -12.752243995666504
+ ],
+ [
+ "165",
+ -12.752270698547363
+ ],
+ [
+ "fiz",
+ -12.752284049987793
+ ],
+ [
+ "▁startet",
+ -12.752326011657715
+ ],
+ [
+ "▁Principle",
+ -12.752327919006348
+ ],
+ [
+ "▁abdominal",
+ -12.752327919006348
+ ],
+ [
+ "▁podium",
+ -12.752528190612793
+ ],
+ [
+ "duty",
+ -12.752616882324219
+ ],
+ [
+ "bonne",
+ -12.752679824829102
+ ],
+ [
+ "▁Serbia",
+ -12.752687454223633
+ ],
+ [
+ "▁brunch",
+ -12.752839088439941
+ ],
+ [
+ "▁Personne",
+ -12.752975463867188
+ ],
+ [
+ "▁Idea",
+ -12.753034591674805
+ ],
+ [
+ "forementioned",
+ -12.753036499023438
+ ],
+ [
+ "▁chassis",
+ -12.753037452697754
+ ],
+ [
+ "gebühr",
+ -12.753050804138184
+ ],
+ [
+ "ucun",
+ -12.753061294555664
+ ],
+ [
+ "▁Maz",
+ -12.7531156539917
+ ],
+ [
+ "1-4",
+ -12.75318431854248
+ ],
+ [
+ "kleid",
+ -12.753273963928223
+ ],
+ [
+ "▁Volvo",
+ -12.753337860107422
+ ],
+ [
+ "brechen",
+ -12.753378868103027
+ ],
+ [
+ "▁homepage",
+ -12.753472328186035
+ ],
+ [
+ "fuz",
+ -12.753509521484375
+ ],
+ [
+ "▁abgeschlossen",
+ -12.753595352172852
+ ],
+ [
+ "▁gelungen",
+ -12.753658294677734
+ ],
+ [
+ "▁booklet",
+ -12.753711700439453
+ ],
+ [
+ "▁Ukrainian",
+ -12.753745079040527
+ ],
+ [
+ "▁Melissa",
+ -12.753746032714844
+ ],
+ [
+ "CENT",
+ -12.75379467010498
+ ],
+ [
+ "▁intégré",
+ -12.753806114196777
+ ],
+ [
+ "weighing",
+ -12.753827095031738
+ ],
+ [
+ "▁crumbl",
+ -12.753894805908203
+ ],
+ [
+ "▁bunk",
+ -12.754167556762695
+ ],
+ [
+ "krieg",
+ -12.754207611083984
+ ],
+ [
+ "▁freshman",
+ -12.754307746887207
+ ],
+ [
+ "alaya",
+ -12.754339218139648
+ ],
+ [
+ "Avem",
+ -12.754353523254395
+ ],
+ [
+ "▁Kne",
+ -12.754423141479492
+ ],
+ [
+ "▁upstairs",
+ -12.75448226928711
+ ],
+ [
+ "AIL",
+ -12.754508972167969
+ ],
+ [
+ "țul",
+ -12.75478744506836
+ ],
+ [
+ "▁Lecture",
+ -12.754817962646484
+ ],
+ [
+ "▁entdecken",
+ -12.754843711853027
+ ],
+ [
+ "▁GMT",
+ -12.754912376403809
+ ],
+ [
+ "▁Leitung",
+ -12.754937171936035
+ ],
+ [
+ "▁inclined",
+ -12.755170822143555
+ ],
+ [
+ "▁skillet",
+ -12.75555419921875
+ ],
+ [
+ "FN",
+ -12.755742073059082
+ ],
+ [
+ "▁Perform",
+ -12.755821228027344
+ ],
+ [
+ "shift",
+ -12.75583267211914
+ ],
+ [
+ "recognizing",
+ -12.755873680114746
+ ],
+ [
+ "▁concise",
+ -12.755873680114746
+ ],
+ [
+ "▁obsessed",
+ -12.755873680114746
+ ],
+ [
+ "▁removable",
+ -12.755873680114746
+ ],
+ [
+ "▁Relax",
+ -12.755888938903809
+ ],
+ [
+ "delegates",
+ -12.75605583190918
+ ],
+ [
+ "▁expedi",
+ -12.756074905395508
+ ],
+ [
+ "▁Schä",
+ -12.756138801574707
+ ],
+ [
+ "iete",
+ -12.756211280822754
+ ],
+ [
+ "▁reciproc",
+ -12.756229400634766
+ ],
+ [
+ "▁neutr",
+ -12.75625228881836
+ ],
+ [
+ "lactic",
+ -12.756314277648926
+ ],
+ [
+ "▁Nah",
+ -12.756328582763672
+ ],
+ [
+ "scene",
+ -12.7565279006958
+ ],
+ [
+ "▁Helm",
+ -12.756563186645508
+ ],
+ [
+ "▁Bewerbung",
+ -12.756671905517578
+ ],
+ [
+ "▁Cassi",
+ -12.75667953491211
+ ],
+ [
+ "▁Gelegenheit",
+ -12.756939888000488
+ ],
+ [
+ "▁reflective",
+ -12.757140159606934
+ ],
+ [
+ "▁încredere",
+ -12.757149696350098
+ ],
+ [
+ "▁cigarettes",
+ -12.75717544555664
+ ],
+ [
+ "▁Zusätzlich",
+ -12.757295608520508
+ ],
+ [
+ "▁intercept",
+ -12.75731372833252
+ ],
+ [
+ "▁Finn",
+ -12.757468223571777
+ ],
+ [
+ "▁ignor",
+ -12.757661819458008
+ ],
+ [
+ "gian",
+ -12.75766372680664
+ ],
+ [
+ "BRA",
+ -12.757740020751953
+ ],
+ [
+ "leader",
+ -12.757957458496094
+ ],
+ [
+ "nius",
+ -12.757981300354004
+ ],
+ [
+ "▁skies",
+ -12.757987022399902
+ ],
+ [
+ "▁nunta",
+ -12.758023262023926
+ ],
+ [
+ "▁grec",
+ -12.758041381835938
+ ],
+ [
+ "arranging",
+ -12.75816822052002
+ ],
+ [
+ "wartet",
+ -12.758231163024902
+ ],
+ [
+ "▁kostet",
+ -12.758377075195312
+ ],
+ [
+ "▁Entre",
+ -12.758541107177734
+ ],
+ [
+ "Mag",
+ -12.758575439453125
+ ],
+ [
+ "▁radiator",
+ -12.758598327636719
+ ],
+ [
+ "übrigens",
+ -12.758689880371094
+ ],
+ [
+ "Internet",
+ -12.758706092834473
+ ],
+ [
+ "▁connexion",
+ -12.758718490600586
+ ],
+ [
+ "▁prolonged",
+ -12.758854866027832
+ ],
+ [
+ "▁capabil",
+ -12.75914192199707
+ ],
+ [
+ "▁feeder",
+ -12.759217262268066
+ ],
+ [
+ "Initially",
+ -12.759223937988281
+ ],
+ [
+ "Green",
+ -12.75926685333252
+ ],
+ [
+ "▁passiert",
+ -12.759272575378418
+ ],
+ [
+ "▁courtyard",
+ -12.759299278259277
+ ],
+ [
+ "▁judeţ",
+ -12.759320259094238
+ ],
+ [
+ "▁Coalition",
+ -12.759431838989258
+ ],
+ [
+ "▁atmospheric",
+ -12.759431838989258
+ ],
+ [
+ "▁velocity",
+ -12.759431838989258
+ ],
+ [
+ "▁Frühstück",
+ -12.759432792663574
+ ],
+ [
+ "vacancies",
+ -12.759438514709473
+ ],
+ [
+ "unified",
+ -12.759538650512695
+ ],
+ [
+ "▁Ahmed",
+ -12.759538650512695
+ ],
+ [
+ "poured",
+ -12.759550094604492
+ ],
+ [
+ "▁Mikro",
+ -12.75959587097168
+ ],
+ [
+ "▁Klar",
+ -12.759661674499512
+ ],
+ [
+ "kommt",
+ -12.759681701660156
+ ],
+ [
+ "seated",
+ -12.759744644165039
+ ],
+ [
+ "musik",
+ -12.75976848602295
+ ],
+ [
+ "▁stimulation",
+ -12.759841918945312
+ ],
+ [
+ "▁solicitat",
+ -12.759880065917969
+ ],
+ [
+ "▁politically",
+ -12.760165214538574
+ ],
+ [
+ "restoring",
+ -12.760322570800781
+ ],
+ [
+ "▁Rag",
+ -12.760435104370117
+ ],
+ [
+ "▁officielle",
+ -12.760468482971191
+ ],
+ [
+ "▁Annie",
+ -12.760479927062988
+ ],
+ [
+ "▁tourne",
+ -12.760634422302246
+ ],
+ [
+ "▁Joel",
+ -12.760642051696777
+ ],
+ [
+ "blieben",
+ -12.760666847229004
+ ],
+ [
+ "▁repayment",
+ -12.760736465454102
+ ],
+ [
+ "▁Strategi",
+ -12.760781288146973
+ ],
+ [
+ "▁prietenii",
+ -12.760804176330566
+ ],
+ [
+ "▁Montgomery",
+ -12.760858535766602
+ ],
+ [
+ "▁résidence",
+ -12.760858535766602
+ ],
+ [
+ "▁sunglasses",
+ -12.760858535766602
+ ],
+ [
+ "▁1956",
+ -12.760882377624512
+ ],
+ [
+ "MEN",
+ -12.76093578338623
+ ],
+ [
+ "pouvant",
+ -12.760997772216797
+ ],
+ [
+ "375",
+ -12.761061668395996
+ ],
+ [
+ "directed",
+ -12.761173248291016
+ ],
+ [
+ "▁grinder",
+ -12.76120662689209
+ ],
+ [
+ "rträge",
+ -12.761279106140137
+ ],
+ [
+ "▁nickel",
+ -12.761299133300781
+ ],
+ [
+ "▁Maintain",
+ -12.761313438415527
+ ],
+ [
+ "▁Holmes",
+ -12.761392593383789
+ ],
+ [
+ "▁obtinut",
+ -12.76157283782959
+ ],
+ [
+ "▁walnut",
+ -12.761585235595703
+ ],
+ [
+ "▁consultancy",
+ -12.761640548706055
+ ],
+ [
+ "cooled",
+ -12.761651039123535
+ ],
+ [
+ "▁Brig",
+ -12.761711120605469
+ ],
+ [
+ "▁Produc",
+ -12.761873245239258
+ ],
+ [
+ "street",
+ -12.76187515258789
+ ],
+ [
+ "▁Einfach",
+ -12.761897087097168
+ ],
+ [
+ "North",
+ -12.762149810791016
+ ],
+ [
+ "▁PET",
+ -12.76220989227295
+ ],
+ [
+ "▁Président",
+ -12.762288093566895
+ ],
+ [
+ "▁produsului",
+ -12.762457847595215
+ ],
+ [
+ "literatur",
+ -12.762483596801758
+ ],
+ [
+ "133",
+ -12.762561798095703
+ ],
+ [
+ "▁recours",
+ -12.762591361999512
+ ],
+ [
+ "▁verpflichtet",
+ -12.76264476776123
+ ],
+ [
+ "▁Wur",
+ -12.762733459472656
+ ],
+ [
+ "▁psiholog",
+ -12.762796401977539
+ ],
+ [
+ "Veg",
+ -12.762871742248535
+ ],
+ [
+ "▁hype",
+ -12.762930870056152
+ ],
+ [
+ "augmenter",
+ -12.762974739074707
+ ],
+ [
+ "▁Welsh",
+ -12.763012886047363
+ ],
+ [
+ "mounted",
+ -12.763158798217773
+ ],
+ [
+ "▁Wann",
+ -12.763425827026367
+ ],
+ [
+ "▁gezeigt",
+ -12.763620376586914
+ ],
+ [
+ "▁memo",
+ -12.763631820678711
+ ],
+ [
+ "veterinary",
+ -12.763717651367188
+ ],
+ [
+ "▁Olympia",
+ -12.763717651367188
+ ],
+ [
+ "▁handsome",
+ -12.763871192932129
+ ],
+ [
+ "yama",
+ -12.763911247253418
+ ],
+ [
+ "studio",
+ -12.763912200927734
+ ],
+ [
+ "sozial",
+ -12.764020919799805
+ ],
+ [
+ "▁reap",
+ -12.764104843139648
+ ],
+ [
+ "▁didactic",
+ -12.764111518859863
+ ],
+ [
+ "▁Cookie",
+ -12.764126777648926
+ ],
+ [
+ "▁cooper",
+ -12.764230728149414
+ ],
+ [
+ "▁discern",
+ -12.76441478729248
+ ],
+ [
+ "▁Ubuntu",
+ -12.764433860778809
+ ],
+ [
+ "domain",
+ -12.76443862915039
+ ],
+ [
+ "▁plasa",
+ -12.764460563659668
+ ],
+ [
+ "hong",
+ -12.764585494995117
+ ],
+ [
+ "▁Freiheit",
+ -12.764662742614746
+ ],
+ [
+ "▁Gateway",
+ -12.764678001403809
+ ],
+ [
+ "▁poke",
+ -12.764796257019043
+ ],
+ [
+ "▁niedrig",
+ -12.76484203338623
+ ],
+ [
+ "▁corrected",
+ -12.764899253845215
+ ],
+ [
+ "▁predator",
+ -12.76490306854248
+ ],
+ [
+ "QA",
+ -12.76507568359375
+ ],
+ [
+ "Physio",
+ -12.765101432800293
+ ],
+ [
+ "MAS",
+ -12.765108108520508
+ ],
+ [
+ "▁sanctuary",
+ -12.765151023864746
+ ],
+ [
+ "▁aferent",
+ -12.76523494720459
+ ],
+ [
+ "▁perdre",
+ -12.765268325805664
+ ],
+ [
+ "▁recherch",
+ -12.765397071838379
+ ],
+ [
+ "ready",
+ -12.76559829711914
+ ],
+ [
+ "without",
+ -12.76560115814209
+ ],
+ [
+ "▁locuitori",
+ -12.765628814697266
+ ],
+ [
+ "▁Memo",
+ -12.765636444091797
+ ],
+ [
+ "▁Laden",
+ -12.765646934509277
+ ],
+ [
+ "danken",
+ -12.76577377319336
+ ],
+ [
+ "▁CNC",
+ -12.765861511230469
+ ],
+ [
+ "▁jealous",
+ -12.765881538391113
+ ],
+ [
+ "▁Background",
+ -12.765951156616211
+ ],
+ [
+ "▁Marx",
+ -12.765999794006348
+ ],
+ [
+ "▁Heli",
+ -12.766039848327637
+ ],
+ [
+ "▁osteo",
+ -12.766057968139648
+ ],
+ [
+ "▁rassembl",
+ -12.766162872314453
+ ],
+ [
+ "▁altceva",
+ -12.766226768493652
+ ],
+ [
+ "▁beschäftigt",
+ -12.766226768493652
+ ],
+ [
+ "▁accru",
+ -12.766266822814941
+ ],
+ [
+ "üft",
+ -12.766273498535156
+ ],
+ [
+ "▁sprout",
+ -12.766288757324219
+ ],
+ [
+ "endorf",
+ -12.76647663116455
+ ],
+ [
+ "▁specialitate",
+ -12.766483306884766
+ ],
+ [
+ "éanmoins",
+ -12.766586303710938
+ ],
+ [
+ "▁poign",
+ -12.766663551330566
+ ],
+ [
+ "▁mânca",
+ -12.766668319702148
+ ],
+ [
+ "▁stretched",
+ -12.766752243041992
+ ],
+ [
+ "fensiv",
+ -12.76677131652832
+ ],
+ [
+ "▁Auction",
+ -12.76683235168457
+ ],
+ [
+ "hints",
+ -12.766944885253906
+ ],
+ [
+ "▁typo",
+ -12.766983032226562
+ ],
+ [
+ "▁Rare",
+ -12.767003059387207
+ ],
+ [
+ "▁interruption",
+ -12.767043113708496
+ ],
+ [
+ "▁Mean",
+ -12.76709270477295
+ ],
+ [
+ "privileged",
+ -12.767108917236328
+ ],
+ [
+ "▁purtat",
+ -12.767129898071289
+ ],
+ [
+ "studie",
+ -12.767229080200195
+ ],
+ [
+ "offres",
+ -12.767248153686523
+ ],
+ [
+ "▁flap",
+ -12.76729679107666
+ ],
+ [
+ "▁rhetoric",
+ -12.767304420471191
+ ],
+ [
+ "▁snapshot",
+ -12.767325401306152
+ ],
+ [
+ "▁Conservative",
+ -12.767367362976074
+ ],
+ [
+ "▁taie",
+ -12.767416954040527
+ ],
+ [
+ "Game",
+ -12.767499923706055
+ ],
+ [
+ "▁naissance",
+ -12.767663955688477
+ ],
+ [
+ "Prof",
+ -12.767704963684082
+ ],
+ [
+ "qualified",
+ -12.767745971679688
+ ],
+ [
+ "▁suppression",
+ -12.767749786376953
+ ],
+ [
+ "▁răspunde",
+ -12.767765045166016
+ ],
+ [
+ "▁1/3",
+ -12.767803192138672
+ ],
+ [
+ "▁lieben",
+ -12.767858505249023
+ ],
+ [
+ "ù",
+ -12.767898559570312
+ ],
+ [
+ "america",
+ -12.767955780029297
+ ],
+ [
+ "▁Mum",
+ -12.768182754516602
+ ],
+ [
+ "▁Researchers",
+ -12.76827335357666
+ ],
+ [
+ "quip",
+ -12.768308639526367
+ ],
+ [
+ "▁fenomen",
+ -12.768383026123047
+ ],
+ [
+ "stools",
+ -12.768387794494629
+ ],
+ [
+ "▁commodity",
+ -12.768742561340332
+ ],
+ [
+ "▁rejuvenat",
+ -12.768745422363281
+ ],
+ [
+ "▁ausgezeichnet",
+ -12.76876449584961
+ ],
+ [
+ "▁păcate",
+ -12.768784523010254
+ ],
+ [
+ "3.6",
+ -12.76882553100586
+ ],
+ [
+ "zwei",
+ -12.768904685974121
+ ],
+ [
+ "accounted",
+ -12.768982887268066
+ ],
+ [
+ "▁Cycle",
+ -12.76900863647461
+ ],
+ [
+ "politischen",
+ -12.769031524658203
+ ],
+ [
+ "Normally",
+ -12.76904010772705
+ ],
+ [
+ "▁transcend",
+ -12.769158363342285
+ ],
+ [
+ "▁Classes",
+ -12.769268989562988
+ ],
+ [
+ "▁vene",
+ -12.769363403320312
+ ],
+ [
+ "protein",
+ -12.76942253112793
+ ],
+ [
+ "formulaire",
+ -12.76944351196289
+ ],
+ [
+ "▁endurance",
+ -12.769463539123535
+ ],
+ [
+ "▁Census",
+ -12.769464492797852
+ ],
+ [
+ "▁census",
+ -12.7694673538208
+ ],
+ [
+ "▁conțin",
+ -12.76952838897705
+ ],
+ [
+ "▁multinational",
+ -12.769563674926758
+ ],
+ [
+ "▁consomm",
+ -12.769572257995605
+ ],
+ [
+ "▁Porter",
+ -12.769762992858887
+ ],
+ [
+ "▁marvel",
+ -12.769777297973633
+ ],
+ [
+ "▁probable",
+ -12.769824028015137
+ ],
+ [
+ "dependable",
+ -12.770044326782227
+ ],
+ [
+ "▁crore",
+ -12.77015495300293
+ ],
+ [
+ "▁6:30",
+ -12.770224571228027
+ ],
+ [
+ "▁Bradley",
+ -12.77032470703125
+ ],
+ [
+ "molecule",
+ -12.770400047302246
+ ],
+ [
+ "inclusiv",
+ -12.770516395568848
+ ],
+ [
+ "▁privilégi",
+ -12.770543098449707
+ ],
+ [
+ "▁cerere",
+ -12.770611763000488
+ ],
+ [
+ "ouille",
+ -12.770696640014648
+ ],
+ [
+ "▁âgé",
+ -12.770787239074707
+ ],
+ [
+ "▁ghid",
+ -12.770801544189453
+ ],
+ [
+ "▁Controller",
+ -12.77082347869873
+ ],
+ [
+ "▁incredere",
+ -12.770988464355469
+ ],
+ [
+ "▁hostel",
+ -12.771015167236328
+ ],
+ [
+ "wissenschaft",
+ -12.771121978759766
+ ],
+ [
+ "▁cooperate",
+ -12.771183967590332
+ ],
+ [
+ "ки",
+ -12.771202087402344
+ ],
+ [
+ "▁Küchen",
+ -12.771384239196777
+ ],
+ [
+ "▁BIO",
+ -12.771406173706055
+ ],
+ [
+ "▁deliveries",
+ -12.771458625793457
+ ],
+ [
+ "▁urmări",
+ -12.771553993225098
+ ],
+ [
+ "▁überzeugen",
+ -12.771631240844727
+ ],
+ [
+ "Roofing",
+ -12.771703720092773
+ ],
+ [
+ "▁Adel",
+ -12.771737098693848
+ ],
+ [
+ "▁navy",
+ -12.77181339263916
+ ],
+ [
+ "▁cider",
+ -12.772101402282715
+ ],
+ [
+ "▁dulce",
+ -12.772109985351562
+ ],
+ [
+ "▁inspirat",
+ -12.772163391113281
+ ],
+ [
+ "allez",
+ -12.772164344787598
+ ],
+ [
+ "HH",
+ -12.77221965789795
+ ],
+ [
+ "▁Danish",
+ -12.7722749710083
+ ],
+ [
+ "CDC",
+ -12.7722806930542
+ ],
+ [
+ "▁Milch",
+ -12.772303581237793
+ ],
+ [
+ "▁Hockey",
+ -12.772346496582031
+ ],
+ [
+ "▁Smooth",
+ -12.772347450256348
+ ],
+ [
+ "▁FIFA",
+ -12.772361755371094
+ ],
+ [
+ "▁Devon",
+ -12.772364616394043
+ ],
+ [
+ "chung",
+ -12.772379875183105
+ ],
+ [
+ "▁villain",
+ -12.772420883178711
+ ],
+ [
+ "▁musée",
+ -12.772441864013672
+ ],
+ [
+ "tiennent",
+ -12.772557258605957
+ ],
+ [
+ "chou",
+ -12.772732734680176
+ ],
+ [
+ "kopf",
+ -12.772809982299805
+ ],
+ [
+ "printed",
+ -12.77281379699707
+ ],
+ [
+ "▁Depression",
+ -12.773076057434082
+ ],
+ [
+ "▁opioid",
+ -12.773082733154297
+ ],
+ [
+ "nomie",
+ -12.773098945617676
+ ],
+ [
+ "▁footwear",
+ -12.773211479187012
+ ],
+ [
+ "▁Cause",
+ -12.773260116577148
+ ],
+ [
+ "SEL",
+ -12.773515701293945
+ ],
+ [
+ "▁Roller",
+ -12.773523330688477
+ ],
+ [
+ "▁einzigartige",
+ -12.773589134216309
+ ],
+ [
+ "desea",
+ -12.773597717285156
+ ],
+ [
+ "▁nasty",
+ -12.773792266845703
+ ],
+ [
+ "formulated",
+ -12.773877143859863
+ ],
+ [
+ "breaker",
+ -12.773958206176758
+ ],
+ [
+ "▁goodies",
+ -12.773961067199707
+ ],
+ [
+ "▁sandy",
+ -12.774189949035645
+ ],
+ [
+ "method",
+ -12.77425479888916
+ ],
+ [
+ "▁Maple",
+ -12.774308204650879
+ ],
+ [
+ "gefragt",
+ -12.774435997009277
+ ],
+ [
+ "▁decreasing",
+ -12.774515151977539
+ ],
+ [
+ "ceşti",
+ -12.774555206298828
+ ],
+ [
+ "▁DUI",
+ -12.774563789367676
+ ],
+ [
+ "▁pierdere",
+ -12.774574279785156
+ ],
+ [
+ "▁brushes",
+ -12.77466869354248
+ ],
+ [
+ "▁Fully",
+ -12.774712562561035
+ ],
+ [
+ "filtered",
+ -12.774789810180664
+ ],
+ [
+ "ruins",
+ -12.774988174438477
+ ],
+ [
+ "Save",
+ -12.775114059448242
+ ],
+ [
+ "sweeping",
+ -12.7752046585083
+ ],
+ [
+ "PCR",
+ -12.775334358215332
+ ],
+ [
+ "▁folded",
+ -12.775337219238281
+ ],
+ [
+ "▁urca",
+ -12.775444030761719
+ ],
+ [
+ "▁clic",
+ -12.775484085083008
+ ],
+ [
+ "▁spécialiste",
+ -12.775614738464355
+ ],
+ [
+ "▁durfte",
+ -12.775686264038086
+ ],
+ [
+ "tuși",
+ -12.775871276855469
+ ],
+ [
+ "▁diligent",
+ -12.77596378326416
+ ],
+ [
+ "▁verdict",
+ -12.775972366333008
+ ],
+ [
+ "▁chaise",
+ -12.776039123535156
+ ],
+ [
+ "▁cleanup",
+ -12.776068687438965
+ ],
+ [
+ "▁Guitar",
+ -12.776076316833496
+ ],
+ [
+ "▁Dip",
+ -12.776142120361328
+ ],
+ [
+ "vru",
+ -12.776260375976562
+ ],
+ [
+ "▁cogn",
+ -12.776373863220215
+ ],
+ [
+ "something",
+ -12.776529312133789
+ ],
+ [
+ "hidr",
+ -12.776535034179688
+ ],
+ [
+ "ENG",
+ -12.776607513427734
+ ],
+ [
+ "Paul",
+ -12.776679039001465
+ ],
+ [
+ "▁reboot",
+ -12.776687622070312
+ ],
+ [
+ "savvy",
+ -12.776688575744629
+ ],
+ [
+ "▁Macron",
+ -12.776710510253906
+ ],
+ [
+ "▁Kino",
+ -12.77682876586914
+ ],
+ [
+ "232",
+ -12.776832580566406
+ ],
+ [
+ "▁gravit",
+ -12.776861190795898
+ ],
+ [
+ "ANC",
+ -12.776883125305176
+ ],
+ [
+ "▁petrecut",
+ -12.776944160461426
+ ],
+ [
+ "▁signage",
+ -12.776959419250488
+ ],
+ [
+ "odia",
+ -12.776987075805664
+ ],
+ [
+ "▁GRA",
+ -12.77712631225586
+ ],
+ [
+ "▁alegeril",
+ -12.777129173278809
+ ],
+ [
+ "leger",
+ -12.77717399597168
+ ],
+ [
+ "▁medicamente",
+ -12.777174949645996
+ ],
+ [
+ "pentru",
+ -12.777249336242676
+ ],
+ [
+ "▁collectif",
+ -12.777251243591309
+ ],
+ [
+ "▁Sohn",
+ -12.777298927307129
+ ],
+ [
+ "205",
+ -12.777313232421875
+ ],
+ [
+ "▁Reach",
+ -12.77733039855957
+ ],
+ [
+ "RAM",
+ -12.777400970458984
+ ],
+ [
+ "3.4",
+ -12.777405738830566
+ ],
+ [
+ "▁bleach",
+ -12.777409553527832
+ ],
+ [
+ "▁diligence",
+ -12.777414321899414
+ ],
+ [
+ "▁MORE",
+ -12.777440071105957
+ ],
+ [
+ "▁Critical",
+ -12.777471542358398
+ ],
+ [
+ "▁singură",
+ -12.77767276763916
+ ],
+ [
+ "▁adversar",
+ -12.777791023254395
+ ],
+ [
+ "▁Buzz",
+ -12.7778902053833
+ ],
+ [
+ "▁demeure",
+ -12.778063774108887
+ ],
+ [
+ "▁nephew",
+ -12.778141021728516
+ ],
+ [
+ "▁Boom",
+ -12.77817440032959
+ ],
+ [
+ "▁shining",
+ -12.77819538116455
+ ],
+ [
+ "▁sponge",
+ -12.778206825256348
+ ],
+ [
+ "liest",
+ -12.77841854095459
+ ],
+ [
+ "rseits",
+ -12.778690338134766
+ ],
+ [
+ "▁capita",
+ -12.778823852539062
+ ],
+ [
+ "esthesia",
+ -12.778867721557617
+ ],
+ [
+ "500,000",
+ -12.77895736694336
+ ],
+ [
+ "▁Pressure",
+ -12.77898120880127
+ ],
+ [
+ "ifikation",
+ -12.779021263122559
+ ],
+ [
+ "▁acceleration",
+ -12.779181480407715
+ ],
+ [
+ "▁Pfarr",
+ -12.779282569885254
+ ],
+ [
+ "▁imobil",
+ -12.779304504394531
+ ],
+ [
+ "▁pericol",
+ -12.779326438903809
+ ],
+ [
+ "▁flock",
+ -12.779454231262207
+ ],
+ [
+ "▁Scholar",
+ -12.77962875366211
+ ],
+ [
+ "▁Fusion",
+ -12.779630661010742
+ ],
+ [
+ "▁revolve",
+ -12.779637336730957
+ ],
+ [
+ "Plugin",
+ -12.779664993286133
+ ],
+ [
+ "▁Ruf",
+ -12.779691696166992
+ ],
+ [
+ "▁tehnici",
+ -12.780024528503418
+ ],
+ [
+ "voice",
+ -12.78005313873291
+ ],
+ [
+ "▁anomal",
+ -12.780203819274902
+ ],
+ [
+ "▁gefallen",
+ -12.780252456665039
+ ],
+ [
+ "▁Wyoming",
+ -12.780322074890137
+ ],
+ [
+ "▁9:00",
+ -12.780354499816895
+ ],
+ [
+ "packed",
+ -12.780461311340332
+ ],
+ [
+ "▁Zimbabwe",
+ -12.780686378479004
+ ],
+ [
+ "▁glücklich",
+ -12.780766487121582
+ ],
+ [
+ "ethanol",
+ -12.78077220916748
+ ],
+ [
+ "▁effektiv",
+ -12.780936241149902
+ ],
+ [
+ "▁saptamani",
+ -12.781049728393555
+ ],
+ [
+ "▁umfasst",
+ -12.781052589416504
+ ],
+ [
+ "▁Werbung",
+ -12.781103134155273
+ ],
+ [
+ "▁undermine",
+ -12.781164169311523
+ ],
+ [
+ "▁Lego",
+ -12.781322479248047
+ ],
+ [
+ "▁Rac",
+ -12.781323432922363
+ ],
+ [
+ "educating",
+ -12.781441688537598
+ ],
+ [
+ "leiten",
+ -12.781451225280762
+ ],
+ [
+ "derma",
+ -12.781518936157227
+ ],
+ [
+ "hängen",
+ -12.781597137451172
+ ],
+ [
+ "Lumin",
+ -12.781846046447754
+ ],
+ [
+ "▁PNL",
+ -12.781913757324219
+ ],
+ [
+ "▁volcano",
+ -12.782064437866211
+ ],
+ [
+ "▁Anfrage",
+ -12.782066345214844
+ ],
+ [
+ "▁resp",
+ -12.782124519348145
+ ],
+ [
+ "leigh",
+ -12.78217601776123
+ ],
+ [
+ "▁addict",
+ -12.782176971435547
+ ],
+ [
+ "WORK",
+ -12.782312393188477
+ ],
+ [
+ "▁FY",
+ -12.782322883605957
+ ],
+ [
+ "▁maneuver",
+ -12.782513618469238
+ ],
+ [
+ "flächen",
+ -12.782525062561035
+ ],
+ [
+ "zweck",
+ -12.782527923583984
+ ],
+ [
+ "tolerant",
+ -12.782609939575195
+ ],
+ [
+ "Davidson",
+ -12.78272533416748
+ ],
+ [
+ "▁meteor",
+ -12.782849311828613
+ ],
+ [
+ "▁Stephanie",
+ -12.78291130065918
+ ],
+ [
+ "▁plafon",
+ -12.783126831054688
+ ],
+ [
+ "technischen",
+ -12.78316879272461
+ ],
+ [
+ "unused",
+ -12.783193588256836
+ ],
+ [
+ "▁voulai",
+ -12.783228874206543
+ ],
+ [
+ "▁fehlt",
+ -12.783447265625
+ ],
+ [
+ "möglichen",
+ -12.783955574035645
+ ],
+ [
+ "▁Twenty",
+ -12.783968925476074
+ ],
+ [
+ "composing",
+ -12.783979415893555
+ ],
+ [
+ "▁rebate",
+ -12.78400707244873
+ ],
+ [
+ "Italie",
+ -12.784036636352539
+ ],
+ [
+ "▁goodbye",
+ -12.784058570861816
+ ],
+ [
+ "wild",
+ -12.784061431884766
+ ],
+ [
+ "▁lancé",
+ -12.784077644348145
+ ],
+ [
+ "▁wunderschöne",
+ -12.784083366394043
+ ],
+ [
+ "▁Frontier",
+ -12.784139633178711
+ ],
+ [
+ "▁murit",
+ -12.784313201904297
+ ],
+ [
+ "▁scump",
+ -12.78464412689209
+ ],
+ [
+ "OVER",
+ -12.784682273864746
+ ],
+ [
+ "▁meme",
+ -12.784709930419922
+ ],
+ [
+ "Super",
+ -12.784733772277832
+ ],
+ [
+ "▁Crack",
+ -12.784849166870117
+ ],
+ [
+ "rennen",
+ -12.784907341003418
+ ],
+ [
+ "▁interessiert",
+ -12.784941673278809
+ ],
+ [
+ "▁relaţi",
+ -12.784942626953125
+ ],
+ [
+ "▁factories",
+ -12.784975051879883
+ ],
+ [
+ "▁[...]",
+ -12.785066604614258
+ ],
+ [
+ "▁vizite",
+ -12.785075187683105
+ ],
+ [
+ "▁erfolgen",
+ -12.785199165344238
+ ],
+ [
+ "▁Hosting",
+ -12.785244941711426
+ ],
+ [
+ "▁localitate",
+ -12.78528118133545
+ ],
+ [
+ "▁chasse",
+ -12.785415649414062
+ ],
+ [
+ "▁Meadow",
+ -12.785465240478516
+ ],
+ [
+ "▁expansive",
+ -12.785513877868652
+ ],
+ [
+ "hov",
+ -12.785874366760254
+ ],
+ [
+ "Phil",
+ -12.785978317260742
+ ],
+ [
+ "illian",
+ -12.786107063293457
+ ],
+ [
+ "▁manipulate",
+ -12.786107063293457
+ ],
+ [
+ "informationen",
+ -12.786130905151367
+ ],
+ [
+ "▁profesionist",
+ -12.786162376403809
+ ],
+ [
+ "risen",
+ -12.786252975463867
+ ],
+ [
+ "frem",
+ -12.786300659179688
+ ],
+ [
+ "Act",
+ -12.78640079498291
+ ],
+ [
+ "supervised",
+ -12.786491394042969
+ ],
+ [
+ "▁capul",
+ -12.786506652832031
+ ],
+ [
+ "▁Craiova",
+ -12.786528587341309
+ ],
+ [
+ "▁victoire",
+ -12.786528587341309
+ ],
+ [
+ "▁guitarist",
+ -12.786680221557617
+ ],
+ [
+ "▁identific",
+ -12.786684036254883
+ ],
+ [
+ "democrat",
+ -12.786864280700684
+ ],
+ [
+ "Authentic",
+ -12.786894798278809
+ ],
+ [
+ "▁Autumn",
+ -12.786894798278809
+ ],
+ [
+ "▁bodi",
+ -12.787014961242676
+ ],
+ [
+ "April",
+ -12.787044525146484
+ ],
+ [
+ "▁Burger",
+ -12.787049293518066
+ ],
+ [
+ "▁BEST",
+ -12.787490844726562
+ ],
+ [
+ "▁torrent",
+ -12.78749942779541
+ ],
+ [
+ "UV",
+ -12.787567138671875
+ ],
+ [
+ "▁renal",
+ -12.787676811218262
+ ],
+ [
+ "founded",
+ -12.787693977355957
+ ],
+ [
+ "203",
+ -12.787956237792969
+ ],
+ [
+ "▁Flooring",
+ -12.78799057006836
+ ],
+ [
+ "▁kilogram",
+ -12.787994384765625
+ ],
+ [
+ "▁garantiert",
+ -12.788139343261719
+ ],
+ [
+ "▁fulfil",
+ -12.788204193115234
+ ],
+ [
+ "303",
+ -12.788330078125
+ ],
+ [
+ "▁schafft",
+ -12.788363456726074
+ ],
+ [
+ "▁butterfly",
+ -12.788365364074707
+ ],
+ [
+ "▁Stuart",
+ -12.788382530212402
+ ],
+ [
+ "▁Versuch",
+ -12.788392066955566
+ ],
+ [
+ "▁liking",
+ -12.788412094116211
+ ],
+ [
+ "▁chercher",
+ -12.788508415222168
+ ],
+ [
+ "▁wrapping",
+ -12.788527488708496
+ ],
+ [
+ "schrieb",
+ -12.788652420043945
+ ],
+ [
+ "▁abuz",
+ -12.788718223571777
+ ],
+ [
+ "▁maîtrise",
+ -12.788772583007812
+ ],
+ [
+ "EQ",
+ -12.788887977600098
+ ],
+ [
+ "▁Erinnerung",
+ -12.789095878601074
+ ],
+ [
+ "▁bridal",
+ -12.78909969329834
+ ],
+ [
+ "Rock",
+ -12.789118766784668
+ ],
+ [
+ "▁copied",
+ -12.789193153381348
+ ],
+ [
+ "Met",
+ -12.789206504821777
+ ],
+ [
+ "▁incep",
+ -12.789233207702637
+ ],
+ [
+ "▁sinus",
+ -12.789336204528809
+ ],
+ [
+ "▁Felix",
+ -12.789831161499023
+ ],
+ [
+ "▁Deluxe",
+ -12.789837837219238
+ ],
+ [
+ "▁GPU",
+ -12.789848327636719
+ ],
+ [
+ "Sie",
+ -12.790164947509766
+ ],
+ [
+ "lowering",
+ -12.790262222290039
+ ],
+ [
+ "▁Trotz",
+ -12.790282249450684
+ ],
+ [
+ "333",
+ -12.790417671203613
+ ],
+ [
+ "withstand",
+ -12.79055118560791
+ ],
+ [
+ "▁Aufenthalt",
+ -12.790566444396973
+ ],
+ [
+ "▁unhealthy",
+ -12.790567398071289
+ ],
+ [
+ "▁urbain",
+ -12.790573120117188
+ ],
+ [
+ "▁LOL",
+ -12.790702819824219
+ ],
+ [
+ "▁Ballet",
+ -12.79074478149414
+ ],
+ [
+ "▁Decoration",
+ -12.79083251953125
+ ],
+ [
+ "weist",
+ -12.790839195251465
+ ],
+ [
+ "▁Residence",
+ -12.790932655334473
+ ],
+ [
+ "▁Leeds",
+ -12.791055679321289
+ ],
+ [
+ "▁Genau",
+ -12.791084289550781
+ ],
+ [
+ "Imagin",
+ -12.791136741638184
+ ],
+ [
+ "▁suspicion",
+ -12.791300773620605
+ ],
+ [
+ "▁pêche",
+ -12.791301727294922
+ ],
+ [
+ "▁Soccer",
+ -12.791306495666504
+ ],
+ [
+ "▁protectie",
+ -12.791553497314453
+ ],
+ [
+ "ATS",
+ -12.791796684265137
+ ],
+ [
+ "stocked",
+ -12.791838645935059
+ ],
+ [
+ "▁gymnas",
+ -12.79184627532959
+ ],
+ [
+ "ASP",
+ -12.792027473449707
+ ],
+ [
+ "▁Independence",
+ -12.792037010192871
+ ],
+ [
+ "▁Wizard",
+ -12.792037963867188
+ ],
+ [
+ "▁nitrogen",
+ -12.79204273223877
+ ],
+ [
+ "amerikanische",
+ -12.7920503616333
+ ],
+ [
+ "▁Indianapolis",
+ -12.79205322265625
+ ],
+ [
+ "catches",
+ -12.792131423950195
+ ],
+ [
+ "stria",
+ -12.792275428771973
+ ],
+ [
+ "schätze",
+ -12.79235553741455
+ ],
+ [
+ "▁Räume",
+ -12.792387962341309
+ ],
+ [
+ "▁Interesting",
+ -12.792403221130371
+ ],
+ [
+ "bürger",
+ -12.79240608215332
+ ],
+ [
+ "sweet",
+ -12.792410850524902
+ ],
+ [
+ "Identify",
+ -12.792632102966309
+ ],
+ [
+ "EEN",
+ -12.792651176452637
+ ],
+ [
+ "▁£3",
+ -12.792654991149902
+ ],
+ [
+ "interacting",
+ -12.7926664352417
+ ],
+ [
+ "NYSE",
+ -12.792762756347656
+ ],
+ [
+ "▁Dynamics",
+ -12.79277515411377
+ ],
+ [
+ "▁modificări",
+ -12.792777061462402
+ ],
+ [
+ "▁Kumar",
+ -12.792936325073242
+ ],
+ [
+ "chette",
+ -12.79313850402832
+ ],
+ [
+ "▁presiune",
+ -12.79316234588623
+ ],
+ [
+ "arni",
+ -12.793164253234863
+ ],
+ [
+ "▁vielfältig",
+ -12.793221473693848
+ ],
+ [
+ "KC",
+ -12.793259620666504
+ ],
+ [
+ "▁Cuisine",
+ -12.793513298034668
+ ],
+ [
+ "▁australia",
+ -12.793885231018066
+ ],
+ [
+ "▁încet",
+ -12.794026374816895
+ ],
+ [
+ "▁caracteristic",
+ -12.794257164001465
+ ],
+ [
+ "▁cookbook",
+ -12.794501304626465
+ ],
+ [
+ "▁douleur",
+ -12.79453182220459
+ ],
+ [
+ "AVI",
+ -12.794593811035156
+ ],
+ [
+ "artikel",
+ -12.794740676879883
+ ],
+ [
+ "feta",
+ -12.79493522644043
+ ],
+ [
+ "▁fréquent",
+ -12.794987678527832
+ ],
+ [
+ "▁Prophet",
+ -12.795051574707031
+ ],
+ [
+ "▁dépense",
+ -12.795202255249023
+ ],
+ [
+ "▁Smile",
+ -12.795235633850098
+ ],
+ [
+ "▁lawmakers",
+ -12.79525375366211
+ ],
+ [
+ "▁Kollegen",
+ -12.795391082763672
+ ],
+ [
+ "▁Pir",
+ -12.79555606842041
+ ],
+ [
+ "serez",
+ -12.79561710357666
+ ],
+ [
+ "▁consumator",
+ -12.795656204223633
+ ],
+ [
+ "▁playlist",
+ -12.795730590820312
+ ],
+ [
+ "▁envisage",
+ -12.795733451843262
+ ],
+ [
+ "swept",
+ -12.795780181884766
+ ],
+ [
+ "▁Grim",
+ -12.795825004577637
+ ],
+ [
+ "▁widow",
+ -12.795836448669434
+ ],
+ [
+ "authorised",
+ -12.795886039733887
+ ],
+ [
+ "▁(...)",
+ -12.796035766601562
+ ],
+ [
+ "▁photographic",
+ -12.796060562133789
+ ],
+ [
+ "▁libertate",
+ -12.796173095703125
+ ],
+ [
+ "▁principalement",
+ -12.796201705932617
+ ],
+ [
+ "umming",
+ -12.796260833740234
+ ],
+ [
+ "▁Montréal",
+ -12.796465873718262
+ ],
+ [
+ "▁compilation",
+ -12.796468734741211
+ ],
+ [
+ "▁erlaubt",
+ -12.79647159576416
+ ],
+ [
+ "▁biblical",
+ -12.796518325805664
+ ],
+ [
+ "volume",
+ -12.796561241149902
+ ],
+ [
+ "5-7",
+ -12.796809196472168
+ ],
+ [
+ "▁Versch",
+ -12.79689884185791
+ ],
+ [
+ "▁Shark",
+ -12.796957015991211
+ ],
+ [
+ "ologne",
+ -12.796969413757324
+ ],
+ [
+ "4.4",
+ -12.797086715698242
+ ],
+ [
+ "decken",
+ -12.797112464904785
+ ],
+ [
+ "▁frequencies",
+ -12.797205924987793
+ ],
+ [
+ "▁inferior",
+ -12.79720687866211
+ ],
+ [
+ "visible",
+ -12.797321319580078
+ ],
+ [
+ "▁educator",
+ -12.797394752502441
+ ],
+ [
+ "▁soziale",
+ -12.797420501708984
+ ],
+ [
+ "▁billet",
+ -12.797523498535156
+ ],
+ [
+ "folosirea",
+ -12.797574996948242
+ ],
+ [
+ "▁aufgenommen",
+ -12.797590255737305
+ ],
+ [
+ "▁Thread",
+ -12.797649383544922
+ ],
+ [
+ "registering",
+ -12.797694206237793
+ ],
+ [
+ "▁Loop",
+ -12.797747611999512
+ ],
+ [
+ "innovation",
+ -12.79783821105957
+ ],
+ [
+ "▁elimination",
+ -12.797857284545898
+ ],
+ [
+ "136",
+ -12.797883987426758
+ ],
+ [
+ "▁fluctu",
+ -12.797892570495605
+ ],
+ [
+ "▁Mercury",
+ -12.79794692993164
+ ],
+ [
+ "▁bouche",
+ -12.797955513000488
+ ],
+ [
+ "▁hurdle",
+ -12.7979736328125
+ ],
+ [
+ "▁Bennett",
+ -12.798040390014648
+ ],
+ [
+ "STI",
+ -12.79818344116211
+ ],
+ [
+ "▁théâtre",
+ -12.798316955566406
+ ],
+ [
+ "▁confortable",
+ -12.798359870910645
+ ],
+ [
+ "▁Automobil",
+ -12.79838752746582
+ ],
+ [
+ "▁Donna",
+ -12.798399925231934
+ ],
+ [
+ "▁foyer",
+ -12.79841136932373
+ ],
+ [
+ "▁hollow",
+ -12.798465728759766
+ ],
+ [
+ "▁règlement",
+ -12.79861068725586
+ ],
+ [
+ "effi",
+ -12.798616409301758
+ ],
+ [
+ "▁sediment",
+ -12.79869270324707
+ ],
+ [
+ "▁Mä",
+ -12.798774719238281
+ ],
+ [
+ "▁faint",
+ -12.798833847045898
+ ],
+ [
+ "feti",
+ -12.79890251159668
+ ],
+ [
+ "▁Concord",
+ -12.798959732055664
+ ],
+ [
+ "▁Ladies",
+ -12.798990249633789
+ ],
+ [
+ "▁pregatit",
+ -12.799052238464355
+ ],
+ [
+ "▁Ensemble",
+ -12.79905891418457
+ ],
+ [
+ "▁Ingredient",
+ -12.79905891418457
+ ],
+ [
+ "▁Respond",
+ -12.79914379119873
+ ],
+ [
+ "▁impaired",
+ -12.799356460571289
+ ],
+ [
+ "▁Feedback",
+ -12.799430847167969
+ ],
+ [
+ "▁ultrasound",
+ -12.799461364746094
+ ],
+ [
+ "▁Guvernului",
+ -12.799617767333984
+ ],
+ [
+ "▁Unterricht",
+ -12.799654006958008
+ ],
+ [
+ "▁prosecut",
+ -12.799662590026855
+ ],
+ [
+ "spend",
+ -12.799732208251953
+ ],
+ [
+ "▁capitol",
+ -12.799800872802734
+ ],
+ [
+ "USD",
+ -12.799822807312012
+ ],
+ [
+ "observing",
+ -12.799947738647461
+ ],
+ [
+ "▁effortlessly",
+ -12.800045013427734
+ ],
+ [
+ "▁Setting",
+ -12.80010986328125
+ ],
+ [
+ "▁spontaneous",
+ -12.80020809173584
+ ],
+ [
+ "▁LEGO",
+ -12.800238609313965
+ ],
+ [
+ "initiative",
+ -12.800299644470215
+ ],
+ [
+ "▁Sak",
+ -12.800299644470215
+ ],
+ [
+ "Interestingly",
+ -12.800326347351074
+ ],
+ [
+ "▁Yale",
+ -12.800352096557617
+ ],
+ [
+ "▁größer",
+ -12.80038070678711
+ ],
+ [
+ "RIC",
+ -12.800406455993652
+ ],
+ [
+ "▁distracted",
+ -12.800436973571777
+ ],
+ [
+ "drafted",
+ -12.800484657287598
+ ],
+ [
+ "▁Brenda",
+ -12.800522804260254
+ ],
+ [
+ "monopol",
+ -12.800551414489746
+ ],
+ [
+ "städt",
+ -12.800580024719238
+ ],
+ [
+ "▁altar",
+ -12.80058765411377
+ ],
+ [
+ "▁Hannover",
+ -12.800596237182617
+ ],
+ [
+ "▁Spiritual",
+ -12.800702095031738
+ ],
+ [
+ "▁thriller",
+ -12.800747871398926
+ ],
+ [
+ "▁Schneider",
+ -12.800760269165039
+ ],
+ [
+ "▁accumulate",
+ -12.800817489624023
+ ],
+ [
+ "▁mediului",
+ -12.800822257995605
+ ],
+ [
+ "▁Mathematics",
+ -12.800914764404297
+ ],
+ [
+ "▁paradox",
+ -12.800986289978027
+ ],
+ [
+ "▁Sham",
+ -12.801230430603027
+ ],
+ [
+ "▁SITE",
+ -12.801375389099121
+ ],
+ [
+ "▁echipei",
+ -12.801508903503418
+ ],
+ [
+ "▁staircase",
+ -12.801660537719727
+ ],
+ [
+ "▁întrebări",
+ -12.801705360412598
+ ],
+ [
+ "Commerce",
+ -12.802020072937012
+ ],
+ [
+ "▁selfie",
+ -12.802353858947754
+ ],
+ [
+ "▁Pocket",
+ -12.802404403686523
+ ],
+ [
+ "▁niemand",
+ -12.80263614654541
+ ],
+ [
+ "Tool",
+ -12.802678108215332
+ ],
+ [
+ "igma",
+ -12.802695274353027
+ ],
+ [
+ "utilisant",
+ -12.802915573120117
+ ],
+ [
+ "▁negatively",
+ -12.80295181274414
+ ],
+ [
+ "Secondly",
+ -12.802955627441406
+ ],
+ [
+ "▁ROI",
+ -12.8030366897583
+ ],
+ [
+ "Arch",
+ -12.803121566772461
+ ],
+ [
+ "▁continuity",
+ -12.80318546295166
+ ],
+ [
+ "▁Prayer",
+ -12.803235054016113
+ ],
+ [
+ "inverse",
+ -12.803241729736328
+ ],
+ [
+ "▁Himmel",
+ -12.803336143493652
+ ],
+ [
+ "prinz",
+ -12.803478240966797
+ ],
+ [
+ "wichtigen",
+ -12.803496360778809
+ ],
+ [
+ "étage",
+ -12.803522109985352
+ ],
+ [
+ "summe",
+ -12.8036527633667
+ ],
+ [
+ "▁Zeitung",
+ -12.80366039276123
+ ],
+ [
+ "▁realization",
+ -12.803897857666016
+ ],
+ [
+ "▁influent",
+ -12.804291725158691
+ ],
+ [
+ "▁Valid",
+ -12.804357528686523
+ ],
+ [
+ "▁publicity",
+ -12.804439544677734
+ ],
+ [
+ "▁vertreten",
+ -12.804447174072266
+ ],
+ [
+ "▁Shoes",
+ -12.804609298706055
+ ],
+ [
+ "▁Diabetes",
+ -12.80463695526123
+ ],
+ [
+ "▁anticipation",
+ -12.804670333862305
+ ],
+ [
+ "▁Blank",
+ -12.8047456741333
+ ],
+ [
+ "asked",
+ -12.804899215698242
+ ],
+ [
+ "Power",
+ -12.804938316345215
+ ],
+ [
+ "arrelage",
+ -12.805140495300293
+ ],
+ [
+ "▁appraisal",
+ -12.80538272857666
+ ],
+ [
+ "▁harassment",
+ -12.805542945861816
+ ],
+ [
+ "Anzeige",
+ -12.805682182312012
+ ],
+ [
+ "liners",
+ -12.80584716796875
+ ],
+ [
+ "Firstly",
+ -12.805851936340332
+ ],
+ [
+ "transferring",
+ -12.805951118469238
+ ],
+ [
+ "▁Diane",
+ -12.806012153625488
+ ],
+ [
+ "▁1/2\"",
+ -12.80606746673584
+ ],
+ [
+ "▁adrenal",
+ -12.806131362915039
+ ],
+ [
+ "▁Prague",
+ -12.806208610534668
+ ],
+ [
+ "insertion",
+ -12.80635929107666
+ ],
+ [
+ "▁Fahrer",
+ -12.806465148925781
+ ],
+ [
+ "▁divin",
+ -12.806585311889648
+ ],
+ [
+ "▁douche",
+ -12.80673885345459
+ ],
+ [
+ "▁meticulous",
+ -12.806879043579102
+ ],
+ [
+ "▁IEEE",
+ -12.806981086730957
+ ],
+ [
+ "▁Rabatt",
+ -12.807259559631348
+ ],
+ [
+ "Runner",
+ -12.807342529296875
+ ],
+ [
+ "▁Leder",
+ -12.807429313659668
+ ],
+ [
+ "project",
+ -12.80745792388916
+ ],
+ [
+ "▁Split",
+ -12.807562828063965
+ ],
+ [
+ "Gold",
+ -12.807600021362305
+ ],
+ [
+ "5.00",
+ -12.807629585266113
+ ],
+ [
+ "iola",
+ -12.807655334472656
+ ],
+ [
+ "standardized",
+ -12.807890892028809
+ ],
+ [
+ "ordination",
+ -12.807984352111816
+ ],
+ [
+ "▁Egal",
+ -12.808158874511719
+ ],
+ [
+ "▁ruhig",
+ -12.808241844177246
+ ],
+ [
+ "▁judiciar",
+ -12.80837345123291
+ ],
+ [
+ "▁Nowadays",
+ -12.808374404907227
+ ],
+ [
+ "▁whistle",
+ -12.808374404907227
+ ],
+ [
+ "▁superhero",
+ -12.808379173278809
+ ],
+ [
+ "▁PowerPoint",
+ -12.808408737182617
+ ],
+ [
+ "flop",
+ -12.808420181274414
+ ],
+ [
+ "olph",
+ -12.808460235595703
+ ],
+ [
+ "▁pallet",
+ -12.808916091918945
+ ],
+ [
+ "posons",
+ -12.809005737304688
+ ],
+ [
+ "▁Listing",
+ -12.809032440185547
+ ],
+ [
+ "Tag",
+ -12.809075355529785
+ ],
+ [
+ "introductory",
+ -12.809122085571289
+ ],
+ [
+ "▁Profil",
+ -12.809123992919922
+ ],
+ [
+ "symmetric",
+ -12.809126853942871
+ ],
+ [
+ "▁aisle",
+ -12.809138298034668
+ ],
+ [
+ "▁ajouté",
+ -12.809147834777832
+ ],
+ [
+ "opathy",
+ -12.809149742126465
+ ],
+ [
+ "prezentate",
+ -12.809155464172363
+ ],
+ [
+ "▁hurry",
+ -12.809165000915527
+ ],
+ [
+ "Auth",
+ -12.809310913085938
+ ],
+ [
+ "▁Homepage",
+ -12.809435844421387
+ ],
+ [
+ "ashes",
+ -12.809489250183105
+ ],
+ [
+ "▁inklusive",
+ -12.809496879577637
+ ],
+ [
+ "populated",
+ -12.809502601623535
+ ],
+ [
+ "▁nein",
+ -12.809554100036621
+ ],
+ [
+ "▁syndicat",
+ -12.809690475463867
+ ],
+ [
+ "▁développé",
+ -12.809842109680176
+ ],
+ [
+ "▁Domestic",
+ -12.809877395629883
+ ],
+ [
+ "essay",
+ -12.809967994689941
+ ],
+ [
+ "Atelier",
+ -12.809980392456055
+ ],
+ [
+ "▁proceeding",
+ -12.810006141662598
+ ],
+ [
+ "▁SAS",
+ -12.810038566589355
+ ],
+ [
+ "task",
+ -12.810063362121582
+ ],
+ [
+ "▁blackjack",
+ -12.810114860534668
+ ],
+ [
+ "Key",
+ -12.810186386108398
+ ],
+ [
+ "thérapie",
+ -12.810247421264648
+ ],
+ [
+ "▁Cohen",
+ -12.810397148132324
+ ],
+ [
+ "Direct",
+ -12.810510635375977
+ ],
+ [
+ "▁Estimat",
+ -12.810517311096191
+ ],
+ [
+ "élève",
+ -12.810616493225098
+ ],
+ [
+ "cind",
+ -12.810640335083008
+ ],
+ [
+ "▁prezenț",
+ -12.810701370239258
+ ],
+ [
+ "▁notorious",
+ -12.810725212097168
+ ],
+ [
+ "climbed",
+ -12.810816764831543
+ ],
+ [
+ "▁flexibil",
+ -12.810830116271973
+ ],
+ [
+ "▁entlang",
+ -12.810855865478516
+ ],
+ [
+ "longed",
+ -12.81103515625
+ ],
+ [
+ "▁elbow",
+ -12.811078071594238
+ ],
+ [
+ "BH",
+ -12.811296463012695
+ ],
+ [
+ "▁Radu",
+ -12.811376571655273
+ ],
+ [
+ "▁lonely",
+ -12.811378479003906
+ ],
+ [
+ "ALA",
+ -12.811405181884766
+ ],
+ [
+ "Variante",
+ -12.811639785766602
+ ],
+ [
+ "▁Influen",
+ -12.81169319152832
+ ],
+ [
+ "▁Budapest",
+ -12.811747550964355
+ ],
+ [
+ "▁Gemüse",
+ -12.811747550964355
+ ],
+ [
+ "▁continental",
+ -12.811750411987305
+ ],
+ [
+ "ippo",
+ -12.811771392822266
+ ],
+ [
+ "▁Affordable",
+ -12.81212329864502
+ ],
+ [
+ "▁niece",
+ -12.812187194824219
+ ],
+ [
+ "oscopic",
+ -12.812190055847168
+ ],
+ [
+ "▁Grid",
+ -12.81222152709961
+ ],
+ [
+ "sliced",
+ -12.812270164489746
+ ],
+ [
+ "▁voici",
+ -12.812294006347656
+ ],
+ [
+ "aveam",
+ -12.812471389770508
+ ],
+ [
+ "▁Lars",
+ -12.812612533569336
+ ],
+ [
+ "APA",
+ -12.812657356262207
+ ],
+ [
+ "▁particulière",
+ -12.812858581542969
+ ],
+ [
+ "sorb",
+ -12.8128662109375
+ ],
+ [
+ "▁1955",
+ -12.812887191772461
+ ],
+ [
+ "▁solutii",
+ -12.812942504882812
+ ],
+ [
+ "loch",
+ -12.812960624694824
+ ],
+ [
+ "▁summon",
+ -12.813212394714355
+ ],
+ [
+ "wurf",
+ -12.813271522521973
+ ],
+ [
+ "▁protecți",
+ -12.813288688659668
+ ],
+ [
+ "2001",
+ -12.813499450683594
+ ],
+ [
+ "▁sophomore",
+ -12.813627243041992
+ ],
+ [
+ "▁Schwerpunkt",
+ -12.813628196716309
+ ],
+ [
+ "▁diplomat",
+ -12.813687324523926
+ ],
+ [
+ "▁artistique",
+ -12.813726425170898
+ ],
+ [
+ "▁accueille",
+ -12.813739776611328
+ ],
+ [
+ "Disp",
+ -12.813746452331543
+ ],
+ [
+ "inherited",
+ -12.813764572143555
+ ],
+ [
+ "▁COMP",
+ -12.813889503479004
+ ],
+ [
+ "▁envoyé",
+ -12.814046859741211
+ ],
+ [
+ "▁tuning",
+ -12.814056396484375
+ ],
+ [
+ "▁entspricht",
+ -12.814062118530273
+ ],
+ [
+ "▁exerc",
+ -12.81406307220459
+ ],
+ [
+ "▁accessoires",
+ -12.8140869140625
+ ],
+ [
+ "▁Automat",
+ -12.814348220825195
+ ],
+ [
+ "importance",
+ -12.814408302307129
+ ],
+ [
+ "▁travellers",
+ -12.814432144165039
+ ],
+ [
+ "seiten",
+ -12.814474105834961
+ ],
+ [
+ "▁slider",
+ -12.814481735229492
+ ],
+ [
+ "effect",
+ -12.814591407775879
+ ],
+ [
+ "▁siding",
+ -12.814669609069824
+ ],
+ [
+ "▁Crit",
+ -12.814780235290527
+ ],
+ [
+ "▁sportif",
+ -12.814827919006348
+ ],
+ [
+ "▁Accessories",
+ -12.81513500213623
+ ],
+ [
+ "▁Anteil",
+ -12.815184593200684
+ ],
+ [
+ "▁limbi",
+ -12.81519603729248
+ ],
+ [
+ "▁vendre",
+ -12.815269470214844
+ ],
+ [
+ "borg",
+ -12.815435409545898
+ ],
+ [
+ "▁Deposit",
+ -12.815508842468262
+ ],
+ [
+ "▁Hö",
+ -12.815717697143555
+ ],
+ [
+ "employé",
+ -12.8157320022583
+ ],
+ [
+ "▁Bangalore",
+ -12.815887451171875
+ ],
+ [
+ "▁itinerary",
+ -12.815888404846191
+ ],
+ [
+ "▁Deliver",
+ -12.816008567810059
+ ],
+ [
+ "dik",
+ -12.816024780273438
+ ],
+ [
+ "▁advent",
+ -12.816100120544434
+ ],
+ [
+ "▁Turk",
+ -12.81614875793457
+ ],
+ [
+ "▁Nico",
+ -12.816154479980469
+ ],
+ [
+ "organizarea",
+ -12.816161155700684
+ ],
+ [
+ "▁remport",
+ -12.816166877746582
+ ],
+ [
+ "▁tribunal",
+ -12.816266059875488
+ ],
+ [
+ "▁Rusia",
+ -12.8162841796875
+ ],
+ [
+ "glazed",
+ -12.816339492797852
+ ],
+ [
+ "▁destiné",
+ -12.816502571105957
+ ],
+ [
+ "304",
+ -12.816533088684082
+ ],
+ [
+ "album",
+ -12.816650390625
+ ],
+ [
+ "▁junction",
+ -12.81665325164795
+ ],
+ [
+ "▁Fleet",
+ -12.816664695739746
+ ],
+ [
+ "venant",
+ -12.81667423248291
+ ],
+ [
+ "▁buddy",
+ -12.816694259643555
+ ],
+ [
+ "▁neglected",
+ -12.816694259643555
+ ],
+ [
+ "▁Mask",
+ -12.816783905029297
+ ],
+ [
+ "▁testament",
+ -12.816844940185547
+ ],
+ [
+ "▁Basil",
+ -12.81690788269043
+ ],
+ [
+ "masă",
+ -12.816922187805176
+ ],
+ [
+ "▁racist",
+ -12.81692886352539
+ ],
+ [
+ "640",
+ -12.816990852355957
+ ],
+ [
+ "▁Standing",
+ -12.817028045654297
+ ],
+ [
+ "▁MUST",
+ -12.817266464233398
+ ],
+ [
+ "situation",
+ -12.817327499389648
+ ],
+ [
+ "▁informiert",
+ -12.817337036132812
+ ],
+ [
+ "ABA",
+ -12.817353248596191
+ ],
+ [
+ "▁Timothy",
+ -12.817397117614746
+ ],
+ [
+ "▁generosity",
+ -12.817397117614746
+ ],
+ [
+ "▁erscheint",
+ -12.817402839660645
+ ],
+ [
+ "▁verarbeitet",
+ -12.81740665435791
+ ],
+ [
+ "▁burial",
+ -12.817444801330566
+ ],
+ [
+ "▁limestone",
+ -12.817458152770996
+ ],
+ [
+ "▁1953",
+ -12.817480087280273
+ ],
+ [
+ "▁Lucr",
+ -12.817506790161133
+ ],
+ [
+ "small",
+ -12.817633628845215
+ ],
+ [
+ "aveau",
+ -12.81763744354248
+ ],
+ [
+ "versiune",
+ -12.81773567199707
+ ],
+ [
+ "▁inkl",
+ -12.81775951385498
+ ],
+ [
+ "▁Minneapolis",
+ -12.81777572631836
+ ],
+ [
+ "Spiel",
+ -12.81781005859375
+ ],
+ [
+ "▁encode",
+ -12.817895889282227
+ ],
+ [
+ "▁beforehand",
+ -12.818021774291992
+ ],
+ [
+ "▁Vital",
+ -12.818086624145508
+ ],
+ [
+ "▁socialist",
+ -12.818228721618652
+ ],
+ [
+ "inho",
+ -12.81824779510498
+ ],
+ [
+ "▁chapel",
+ -12.81825065612793
+ ],
+ [
+ "▁Monitoring",
+ -12.81838607788086
+ ],
+ [
+ "▁quotidienne",
+ -12.818404197692871
+ ],
+ [
+ "cloud",
+ -12.818506240844727
+ ],
+ [
+ "▁desfăşur",
+ -12.818531036376953
+ ],
+ [
+ "▁1952",
+ -12.818638801574707
+ ],
+ [
+ "▁Rü",
+ -12.818690299987793
+ ],
+ [
+ "▁Sigma",
+ -12.818804740905762
+ ],
+ [
+ "134",
+ -12.818835258483887
+ ],
+ [
+ "Sullivan",
+ -12.818909645080566
+ ],
+ [
+ "▁Bevölkerung",
+ -12.818909645080566
+ ],
+ [
+ "▁sufficiently",
+ -12.818953514099121
+ ],
+ [
+ "Check",
+ -12.818992614746094
+ ],
+ [
+ "rnie",
+ -12.8190336227417
+ ],
+ [
+ "contamin",
+ -12.819132804870605
+ ],
+ [
+ "▁gewonnen",
+ -12.81928825378418
+ ],
+ [
+ "▁bugetul",
+ -12.819376945495605
+ ],
+ [
+ "▁mustard",
+ -12.819414138793945
+ ],
+ [
+ "132",
+ -12.819478988647461
+ ],
+ [
+ "0.9",
+ -12.819535255432129
+ ],
+ [
+ "▁tratat",
+ -12.81957721710205
+ ],
+ [
+ "▁dilemma",
+ -12.819666862487793
+ ],
+ [
+ "▁versatility",
+ -12.819666862487793
+ ],
+ [
+ "▁clutter",
+ -12.819670677185059
+ ],
+ [
+ "▁Musk",
+ -12.81973934173584
+ ],
+ [
+ "▁Beide",
+ -12.819750785827637
+ ],
+ [
+ "hurst",
+ -12.819758415222168
+ ],
+ [
+ "atsu",
+ -12.819767951965332
+ ],
+ [
+ "absence",
+ -12.819784164428711
+ ],
+ [
+ "rebounds",
+ -12.819881439208984
+ ],
+ [
+ "6.1",
+ -12.820029258728027
+ ],
+ [
+ "Dia",
+ -12.820046424865723
+ ],
+ [
+ "▁siguranță",
+ -12.820060729980469
+ ],
+ [
+ "▁Blade",
+ -12.820072174072266
+ ],
+ [
+ "▁disrupt",
+ -12.820074081420898
+ ],
+ [
+ "▁visiteurs",
+ -12.820169448852539
+ ],
+ [
+ "tested",
+ -12.820282936096191
+ ],
+ [
+ "▁Lup",
+ -12.820353507995605
+ ],
+ [
+ "▁Rouge",
+ -12.820371627807617
+ ],
+ [
+ "▁asbestos",
+ -12.82042407989502
+ ],
+ [
+ "▁moisturize",
+ -12.820427894592285
+ ],
+ [
+ "▁acknowledg",
+ -12.82045841217041
+ ],
+ [
+ "▁procent",
+ -12.820467948913574
+ ],
+ [
+ "▁swear",
+ -12.82050895690918
+ ],
+ [
+ "▁911",
+ -12.820647239685059
+ ],
+ [
+ "präsent",
+ -12.820724487304688
+ ],
+ [
+ "▁cohort",
+ -12.82072639465332
+ ],
+ [
+ "▁intimid",
+ -12.820830345153809
+ ],
+ [
+ "JS",
+ -12.820849418640137
+ ],
+ [
+ "îm",
+ -12.82096004486084
+ ],
+ [
+ "▁Kunststoff",
+ -12.820963859558105
+ ],
+ [
+ "rison",
+ -12.820972442626953
+ ],
+ [
+ "▁praf",
+ -12.82097339630127
+ ],
+ [
+ "▁convient",
+ -12.821019172668457
+ ],
+ [
+ "▁partenaire",
+ -12.821088790893555
+ ],
+ [
+ "▁Verantwortlich",
+ -12.821182250976562
+ ],
+ [
+ "▁semiconductor",
+ -12.821182250976562
+ ],
+ [
+ "▁kürz",
+ -12.821187019348145
+ ],
+ [
+ "▁Bottom",
+ -12.821187973022461
+ ],
+ [
+ "▁tratamentul",
+ -12.82127571105957
+ ],
+ [
+ "Source",
+ -12.821331024169922
+ ],
+ [
+ "authored",
+ -12.82172679901123
+ ],
+ [
+ "robo",
+ -12.821867942810059
+ ],
+ [
+ "▁turf",
+ -12.82194709777832
+ ],
+ [
+ "▁liebe",
+ -12.821971893310547
+ ],
+ [
+ "▁Fotografi",
+ -12.821995735168457
+ ],
+ [
+ "Big",
+ -12.822064399719238
+ ],
+ [
+ "▁fireworks",
+ -12.822081565856934
+ ],
+ [
+ "▁presă",
+ -12.822135925292969
+ ],
+ [
+ "▁conceal",
+ -12.822269439697266
+ ],
+ [
+ "▁originated",
+ -12.82227897644043
+ ],
+ [
+ "▁biciclet",
+ -12.822319984436035
+ ],
+ [
+ "acești",
+ -12.822577476501465
+ ],
+ [
+ "▁mortar",
+ -12.822585105895996
+ ],
+ [
+ "▁Wunder",
+ -12.822626113891602
+ ],
+ [
+ "ionist",
+ -12.822696685791016
+ ],
+ [
+ "KM",
+ -12.822871208190918
+ ],
+ [
+ "▁Marion",
+ -12.822918891906738
+ ],
+ [
+ "produkte",
+ -12.822933197021484
+ ],
+ [
+ "▁Sprint",
+ -12.822999000549316
+ ],
+ [
+ "▁Nachde",
+ -12.8230619430542
+ ],
+ [
+ "▁verfüge",
+ -12.823100090026855
+ ],
+ [
+ "Marea",
+ -12.823177337646484
+ ],
+ [
+ "▁compressor",
+ -12.823253631591797
+ ],
+ [
+ "Arm",
+ -12.823290824890137
+ ],
+ [
+ "Auf",
+ -12.823311805725098
+ ],
+ [
+ "▁Polyester",
+ -12.823461532592773
+ ],
+ [
+ "▁Sheffield",
+ -12.823461532592773
+ ],
+ [
+ "illiard",
+ -12.823494911193848
+ ],
+ [
+ "▁misleading",
+ -12.82353401184082
+ ],
+ [
+ "multi",
+ -12.823749542236328
+ ],
+ [
+ "ripped",
+ -12.82381820678711
+ ],
+ [
+ "▁Cosmetic",
+ -12.82383918762207
+ ],
+ [
+ "▁Regal",
+ -12.823890686035156
+ ],
+ [
+ "▁authenticity",
+ -12.82414436340332
+ ],
+ [
+ "▁customizable",
+ -12.824219703674316
+ ],
+ [
+ "▁bathtub",
+ -12.824275016784668
+ ],
+ [
+ "▁Average",
+ -12.824292182922363
+ ],
+ [
+ "▁Muster",
+ -12.824522018432617
+ ],
+ [
+ "290",
+ -12.824529647827148
+ ],
+ [
+ "▁Ersatz",
+ -12.824570655822754
+ ],
+ [
+ "▁Might",
+ -12.824588775634766
+ ],
+ [
+ "published",
+ -12.82461929321289
+ ],
+ [
+ "▁Interpret",
+ -12.824640274047852
+ ],
+ [
+ "▁încep",
+ -12.82480239868164
+ ],
+ [
+ "▁proto",
+ -12.824851036071777
+ ],
+ [
+ "▁disque",
+ -12.824889183044434
+ ],
+ [
+ "▁Palestine",
+ -12.824980735778809
+ ],
+ [
+ "Over",
+ -12.824981689453125
+ ],
+ [
+ "▁verbessert",
+ -12.824983596801758
+ ],
+ [
+ "▁liefern",
+ -12.825017929077148
+ ],
+ [
+ "▁Handlung",
+ -12.825095176696777
+ ],
+ [
+ "▁Handels",
+ -12.825150489807129
+ ],
+ [
+ "▁eater",
+ -12.825201988220215
+ ],
+ [
+ "▁$40",
+ -12.825251579284668
+ ],
+ [
+ "illard",
+ -12.825334548950195
+ ],
+ [
+ "▁apariti",
+ -12.825413703918457
+ ],
+ [
+ "▁gag",
+ -12.825422286987305
+ ],
+ [
+ "▁chimic",
+ -12.825541496276855
+ ],
+ [
+ "▁Guru",
+ -12.825594902038574
+ ],
+ [
+ "▁Toilet",
+ -12.82571792602539
+ ],
+ [
+ "▁Tochter",
+ -12.825748443603516
+ ],
+ [
+ "▁Aurora",
+ -12.82579231262207
+ ],
+ [
+ "contro",
+ -12.825922966003418
+ ],
+ [
+ "▁GOP",
+ -12.825995445251465
+ ],
+ [
+ "Provence",
+ -12.826130867004395
+ ],
+ [
+ "▁Frieden",
+ -12.82614803314209
+ ],
+ [
+ "ăci",
+ -12.826216697692871
+ ],
+ [
+ "portée",
+ -12.826268196105957
+ ],
+ [
+ "▁upright",
+ -12.826300621032715
+ ],
+ [
+ "▁Physician",
+ -12.82650375366211
+ ],
+ [
+ "▁juridique",
+ -12.82650375366211
+ ],
+ [
+ "▁territorial",
+ -12.82650375366211
+ ],
+ [
+ "▁kindergarten",
+ -12.826505661010742
+ ],
+ [
+ "aéroport",
+ -12.826510429382324
+ ],
+ [
+ "▁whisper",
+ -12.826513290405273
+ ],
+ [
+ "▁capacities",
+ -12.826562881469727
+ ],
+ [
+ "dichte",
+ -12.826641082763672
+ ],
+ [
+ "▁Grenzen",
+ -12.826822280883789
+ ],
+ [
+ "▁Riv",
+ -12.82710075378418
+ ],
+ [
+ "épreuve",
+ -12.827266693115234
+ ],
+ [
+ "▁Scheme",
+ -12.827290534973145
+ ],
+ [
+ "mesures",
+ -12.827330589294434
+ ],
+ [
+ "▁Einfluss",
+ -12.827333450317383
+ ],
+ [
+ "appui",
+ -12.827713966369629
+ ],
+ [
+ "▁apuc",
+ -12.827827453613281
+ ],
+ [
+ "▁radiat",
+ -12.82794189453125
+ ],
+ [
+ "▁allergy",
+ -12.828035354614258
+ ],
+ [
+ "▁spear",
+ -12.828038215637207
+ ],
+ [
+ "▁Luxembourg",
+ -12.828086853027344
+ ],
+ [
+ "▁Registered",
+ -12.828115463256836
+ ],
+ [
+ "▁Shape",
+ -12.828198432922363
+ ],
+ [
+ "genie",
+ -12.828328132629395
+ ],
+ [
+ "nsonsten",
+ -12.828385353088379
+ ],
+ [
+ "▁Symposium",
+ -12.828412055969238
+ ],
+ [
+ "forderung",
+ -12.828474998474121
+ ],
+ [
+ "▁personalizat",
+ -12.82866096496582
+ ],
+ [
+ "▁ştiu",
+ -12.82875919342041
+ ],
+ [
+ "blatt",
+ -12.828804016113281
+ ],
+ [
+ "▁geometry",
+ -12.828807830810547
+ ],
+ [
+ "▁8:30",
+ -12.828831672668457
+ ],
+ [
+ "▁Fahrrad",
+ -12.828861236572266
+ ],
+ [
+ "After",
+ -12.828927040100098
+ ],
+ [
+ "▁ventilat",
+ -12.829072952270508
+ ],
+ [
+ "▁nylon",
+ -12.829190254211426
+ ],
+ [
+ "▁verkauft",
+ -12.829304695129395
+ ],
+ [
+ "öß",
+ -12.829345703125
+ ],
+ [
+ "▁Kath",
+ -12.829523086547852
+ ],
+ [
+ "▁Nuclear",
+ -12.829558372497559
+ ],
+ [
+ "▁Verizon",
+ -12.829560279846191
+ ],
+ [
+ "▁spokesperson",
+ -12.829560279846191
+ ],
+ [
+ "▁vietii",
+ -12.829560279846191
+ ],
+ [
+ "▁prescri",
+ -12.829629898071289
+ ],
+ [
+ "ру",
+ -12.829666137695312
+ ],
+ [
+ "6.2",
+ -12.829801559448242
+ ],
+ [
+ "▁spațiu",
+ -12.830018997192383
+ ],
+ [
+ "▁solvent",
+ -12.83006763458252
+ ],
+ [
+ ",000,000",
+ -12.830142974853516
+ ],
+ [
+ "reuen",
+ -12.830185890197754
+ ],
+ [
+ "plast",
+ -12.830245018005371
+ ],
+ [
+ "▁Activities",
+ -12.830334663391113
+ ],
+ [
+ "▁domni",
+ -12.83056926727295
+ ],
+ [
+ "▁trophy",
+ -12.830572128295898
+ ],
+ [
+ "▁saddle",
+ -12.830657958984375
+ ],
+ [
+ "▁renovat",
+ -12.830708503723145
+ ],
+ [
+ "▁bumper",
+ -12.830717086791992
+ ],
+ [
+ "▁penny",
+ -12.830741882324219
+ ],
+ [
+ "omato",
+ -12.830743789672852
+ ],
+ [
+ "AQ",
+ -12.83083438873291
+ ],
+ [
+ "kunst",
+ -12.830843925476074
+ ],
+ [
+ "hydrat",
+ -12.830860137939453
+ ],
+ [
+ "minder",
+ -12.830931663513184
+ ],
+ [
+ "trecerea",
+ -12.830949783325195
+ ],
+ [
+ "brush",
+ -12.831185340881348
+ ],
+ [
+ "TEC",
+ -12.83121395111084
+ ],
+ [
+ "Please",
+ -12.831253051757812
+ ],
+ [
+ "hydrated",
+ -12.831483840942383
+ ],
+ [
+ "ICAL",
+ -12.831636428833008
+ ],
+ [
+ "trauen",
+ -12.831639289855957
+ ],
+ [
+ "9,000",
+ -12.83175277709961
+ ],
+ [
+ "▁2030",
+ -12.831830024719238
+ ],
+ [
+ "▁Chennai",
+ -12.831854820251465
+ ],
+ [
+ "▁empirical",
+ -12.831854820251465
+ ],
+ [
+ "▁Subscribe",
+ -12.83206844329834
+ ],
+ [
+ "▁vorgestellt",
+ -12.832120895385742
+ ],
+ [
+ "▁Springfield",
+ -12.832159996032715
+ ],
+ [
+ "▁continuu",
+ -12.832311630249023
+ ],
+ [
+ "208",
+ -12.832351684570312
+ ],
+ [
+ "▁Bearing",
+ -12.83240795135498
+ ],
+ [
+ "2003",
+ -12.832572937011719
+ ],
+ [
+ "cheta",
+ -12.832608222961426
+ ],
+ [
+ "▁empathy",
+ -12.832623481750488
+ ],
+ [
+ "▁Alert",
+ -12.832817077636719
+ ],
+ [
+ "▁recreate",
+ -12.832879066467285
+ ],
+ [
+ "PJ",
+ -12.833159446716309
+ ],
+ [
+ "Name",
+ -12.83323860168457
+ ],
+ [
+ "▁Mouse",
+ -12.833405494689941
+ ],
+ [
+ "▁disturbing",
+ -12.833443641662598
+ ],
+ [
+ "▁leichter",
+ -12.83344841003418
+ ],
+ [
+ "▁cruel",
+ -12.833507537841797
+ ],
+ [
+ "▁detective",
+ -12.833531379699707
+ ],
+ [
+ "▁reimbursement",
+ -12.833626747131348
+ ],
+ [
+ "▁Gemeinschaft",
+ -12.833772659301758
+ ],
+ [
+ "▁adolescents",
+ -12.833772659301758
+ ],
+ [
+ "▁Reality",
+ -12.833954811096191
+ ],
+ [
+ "▁Stockholm",
+ -12.83415699005127
+ ],
+ [
+ "▁Gründen",
+ -12.834304809570312
+ ],
+ [
+ "▁Reflect",
+ -12.83432388305664
+ ],
+ [
+ "▁Palmer",
+ -12.834336280822754
+ ],
+ [
+ "▁treac",
+ -12.8343505859375
+ ],
+ [
+ "▁tentative",
+ -12.834497451782227
+ ],
+ [
+ "▁surrender",
+ -12.834677696228027
+ ],
+ [
+ "▁broadly",
+ -12.834734916687012
+ ],
+ [
+ "▁județ",
+ -12.834814071655273
+ ],
+ [
+ "▁Thu",
+ -12.834845542907715
+ ],
+ [
+ "wärts",
+ -12.834961891174316
+ ],
+ [
+ "▁crește",
+ -12.835074424743652
+ ],
+ [
+ "▁déplacement",
+ -12.835208892822266
+ ],
+ [
+ "blanc",
+ -12.835268020629883
+ ],
+ [
+ "▁£5",
+ -12.835308074951172
+ ],
+ [
+ "▁confidentiality",
+ -12.835320472717285
+ ],
+ [
+ "veraging",
+ -12.835444450378418
+ ],
+ [
+ "unité",
+ -12.835609436035156
+ ],
+ [
+ "clar",
+ -12.83564567565918
+ ],
+ [
+ "rigg",
+ -12.835693359375
+ ],
+ [
+ "honneur",
+ -12.835694313049316
+ ],
+ [
+ "▁adventurous",
+ -12.835694313049316
+ ],
+ [
+ "▁Nutzen",
+ -12.835758209228516
+ ],
+ [
+ "▁Kabel",
+ -12.835800170898438
+ ],
+ [
+ "empowering",
+ -12.836040496826172
+ ],
+ [
+ "verhalten",
+ -12.836042404174805
+ ],
+ [
+ "▁prevail",
+ -12.8361234664917
+ ],
+ [
+ "mashed",
+ -12.836138725280762
+ ],
+ [
+ "▁1947",
+ -12.83616828918457
+ ],
+ [
+ "function",
+ -12.836292266845703
+ ],
+ [
+ "niveaux",
+ -12.83633041381836
+ ],
+ [
+ "▁territories",
+ -12.836463928222656
+ ],
+ [
+ "▁Permanent",
+ -12.836465835571289
+ ],
+ [
+ "▁christmas",
+ -12.836471557617188
+ ],
+ [
+ "arguing",
+ -12.836490631103516
+ ],
+ [
+ "zukünftig",
+ -12.836654663085938
+ ],
+ [
+ "▁Eindruck",
+ -12.836817741394043
+ ],
+ [
+ "personalised",
+ -12.836854934692383
+ ],
+ [
+ "▁vecin",
+ -12.837211608886719
+ ],
+ [
+ "▁Affiliate",
+ -12.837234497070312
+ ],
+ [
+ "▁Silk",
+ -12.837249755859375
+ ],
+ [
+ "▁Tub",
+ -12.837440490722656
+ ],
+ [
+ "▁remont",
+ -12.837493896484375
+ ],
+ [
+ "▁sauber",
+ -12.837530136108398
+ ],
+ [
+ "gehörig",
+ -12.837562561035156
+ ],
+ [
+ "Maritime",
+ -12.83771800994873
+ ],
+ [
+ "▁Bö",
+ -12.837973594665527
+ ],
+ [
+ "▁1957",
+ -12.83800220489502
+ ],
+ [
+ "▁unparalleled",
+ -12.838005065917969
+ ],
+ [
+ "▁fulfillment",
+ -12.838042259216309
+ ],
+ [
+ "▁collage",
+ -12.838179588317871
+ ],
+ [
+ "fenders",
+ -12.838248252868652
+ ],
+ [
+ "▁neige",
+ -12.838275909423828
+ ],
+ [
+ "▁gamers",
+ -12.838325500488281
+ ],
+ [
+ "tefan",
+ -12.838339805603027
+ ],
+ [
+ "▁wifi",
+ -12.838349342346191
+ ],
+ [
+ "▁leisten",
+ -12.83835506439209
+ ],
+ [
+ "▁Verbesserung",
+ -12.838390350341797
+ ],
+ [
+ "▁composant",
+ -12.838400840759277
+ ],
+ [
+ "▁LORD",
+ -12.8384370803833
+ ],
+ [
+ "arrive",
+ -12.838472366333008
+ ],
+ [
+ "▁conquer",
+ -12.838562965393066
+ ],
+ [
+ "▁lentil",
+ -12.838767051696777
+ ],
+ [
+ "▁Sprech",
+ -12.838995933532715
+ ],
+ [
+ "▁substitution",
+ -12.839015007019043
+ ],
+ [
+ ".05.",
+ -12.839020729064941
+ ],
+ [
+ "FORM",
+ -12.839144706726074
+ ],
+ [
+ "cădere",
+ -12.839154243469238
+ ],
+ [
+ "▁canyon",
+ -12.839430809020996
+ ],
+ [
+ "▁capacitate",
+ -12.839442253112793
+ ],
+ [
+ "▁menace",
+ -12.839461326599121
+ ],
+ [
+ "▁Antique",
+ -12.839519500732422
+ ],
+ [
+ "▁dizaine",
+ -12.839550971984863
+ ],
+ [
+ "▁Saturn",
+ -12.839578628540039
+ ],
+ [
+ "▁gastro",
+ -12.83962631225586
+ ],
+ [
+ "▁Vand",
+ -12.839641571044922
+ ],
+ [
+ "▁africa",
+ -12.839682579040527
+ ],
+ [
+ "▁hackers",
+ -12.839702606201172
+ ],
+ [
+ "▁Bailey",
+ -12.839736938476562
+ ],
+ [
+ "ouette",
+ -12.839822769165039
+ ],
+ [
+ "hoch",
+ -12.839885711669922
+ ],
+ [
+ "étudiant",
+ -12.839973449707031
+ ],
+ [
+ "▁1600",
+ -12.840004920959473
+ ],
+ [
+ "utiliz",
+ -12.840167999267578
+ ],
+ [
+ "reinigung",
+ -12.840263366699219
+ ],
+ [
+ "▁mileage",
+ -12.84029483795166
+ ],
+ [
+ "▁consacré",
+ -12.840309143066406
+ ],
+ [
+ "▁Norfolk",
+ -12.840327262878418
+ ],
+ [
+ "stacked",
+ -12.840659141540527
+ ],
+ [
+ "anbieter",
+ -12.840731620788574
+ ],
+ [
+ "▁gewünschte",
+ -12.84073543548584
+ ],
+ [
+ "▁silicon",
+ -12.840761184692383
+ ],
+ [
+ "Ensuite",
+ -12.840794563293457
+ ],
+ [
+ "▁vendu",
+ -12.840850830078125
+ ],
+ [
+ "▁viteza",
+ -12.840851783752441
+ ],
+ [
+ "▁evaluare",
+ -12.840913772583008
+ ],
+ [
+ "▁contient",
+ -12.841036796569824
+ ],
+ [
+ "▁Viagra",
+ -12.841100692749023
+ ],
+ [
+ "▁circumstance",
+ -12.841283798217773
+ ],
+ [
+ "walker",
+ -12.841383934020996
+ ],
+ [
+ "▁Aluminium",
+ -12.84148120880127
+ ],
+ [
+ "ço",
+ -12.841556549072266
+ ],
+ [
+ "▁Kli",
+ -12.841643333435059
+ ],
+ [
+ "▁deliberately",
+ -12.841649055480957
+ ],
+ [
+ "▁gamble",
+ -12.841893196105957
+ ],
+ [
+ "▁nourri",
+ -12.841903686523438
+ ],
+ [
+ "▁sealing",
+ -12.84194278717041
+ ],
+ [
+ "▁Atmosphäre",
+ -12.842255592346191
+ ],
+ [
+ "▁erschien",
+ -12.842260360717773
+ ],
+ [
+ "▁brightness",
+ -12.842340469360352
+ ],
+ [
+ "autonomie",
+ -12.84251594543457
+ ],
+ [
+ "▁propel",
+ -12.842525482177734
+ ],
+ [
+ "▁Infrastructure",
+ -12.842642784118652
+ ],
+ [
+ "▁război",
+ -12.842642784118652
+ ],
+ [
+ "▁jelly",
+ -12.842684745788574
+ ],
+ [
+ "scalable",
+ -12.84280776977539
+ ],
+ [
+ "regal",
+ -12.84296703338623
+ ],
+ [
+ "▁sarcini",
+ -12.843031883239746
+ ],
+ [
+ "▁Dienstag",
+ -12.84304428100586
+ ],
+ [
+ "▁Receive",
+ -12.8430814743042
+ ],
+ [
+ "▁mango",
+ -12.843356132507324
+ ],
+ [
+ "▁compétition",
+ -12.84341812133789
+ ],
+ [
+ "▁Monument",
+ -12.843428611755371
+ ],
+ [
+ "▁mast",
+ -12.844159126281738
+ ],
+ [
+ "▁instructed",
+ -12.84425163269043
+ ],
+ [
+ "▁aventur",
+ -12.844277381896973
+ ],
+ [
+ "139",
+ -12.844298362731934
+ ],
+ [
+ "▁Parmi",
+ -12.84435749053955
+ ],
+ [
+ "confined",
+ -12.844416618347168
+ ],
+ [
+ "acious",
+ -12.844441413879395
+ ],
+ [
+ "▁simptome",
+ -12.844581604003906
+ ],
+ [
+ "▁Fischer",
+ -12.844897270202637
+ ],
+ [
+ "störung",
+ -12.844985008239746
+ ],
+ [
+ "▁bilateral",
+ -12.84504508972168
+ ],
+ [
+ "preşedintele",
+ -12.845274925231934
+ ],
+ [
+ "accueillir",
+ -12.845357894897461
+ ],
+ [
+ "▁Schmidt",
+ -12.845359802246094
+ ],
+ [
+ "litis",
+ -12.845373153686523
+ ],
+ [
+ "WL",
+ -12.8454008102417
+ ],
+ [
+ "▁Rise",
+ -12.845436096191406
+ ],
+ [
+ "▁streamline",
+ -12.845556259155273
+ ],
+ [
+ "sozialen",
+ -12.845585823059082
+ ],
+ [
+ "▁Emirates",
+ -12.845746040344238
+ ],
+ [
+ "▁encrypted",
+ -12.845746040344238
+ ],
+ [
+ "▁unfamiliar",
+ -12.845746040344238
+ ],
+ [
+ "established",
+ -12.84577751159668
+ ],
+ [
+ "▁Tätigkeit",
+ -12.845818519592285
+ ],
+ [
+ "▁unaware",
+ -12.845913887023926
+ ],
+ [
+ "2:00",
+ -12.8460054397583
+ ],
+ [
+ "macher",
+ -12.846013069152832
+ ],
+ [
+ "NSA",
+ -12.8461275100708
+ ],
+ [
+ "▁rutier",
+ -12.846177101135254
+ ],
+ [
+ "▁Trent",
+ -12.846212387084961
+ ],
+ [
+ "▁sickness",
+ -12.846277236938477
+ ],
+ [
+ "▁advert",
+ -12.846417427062988
+ ],
+ [
+ "▁Kranken",
+ -12.846426963806152
+ ],
+ [
+ "▁Sandra",
+ -12.846443176269531
+ ],
+ [
+ "▁Recreation",
+ -12.846449851989746
+ ],
+ [
+ "▁Evidence",
+ -12.846524238586426
+ ],
+ [
+ "▁Immigration",
+ -12.846524238586426
+ ],
+ [
+ "▁carriage",
+ -12.846524238586426
+ ],
+ [
+ "▁justified",
+ -12.84655475616455
+ ],
+ [
+ "▁veche",
+ -12.846579551696777
+ ],
+ [
+ "PGA",
+ -12.846604347229004
+ ],
+ [
+ "▁Carmen",
+ -12.846735000610352
+ ],
+ [
+ "▁Faites",
+ -12.846750259399414
+ ],
+ [
+ "▁erfüllt",
+ -12.84691333770752
+ ],
+ [
+ "▁voilà",
+ -12.846931457519531
+ ],
+ [
+ "▁împlin",
+ -12.846959114074707
+ ],
+ [
+ "deposited",
+ -12.84721565246582
+ ],
+ [
+ "▁decisiv",
+ -12.847241401672363
+ ],
+ [
+ "CSA",
+ -12.847249031066895
+ ],
+ [
+ "pathy",
+ -12.84726619720459
+ ],
+ [
+ "▁erweitert",
+ -12.847302436828613
+ ],
+ [
+ "▁liquor",
+ -12.847302436828613
+ ],
+ [
+ "▁resilient",
+ -12.847302436828613
+ ],
+ [
+ "▁walmart",
+ -12.847302436828613
+ ],
+ [
+ "▁fencing",
+ -12.847308158874512
+ ],
+ [
+ "▁dépasse",
+ -12.84731388092041
+ ],
+ [
+ "KT",
+ -12.847354888916016
+ ],
+ [
+ "▁fries",
+ -12.847368240356445
+ ],
+ [
+ "vadă",
+ -12.847421646118164
+ ],
+ [
+ "▁Spania",
+ -12.847478866577148
+ ],
+ [
+ "▁complètement",
+ -12.847725868225098
+ ],
+ [
+ "▁lucrari",
+ -12.84777545928955
+ ],
+ [
+ "▁Lieb",
+ -12.847908973693848
+ ],
+ [
+ "leistungen",
+ -12.847943305969238
+ ],
+ [
+ "198",
+ -12.847979545593262
+ ],
+ [
+ "▁Schnell",
+ -12.847997665405273
+ ],
+ [
+ "▁radius",
+ -12.84814453125
+ ],
+ [
+ "▁beneficiaries",
+ -12.848151206970215
+ ],
+ [
+ "▁northwest",
+ -12.848174095153809
+ ],
+ [
+ "▁#4",
+ -12.848223686218262
+ ],
+ [
+ "▁embryo",
+ -12.848492622375488
+ ],
+ [
+ "▁ditch",
+ -12.848791122436523
+ ],
+ [
+ "▁Seriously",
+ -12.848859786987305
+ ],
+ [
+ "oppel",
+ -12.848941802978516
+ ],
+ [
+ "▁stalk",
+ -12.849053382873535
+ ],
+ [
+ "écriture",
+ -12.849066734313965
+ ],
+ [
+ "512",
+ -12.84912109375
+ ],
+ [
+ "wiesen",
+ -12.849271774291992
+ ],
+ [
+ "▁Consum",
+ -12.849321365356445
+ ],
+ [
+ "▁lună",
+ -12.849405288696289
+ ],
+ [
+ "▁lantern",
+ -12.849441528320312
+ ],
+ [
+ "▁italian",
+ -12.849629402160645
+ ],
+ [
+ "▁achiziți",
+ -12.849639892578125
+ ],
+ [
+ "▁catalyst",
+ -12.849639892578125
+ ],
+ [
+ "▁Arbeitgeber",
+ -12.849662780761719
+ ],
+ [
+ "▁researched",
+ -12.8496675491333
+ ],
+ [
+ "▁drastically",
+ -12.849679946899414
+ ],
+ [
+ "versammlung",
+ -12.849735260009766
+ ],
+ [
+ "410",
+ -12.849800109863281
+ ],
+ [
+ "▁impus",
+ -12.850153923034668
+ ],
+ [
+ "▁interchange",
+ -12.850173950195312
+ ],
+ [
+ "▁pharmacie",
+ -12.850215911865234
+ ],
+ [
+ "Live",
+ -12.850354194641113
+ ],
+ [
+ "dents",
+ -12.850384712219238
+ ],
+ [
+ "▁charcoal",
+ -12.850419998168945
+ ],
+ [
+ "▁odihn",
+ -12.850420951843262
+ ],
+ [
+ "▁pistol",
+ -12.850444793701172
+ ],
+ [
+ "▁complaining",
+ -12.850576400756836
+ ],
+ [
+ "manager",
+ -12.850578308105469
+ ],
+ [
+ "themed",
+ -12.850578308105469
+ ],
+ [
+ "▁Chang",
+ -12.850650787353516
+ ],
+ [
+ "▁rookie",
+ -12.85070514678955
+ ],
+ [
+ "Great",
+ -12.850706100463867
+ ],
+ [
+ "▁smoker",
+ -12.850733757019043
+ ],
+ [
+ "▁Container",
+ -12.850812911987305
+ ],
+ [
+ "▁bancaire",
+ -12.850852966308594
+ ],
+ [
+ "▁Actual",
+ -12.850966453552246
+ ],
+ [
+ "füllen",
+ -12.850982666015625
+ ],
+ [
+ "forum",
+ -12.850985527038574
+ ],
+ [
+ "bleib",
+ -12.851073265075684
+ ],
+ [
+ "▁combi",
+ -12.851079940795898
+ ],
+ [
+ "smoked",
+ -12.851137161254883
+ ],
+ [
+ "difficultés",
+ -12.851161003112793
+ ],
+ [
+ "▁tactical",
+ -12.851240158081055
+ ],
+ [
+ "▁sichtbar",
+ -12.851483345031738
+ ],
+ [
+ "▁dreptate",
+ -12.851598739624023
+ ],
+ [
+ "ERT",
+ -12.85168743133545
+ ],
+ [
+ "▁Pond",
+ -12.85177993774414
+ ],
+ [
+ "▁Holly",
+ -12.851844787597656
+ ],
+ [
+ "erfolg",
+ -12.8518705368042
+ ],
+ [
+ "▁Nordic",
+ -12.851896286010742
+ ],
+ [
+ "évènement",
+ -12.851983070373535
+ ],
+ [
+ "embracing",
+ -12.851984024047852
+ ],
+ [
+ "▁Maximum",
+ -12.851984024047852
+ ],
+ [
+ "▁défend",
+ -12.85205078125
+ ],
+ [
+ "▁fruct",
+ -12.852056503295898
+ ],
+ [
+ "▁Conditioning",
+ -12.852099418640137
+ ],
+ [
+ "LG",
+ -12.852127075195312
+ ],
+ [
+ "exigence",
+ -12.852166175842285
+ ],
+ [
+ "amide",
+ -12.852187156677246
+ ],
+ [
+ "▁darunter",
+ -12.852208137512207
+ ],
+ [
+ "▁EVERY",
+ -12.852420806884766
+ ],
+ [
+ "▁comparat",
+ -12.85244083404541
+ ],
+ [
+ "boosting",
+ -12.852452278137207
+ ],
+ [
+ "▁Hawaiian",
+ -12.852553367614746
+ ],
+ [
+ "▁Geburt",
+ -12.852752685546875
+ ],
+ [
+ "deci",
+ -12.852782249450684
+ ],
+ [
+ "▁Apollo",
+ -12.852803230285645
+ ],
+ [
+ "▁schützen",
+ -12.852821350097656
+ ],
+ [
+ "tragere",
+ -12.852893829345703
+ ],
+ [
+ "Online",
+ -12.852904319763184
+ ],
+ [
+ "▁neural",
+ -12.852913856506348
+ ],
+ [
+ "▁lucrez",
+ -12.853188514709473
+ ],
+ [
+ "▁phenomenal",
+ -12.853253364562988
+ ],
+ [
+ "▁Height",
+ -12.853368759155273
+ ],
+ [
+ "coordinating",
+ -12.853548049926758
+ ],
+ [
+ "geschnitten",
+ -12.853631019592285
+ ],
+ [
+ "auront",
+ -12.853641510009766
+ ],
+ [
+ "▁administer",
+ -12.853644371032715
+ ],
+ [
+ "▁contend",
+ -12.853707313537598
+ ],
+ [
+ "▁crispy",
+ -12.853784561157227
+ ],
+ [
+ "chuck",
+ -12.854011535644531
+ ],
+ [
+ "▁Condition",
+ -12.8540678024292
+ ],
+ [
+ "gestaltung",
+ -12.854324340820312
+ ],
+ [
+ "▁Blvd",
+ -12.854331970214844
+ ],
+ [
+ "▁subjective",
+ -12.854470252990723
+ ],
+ [
+ "▁événements",
+ -12.854708671569824
+ ],
+ [
+ "▁Jenny",
+ -12.855131149291992
+ ],
+ [
+ "▁cumpăra",
+ -12.85519027709961
+ ],
+ [
+ "constructing",
+ -12.855262756347656
+ ],
+ [
+ "▁instructional",
+ -12.85539436340332
+ ],
+ [
+ "▁sterling",
+ -12.855446815490723
+ ],
+ [
+ "scrise",
+ -12.855470657348633
+ ],
+ [
+ "▁Boulevard",
+ -12.855551719665527
+ ],
+ [
+ "pipe",
+ -12.855620384216309
+ ],
+ [
+ "▁Pride",
+ -12.855748176574707
+ ],
+ [
+ "▁Kau",
+ -12.855751991271973
+ ],
+ [
+ "▁overhaul",
+ -12.855924606323242
+ ],
+ [
+ "▁Recruitment",
+ -12.855925559997559
+ ],
+ [
+ "▁thrilling",
+ -12.856218338012695
+ ],
+ [
+ "living",
+ -12.856302261352539
+ ],
+ [
+ "▁rămân",
+ -12.85645866394043
+ ],
+ [
+ "▁MOD",
+ -12.85661792755127
+ ],
+ [
+ "▁Newport",
+ -12.856675148010254
+ ],
+ [
+ "▁infectious",
+ -12.856688499450684
+ ],
+ [
+ "6-3",
+ -12.856860160827637
+ ],
+ [
+ "▁Apache",
+ -12.856976509094238
+ ],
+ [
+ "▁dependence",
+ -12.85698413848877
+ ],
+ [
+ "nutzung",
+ -12.857199668884277
+ ],
+ [
+ "praised",
+ -12.857211112976074
+ ],
+ [
+ "▁craving",
+ -12.857346534729004
+ ],
+ [
+ "▁cramp",
+ -12.857397079467773
+ ],
+ [
+ "▁mancare",
+ -12.857455253601074
+ ],
+ [
+ "▁entdeckt",
+ -12.857474327087402
+ ],
+ [
+ "▁Pioneer",
+ -12.857484817504883
+ ],
+ [
+ "▁Adelaide",
+ -12.857490539550781
+ ],
+ [
+ "2.0",
+ -12.857503890991211
+ ],
+ [
+ "168",
+ -12.857526779174805
+ ],
+ [
+ "▁Decorating",
+ -12.857611656188965
+ ],
+ [
+ "▁unpleasant",
+ -12.857854843139648
+ ],
+ [
+ "▁déclaration",
+ -12.857865333557129
+ ],
+ [
+ "▁Grafik",
+ -12.857908248901367
+ ],
+ [
+ "5-2",
+ -12.857937812805176
+ ],
+ [
+ "căci",
+ -12.857940673828125
+ ],
+ [
+ "▁invade",
+ -12.858171463012695
+ ],
+ [
+ "▁internaţional",
+ -12.858259201049805
+ ],
+ [
+ "▁fraudulent",
+ -12.858281135559082
+ ],
+ [
+ "▁crestere",
+ -12.858441352844238
+ ],
+ [
+ "ografic",
+ -12.858729362487793
+ ],
+ [
+ "plină",
+ -12.859140396118164
+ ],
+ [
+ "sunteti",
+ -12.859150886535645
+ ],
+ [
+ "/04",
+ -12.859176635742188
+ ],
+ [
+ "▁admis",
+ -12.85935115814209
+ ],
+ [
+ "▁mediation",
+ -12.859403610229492
+ ],
+ [
+ "ICC",
+ -12.859424591064453
+ ],
+ [
+ "roș",
+ -12.859660148620605
+ ],
+ [
+ "▁Aroma",
+ -12.8596773147583
+ ],
+ [
+ "1:00",
+ -12.859792709350586
+ ],
+ [
+ "gasesc",
+ -12.859822273254395
+ ],
+ [
+ "▁Defence",
+ -12.859850883483887
+ ],
+ [
+ "▁dictionary",
+ -12.859856605529785
+ ],
+ [
+ "▁Batterie",
+ -12.859865188598633
+ ],
+ [
+ "▁gesunde",
+ -12.85997486114502
+ ],
+ [
+ "146",
+ -12.860099792480469
+ ],
+ [
+ "▁mortal",
+ -12.860129356384277
+ ],
+ [
+ "▁Flughafen",
+ -12.860230445861816
+ ],
+ [
+ "hhh",
+ -12.860284805297852
+ ],
+ [
+ "▁novice",
+ -12.860342025756836
+ ],
+ [
+ "▁Develop",
+ -12.86043930053711
+ ],
+ [
+ "▁accidental",
+ -12.860516548156738
+ ],
+ [
+ "Muzeul",
+ -12.86054515838623
+ ],
+ [
+ "▁Jupiter",
+ -12.86062240600586
+ ],
+ [
+ "supposedly",
+ -12.860662460327148
+ ],
+ [
+ "energy",
+ -12.860758781433105
+ ],
+ [
+ "▁montrer",
+ -12.860764503479004
+ ],
+ [
+ "recalled",
+ -12.860795021057129
+ ],
+ [
+ "Press",
+ -12.860801696777344
+ ],
+ [
+ "▁postcard",
+ -12.86080265045166
+ ],
+ [
+ "target",
+ -12.86081600189209
+ ],
+ [
+ "▁vêtements",
+ -12.860881805419922
+ ],
+ [
+ "▁particle",
+ -12.860888481140137
+ ],
+ [
+ "professional",
+ -12.8608980178833
+ ],
+ [
+ "▁1949",
+ -12.860917091369629
+ ],
+ [
+ "yah",
+ -12.860980033874512
+ ],
+ [
+ "▁Spiegel",
+ -12.861017227172852
+ ],
+ [
+ "▁Jeffrey",
+ -12.861023902893066
+ ],
+ [
+ "fahrzeug",
+ -12.861027717590332
+ ],
+ [
+ "▁Plug",
+ -12.861051559448242
+ ],
+ [
+ "▁violin",
+ -12.861150741577148
+ ],
+ [
+ "▁condemn",
+ -12.861381530761719
+ ],
+ [
+ "▁conducere",
+ -12.861398696899414
+ ],
+ [
+ "▁Chevrolet",
+ -12.861412048339844
+ ],
+ [
+ "▁conceput",
+ -12.861461639404297
+ ],
+ [
+ "▁Merri",
+ -12.861493110656738
+ ],
+ [
+ "judging",
+ -12.861559867858887
+ ],
+ [
+ "embraced",
+ -12.86168098449707
+ ],
+ [
+ "▁Compact",
+ -12.861715316772461
+ ],
+ [
+ "▁château",
+ -12.861807823181152
+ ],
+ [
+ "etch",
+ -12.861945152282715
+ ],
+ [
+ "bedroom",
+ -12.861995697021484
+ ],
+ [
+ "People",
+ -12.862038612365723
+ ],
+ [
+ "25,000",
+ -12.86209774017334
+ ],
+ [
+ "ocyte",
+ -12.862146377563477
+ ],
+ [
+ "▁Lenovo",
+ -12.862205505371094
+ ],
+ [
+ "▁Hampton",
+ -12.862241744995117
+ ],
+ [
+ "5.2",
+ -12.862244606018066
+ ],
+ [
+ "▁progres",
+ -12.862266540527344
+ ],
+ [
+ "hoc",
+ -12.862288475036621
+ ],
+ [
+ "▁complementary",
+ -12.86241340637207
+ ],
+ [
+ "turned",
+ -12.862485885620117
+ ],
+ [
+ "mangel",
+ -12.862508773803711
+ ],
+ [
+ "▁Drew",
+ -12.862592697143555
+ ],
+ [
+ "épisode",
+ -12.86259651184082
+ ],
+ [
+ "▁Versorgung",
+ -12.86259651184082
+ ],
+ [
+ "▁ausdrücklich",
+ -12.86259651184082
+ ],
+ [
+ "ciune",
+ -12.862788200378418
+ ],
+ [
+ "▁sfârșit",
+ -12.862990379333496
+ ],
+ [
+ "Agricultural",
+ -12.862991333007812
+ ],
+ [
+ "▁caffeine",
+ -12.862991333007812
+ ],
+ [
+ "▁emergencies",
+ -12.862991333007812
+ ],
+ [
+ "▁unhappy",
+ -12.862991333007812
+ ],
+ [
+ "(7)",
+ -12.863043785095215
+ ],
+ [
+ "▁inlocui",
+ -12.863059043884277
+ ],
+ [
+ "▁Rochester",
+ -12.863153457641602
+ ],
+ [
+ "183",
+ -12.863155364990234
+ ],
+ [
+ "niz",
+ -12.863285064697266
+ ],
+ [
+ "tasche",
+ -12.863462448120117
+ ],
+ [
+ "▁Salle",
+ -12.86347484588623
+ ],
+ [
+ "cît",
+ -12.863478660583496
+ ],
+ [
+ "▁Singer",
+ -12.863489151000977
+ ],
+ [
+ "▁economically",
+ -12.863506317138672
+ ],
+ [
+ "▁ieși",
+ -12.863525390625
+ ],
+ [
+ "▁façade",
+ -12.86378288269043
+ ],
+ [
+ "Ohne",
+ -12.863801956176758
+ ],
+ [
+ "▁edible",
+ -12.863842964172363
+ ],
+ [
+ "Rob",
+ -12.863851547241211
+ ],
+ [
+ "▁(2014)",
+ -12.863859176635742
+ ],
+ [
+ "▁Zar",
+ -12.863919258117676
+ ],
+ [
+ "▁obey",
+ -12.863995552062988
+ ],
+ [
+ "Pack",
+ -12.864087104797363
+ ],
+ [
+ "▁Omni",
+ -12.864198684692383
+ ],
+ [
+ "▁Gilbert",
+ -12.864212036132812
+ ],
+ [
+ "▁Vlad",
+ -12.86429500579834
+ ],
+ [
+ "▁pauvre",
+ -12.864333152770996
+ ],
+ [
+ "▁secular",
+ -12.864383697509766
+ ],
+ [
+ "Center",
+ -12.864415168762207
+ ],
+ [
+ "▁Prospect",
+ -12.864457130432129
+ ],
+ [
+ "▁Noah",
+ -12.86450481414795
+ ],
+ [
+ "▁Interactive",
+ -12.86471176147461
+ ],
+ [
+ "▁centaine",
+ -12.86485767364502
+ ],
+ [
+ "▁cerebral",
+ -12.864971160888672
+ ],
+ [
+ "▁Novel",
+ -12.865013122558594
+ ],
+ [
+ "▁Käufer",
+ -12.865039825439453
+ ],
+ [
+ "werfen",
+ -12.865056991577148
+ ],
+ [
+ "▁reluctant",
+ -12.865143775939941
+ ],
+ [
+ "ес",
+ -12.86520004272461
+ ],
+ [
+ "Look",
+ -12.86521053314209
+ ],
+ [
+ "Erkrankung",
+ -12.86536693572998
+ ],
+ [
+ "▁cucumber",
+ -12.86536693572998
+ ],
+ [
+ "/2017",
+ -12.865399360656738
+ ],
+ [
+ "▁flank",
+ -12.865405082702637
+ ],
+ [
+ "opportunité",
+ -12.865667343139648
+ ],
+ [
+ "zugleich",
+ -12.865766525268555
+ ],
+ [
+ "RAT",
+ -12.865840911865234
+ ],
+ [
+ "▁avantages",
+ -12.865880012512207
+ ],
+ [
+ "▁außer",
+ -12.866008758544922
+ ],
+ [
+ "GV",
+ -12.866090774536133
+ ],
+ [
+ "▁Continental",
+ -12.866159439086914
+ ],
+ [
+ "▁affiliation",
+ -12.866159439086914
+ ],
+ [
+ "▁ursprünglich",
+ -12.86618423461914
+ ],
+ [
+ "▁hardship",
+ -12.866349220275879
+ ],
+ [
+ "âme",
+ -12.86647891998291
+ ],
+ [
+ "▁hallway",
+ -12.866576194763184
+ ],
+ [
+ "▁afară",
+ -12.866578102111816
+ ],
+ [
+ "western",
+ -12.866714477539062
+ ],
+ [
+ "▁Jacket",
+ -12.866802215576172
+ ],
+ [
+ "▁culturelle",
+ -12.866876602172852
+ ],
+ [
+ "▁glaci",
+ -12.866995811462402
+ ],
+ [
+ "metoda",
+ -12.867036819458008
+ ],
+ [
+ "▁clerk",
+ -12.867045402526855
+ ],
+ [
+ "▁ordinance",
+ -12.867185592651367
+ ],
+ [
+ "▁Initial",
+ -12.867197036743164
+ ],
+ [
+ "waking",
+ -12.86722469329834
+ ],
+ [
+ "▁Secondary",
+ -12.867366790771484
+ ],
+ [
+ "▁Solomon",
+ -12.867411613464355
+ ],
+ [
+ "glomer",
+ -12.867488861083984
+ ],
+ [
+ "SYS",
+ -12.867530822753906
+ ],
+ [
+ "▁Florin",
+ -12.867596626281738
+ ],
+ [
+ "ffentlich",
+ -12.867670059204102
+ ],
+ [
+ "▁Printer",
+ -12.867674827575684
+ ],
+ [
+ "▁dimineata",
+ -12.86774730682373
+ ],
+ [
+ "▁stripes",
+ -12.867748260498047
+ ],
+ [
+ "plugged",
+ -12.86776065826416
+ ],
+ [
+ "öhl",
+ -12.867836952209473
+ ],
+ [
+ "infused",
+ -12.867875099182129
+ ],
+ [
+ "▁Rubber",
+ -12.867895126342773
+ ],
+ [
+ "paved",
+ -12.867898941040039
+ ],
+ [
+ "▁Devi",
+ -12.867995262145996
+ ],
+ [
+ "▁subway",
+ -12.8681640625
+ ],
+ [
+ "▁gases",
+ -12.868306159973145
+ ],
+ [
+ "▁reguli",
+ -12.868371963500977
+ ],
+ [
+ "▁Rebel",
+ -12.868413925170898
+ ],
+ [
+ "▁destructive",
+ -12.868546485900879
+ ],
+ [
+ "▁oferind",
+ -12.868664741516113
+ ],
+ [
+ "9001",
+ -12.868876457214355
+ ],
+ [
+ "CRA",
+ -12.868912696838379
+ ],
+ [
+ "why",
+ -12.868932723999023
+ ],
+ [
+ "sensul",
+ -12.869036674499512
+ ],
+ [
+ "guter",
+ -12.869277000427246
+ ],
+ [
+ "Empfehlung",
+ -12.869338035583496
+ ],
+ [
+ "▁convertible",
+ -12.86953353881836
+ ],
+ [
+ "▁predominantly",
+ -12.869637489318848
+ ],
+ [
+ "▁Mentor",
+ -12.869649887084961
+ ],
+ [
+ "Practic",
+ -12.869720458984375
+ ],
+ [
+ "▁echipă",
+ -12.869754791259766
+ ],
+ [
+ "onsite",
+ -12.869853019714355
+ ],
+ [
+ "▁zunehmend",
+ -12.86994743347168
+ ],
+ [
+ "▁Harbour",
+ -12.870016098022461
+ ],
+ [
+ "▁pineapple",
+ -12.870133399963379
+ ],
+ [
+ "▁gasoline",
+ -12.870139122009277
+ ],
+ [
+ "▁Jaguar",
+ -12.870158195495605
+ ],
+ [
+ "kno",
+ -12.870259284973145
+ ],
+ [
+ "▁heap",
+ -12.870448112487793
+ ],
+ [
+ "▁fictional",
+ -12.870481491088867
+ ],
+ [
+ "fiinta",
+ -12.870753288269043
+ ],
+ [
+ "▁Amber",
+ -12.87081241607666
+ ],
+ [
+ "▁Exclusive",
+ -12.870929718017578
+ ],
+ [
+ "▁Pharmaceutical",
+ -12.870929718017578
+ ],
+ [
+ "▁unterscheide",
+ -12.871044158935547
+ ],
+ [
+ "▁1942",
+ -12.871116638183594
+ ],
+ [
+ "▁Ceiling",
+ -12.87115478515625
+ ],
+ [
+ "developed",
+ -12.871228218078613
+ ],
+ [
+ "▁consacr",
+ -12.87132453918457
+ ],
+ [
+ "▁Membr",
+ -12.871411323547363
+ ],
+ [
+ "erton",
+ -12.871447563171387
+ ],
+ [
+ "habitation",
+ -12.871685981750488
+ ],
+ [
+ "▁longevity",
+ -12.871726989746094
+ ],
+ [
+ "▁Starbucks",
+ -12.871728897094727
+ ],
+ [
+ "▁poat",
+ -12.871771812438965
+ ],
+ [
+ "▁commissioner",
+ -12.871794700622559
+ ],
+ [
+ "pedia",
+ -12.871938705444336
+ ],
+ [
+ "popped",
+ -12.872468948364258
+ ],
+ [
+ "versorgung",
+ -12.872525215148926
+ ],
+ [
+ "▁Aktivitäten",
+ -12.872525215148926
+ ],
+ [
+ "▁Betreuung",
+ -12.872525215148926
+ ],
+ [
+ "▁afacere",
+ -12.872968673706055
+ ],
+ [
+ "▁Mechanical",
+ -12.873323440551758
+ ],
+ [
+ "▁Leiter",
+ -12.873346328735352
+ ],
+ [
+ "▁scaling",
+ -12.873427391052246
+ ],
+ [
+ "▁Slim",
+ -12.87350082397461
+ ],
+ [
+ "▁temperaturi",
+ -12.873516082763672
+ ],
+ [
+ "ACH",
+ -12.873558044433594
+ ],
+ [
+ "▁jährlich",
+ -12.873682022094727
+ ],
+ [
+ "▁photographie",
+ -12.873722076416016
+ ],
+ [
+ "▁préalable",
+ -12.873725891113281
+ ],
+ [
+ "▁părinți",
+ -12.87372875213623
+ ],
+ [
+ "▁Farmers",
+ -12.873873710632324
+ ],
+ [
+ "▁Printable",
+ -12.873905181884766
+ ],
+ [
+ "Früh",
+ -12.873908996582031
+ ],
+ [
+ "approved",
+ -12.87398624420166
+ ],
+ [
+ "otro",
+ -12.874094009399414
+ ],
+ [
+ "▁veneer",
+ -12.874099731445312
+ ],
+ [
+ "▁Warriors",
+ -12.874122619628906
+ ],
+ [
+ "▁Approach",
+ -12.874149322509766
+ ],
+ [
+ "Share",
+ -12.874238967895508
+ ],
+ [
+ "▁buds",
+ -12.874252319335938
+ ],
+ [
+ "▁Într",
+ -12.874330520629883
+ ],
+ [
+ "glichen",
+ -12.87452507019043
+ ],
+ [
+ "▁anbieten",
+ -12.87452507019043
+ ],
+ [
+ "MET",
+ -12.874539375305176
+ ],
+ [
+ "amélioration",
+ -12.87468147277832
+ ],
+ [
+ "ländische",
+ -12.87468433380127
+ ],
+ [
+ "nsgesamt",
+ -12.874764442443848
+ ],
+ [
+ "einiger",
+ -12.874822616577148
+ ],
+ [
+ "▁Förderung",
+ -12.874876022338867
+ ],
+ [
+ "destroying",
+ -12.874910354614258
+ ],
+ [
+ "▁accreditation",
+ -12.874922752380371
+ ],
+ [
+ "reminiscent",
+ -12.875094413757324
+ ],
+ [
+ "▁retriev",
+ -12.87528133392334
+ ],
+ [
+ "▁Flü",
+ -12.875306129455566
+ ],
+ [
+ "▁Monsieur",
+ -12.875322341918945
+ ],
+ [
+ "German",
+ -12.87536334991455
+ ],
+ [
+ "Orice",
+ -12.875443458557129
+ ],
+ [
+ "künftig",
+ -12.875523567199707
+ ],
+ [
+ "▁vorbi",
+ -12.875639915466309
+ ],
+ [
+ "▁intentionally",
+ -12.875733375549316
+ ],
+ [
+ "▁îngrij",
+ -12.875743865966797
+ ],
+ [
+ "▁laughed",
+ -12.875850677490234
+ ],
+ [
+ "▁Fiction",
+ -12.875913619995117
+ ],
+ [
+ "▁inteligent",
+ -12.875914573669434
+ ],
+ [
+ "▁Translation",
+ -12.875953674316406
+ ],
+ [
+ "greete",
+ -12.875983238220215
+ ],
+ [
+ "▁énergétique",
+ -12.876123428344727
+ ],
+ [
+ "uncovered",
+ -12.876248359680176
+ ],
+ [
+ "▁évidemment",
+ -12.876523971557617
+ ],
+ [
+ "▁Vietnamese",
+ -12.876535415649414
+ ],
+ [
+ "▁Libya",
+ -12.876675605773926
+ ],
+ [
+ "▁Trailer",
+ -12.876734733581543
+ ],
+ [
+ "▁Wohl",
+ -12.876871109008789
+ ],
+ [
+ "▁Congo",
+ -12.87698745727539
+ ],
+ [
+ "▁freut",
+ -12.877002716064453
+ ],
+ [
+ "zauber",
+ -12.877090454101562
+ ],
+ [
+ "▁Pân",
+ -12.877142906188965
+ ],
+ [
+ "▁mentine",
+ -12.877333641052246
+ ],
+ [
+ "▁welding",
+ -12.877335548400879
+ ],
+ [
+ "▁Mircea",
+ -12.8773775100708
+ ],
+ [
+ "▁optimism",
+ -12.877455711364746
+ ],
+ [
+ "VEL",
+ -12.877504348754883
+ ],
+ [
+ "oilea",
+ -12.877540588378906
+ ],
+ [
+ "▁thereafter",
+ -12.877612113952637
+ ],
+ [
+ "▁André",
+ -12.877710342407227
+ ],
+ [
+ "forschung",
+ -12.877799987792969
+ ],
+ [
+ "running",
+ -12.878022193908691
+ ],
+ [
+ "▁hostile",
+ -12.878059387207031
+ ],
+ [
+ "Homme",
+ -12.87811279296875
+ ],
+ [
+ "▁Satellite",
+ -12.878129005432129
+ ],
+ [
+ "▁collagen",
+ -12.87841796875
+ ],
+ [
+ "▁concedi",
+ -12.878518104553223
+ ],
+ [
+ "▁produziert",
+ -12.87852954864502
+ ],
+ [
+ "▁virgin",
+ -12.878540992736816
+ ],
+ [
+ "frant",
+ -12.87857723236084
+ ],
+ [
+ "▁teammates",
+ -12.878744125366211
+ ],
+ [
+ "▁faceti",
+ -12.878802299499512
+ ],
+ [
+ "▁Restoration",
+ -12.87893295288086
+ ],
+ [
+ "▁detached",
+ -12.878935813903809
+ ],
+ [
+ "▁Instructor",
+ -12.878950119018555
+ ],
+ [
+ "montag",
+ -12.879227638244629
+ ],
+ [
+ "▁borrowing",
+ -12.879375457763672
+ ],
+ [
+ "▁Retro",
+ -12.879446983337402
+ ],
+ [
+ "▁behandelt",
+ -12.879536628723145
+ ],
+ [
+ "▁Aussage",
+ -12.879715919494629
+ ],
+ [
+ "▁snorkel",
+ -12.879734992980957
+ ],
+ [
+ "▁Proceedings",
+ -12.879754066467285
+ ],
+ [
+ "▁Judy",
+ -12.879776000976562
+ ],
+ [
+ "▁Wendy",
+ -12.879783630371094
+ ],
+ [
+ "artă",
+ -12.879920959472656
+ ],
+ [
+ "▁Vergangenheit",
+ -12.88013744354248
+ ],
+ [
+ "▁Gegner",
+ -12.880139350891113
+ ],
+ [
+ "▁ulcer",
+ -12.880166053771973
+ ],
+ [
+ "wirksam",
+ -12.880553245544434
+ ],
+ [
+ "▁închis",
+ -12.880560874938965
+ ],
+ [
+ "▁emission",
+ -12.88068962097168
+ ],
+ [
+ "ulescu",
+ -12.880754470825195
+ ],
+ [
+ "▁bancar",
+ -12.880819320678711
+ ],
+ [
+ "compromising",
+ -12.880924224853516
+ ],
+ [
+ "▁Priest",
+ -12.881156921386719
+ ],
+ [
+ "▁Progress",
+ -12.881318092346191
+ ],
+ [
+ "▁punish",
+ -12.88144588470459
+ ],
+ [
+ "▁Afin",
+ -12.881450653076172
+ ],
+ [
+ "▁Bog",
+ -12.881514549255371
+ ],
+ [
+ "lunii",
+ -12.881525039672852
+ ],
+ [
+ "▁ressembl",
+ -12.881570816040039
+ ],
+ [
+ "▁Creation",
+ -12.881644248962402
+ ],
+ [
+ "effet",
+ -12.881668090820312
+ ],
+ [
+ "Versicherung",
+ -12.881671905517578
+ ],
+ [
+ "médias",
+ -12.881672859191895
+ ],
+ [
+ "▁Kritik",
+ -12.881793975830078
+ ],
+ [
+ "idia",
+ -12.881896018981934
+ ],
+ [
+ "▁Wasch",
+ -12.881929397583008
+ ],
+ [
+ "UAL",
+ -12.882059097290039
+ ],
+ [
+ "Approximately",
+ -12.882149696350098
+ ],
+ [
+ "izari",
+ -12.882152557373047
+ ],
+ [
+ "▁Dortmund",
+ -12.882152557373047
+ ],
+ [
+ "▁contul",
+ -12.882343292236328
+ ],
+ [
+ "▁Airways",
+ -12.882408142089844
+ ],
+ [
+ "sicherung",
+ -12.882535934448242
+ ],
+ [
+ "échelle",
+ -12.882560729980469
+ ],
+ [
+ "ADD",
+ -12.882582664489746
+ ],
+ [
+ "DIA",
+ -12.88259506225586
+ ],
+ [
+ "kabel",
+ -12.882621765136719
+ ],
+ [
+ "Media",
+ -12.88268756866455
+ ],
+ [
+ "ampli",
+ -12.882894515991211
+ ],
+ [
+ "▁quarry",
+ -12.88295841217041
+ ],
+ [
+ "▁acoper",
+ -12.883072853088379
+ ],
+ [
+ "halter",
+ -12.883326530456543
+ ],
+ [
+ "▁solicitor",
+ -12.883684158325195
+ ],
+ [
+ "phosphat",
+ -12.883763313293457
+ ],
+ [
+ "▁drown",
+ -12.883773803710938
+ ],
+ [
+ "congratulat",
+ -12.884047508239746
+ ],
+ [
+ "▁uneven",
+ -12.884087562561035
+ ],
+ [
+ "▁rupe",
+ -12.884154319763184
+ ],
+ [
+ "▁heureux",
+ -12.88417911529541
+ ],
+ [
+ "caractéristiques",
+ -12.884221076965332
+ ],
+ [
+ "60,000",
+ -12.884283065795898
+ ],
+ [
+ "ambigu",
+ -12.884340286254883
+ ],
+ [
+ "224",
+ -12.884417533874512
+ ],
+ [
+ "dov",
+ -12.88454532623291
+ ],
+ [
+ "▁Naturally",
+ -12.884629249572754
+ ],
+ [
+ "▁Ernst",
+ -12.884634017944336
+ ],
+ [
+ "Camp",
+ -12.884757995605469
+ ],
+ [
+ "▁Worldwide",
+ -12.884909629821777
+ ],
+ [
+ "▁antrenament",
+ -12.885042190551758
+ ],
+ [
+ "▁jocul",
+ -12.88521671295166
+ ],
+ [
+ "▁broccoli",
+ -12.88537883758545
+ ],
+ [
+ "▁fascinated",
+ -12.88537883758545
+ ],
+ [
+ "▁Abbey",
+ -12.885387420654297
+ ],
+ [
+ "▁aquarium",
+ -12.885390281677246
+ ],
+ [
+ "HAN",
+ -12.885458946228027
+ ],
+ [
+ "chaffung",
+ -12.885480880737305
+ ],
+ [
+ "137",
+ -12.885503768920898
+ ],
+ [
+ "rumors",
+ -12.885515213012695
+ ],
+ [
+ "reliance",
+ -12.885557174682617
+ ],
+ [
+ "▁vaccination",
+ -12.8856782913208
+ ],
+ [
+ "responsabilitate",
+ -12.885777473449707
+ ],
+ [
+ "▁legislati",
+ -12.885782241821289
+ ],
+ [
+ "ATT",
+ -12.885826110839844
+ ],
+ [
+ "206",
+ -12.885896682739258
+ ],
+ [
+ "▁miere",
+ -12.885967254638672
+ ],
+ [
+ "▁rezultatul",
+ -12.885988235473633
+ ],
+ [
+ "părea",
+ -12.88599681854248
+ ],
+ [
+ "zuführen",
+ -12.886159896850586
+ ],
+ [
+ "▁Kompetenz",
+ -12.886187553405762
+ ],
+ [
+ "▁nickname",
+ -12.886195182800293
+ ],
+ [
+ "pilot",
+ -12.88620376586914
+ ],
+ [
+ "▁ninth",
+ -12.886252403259277
+ ],
+ [
+ "▁Tyr",
+ -12.886446952819824
+ ],
+ [
+ "▁misuse",
+ -12.886469841003418
+ ],
+ [
+ "▁SUP",
+ -12.886514663696289
+ ],
+ [
+ "▁Attack",
+ -12.88667106628418
+ ],
+ [
+ "Smart",
+ -12.88669490814209
+ ],
+ [
+ "▁Philosoph",
+ -12.886930465698242
+ ],
+ [
+ "▁Alege",
+ -12.886931419372559
+ ],
+ [
+ "▁femeile",
+ -12.886967658996582
+ ],
+ [
+ "▁Heating",
+ -12.88698673248291
+ ],
+ [
+ "▁Cricket",
+ -12.886999130249023
+ ],
+ [
+ "▁scholar",
+ -12.887049674987793
+ ],
+ [
+ "Model",
+ -12.887073516845703
+ ],
+ [
+ "▁stimulating",
+ -12.887182235717773
+ ],
+ [
+ "▁industrielle",
+ -12.887189865112305
+ ],
+ [
+ "▁phenomena",
+ -12.887303352355957
+ ],
+ [
+ "▁Nahrung",
+ -12.887414932250977
+ ],
+ [
+ "▁Conditioner",
+ -12.887433052062988
+ ],
+ [
+ "führ",
+ -12.887489318847656
+ ],
+ [
+ "▁révolution",
+ -12.88757610321045
+ ],
+ [
+ "plastic",
+ -12.887595176696777
+ ],
+ [
+ "▁approximate",
+ -12.887596130371094
+ ],
+ [
+ "▁dienen",
+ -12.887624740600586
+ ],
+ [
+ "▁obsession",
+ -12.887807846069336
+ ],
+ [
+ "▁rectangular",
+ -12.887807846069336
+ ],
+ [
+ "Allemagne",
+ -12.887808799743652
+ ],
+ [
+ "▁Tanzania",
+ -12.887824058532715
+ ],
+ [
+ "border",
+ -12.887884140014648
+ ],
+ [
+ "▁crashed",
+ -12.887958526611328
+ ],
+ [
+ "visor",
+ -12.887974739074707
+ ],
+ [
+ "▁autorizat",
+ -12.888072967529297
+ ],
+ [
+ "▁Champagne",
+ -12.888222694396973
+ ],
+ [
+ "längst",
+ -12.888238906860352
+ ],
+ [
+ "▁realities",
+ -12.888314247131348
+ ],
+ [
+ "▁Keyword",
+ -12.88831615447998
+ ],
+ [
+ "▁GUI",
+ -12.888495445251465
+ ],
+ [
+ "▁simplified",
+ -12.88865852355957
+ ],
+ [
+ "▁Rack",
+ -12.888681411743164
+ ],
+ [
+ "▁Zahlen",
+ -12.888693809509277
+ ],
+ [
+ "growth",
+ -12.888897895812988
+ ],
+ [
+ "▁rehearsal",
+ -12.888991355895996
+ ],
+ [
+ "▁Epic",
+ -12.888999938964844
+ ],
+ [
+ "▁réussite",
+ -12.889195442199707
+ ],
+ [
+ "▁politician",
+ -12.889263153076172
+ ],
+ [
+ "▁emoți",
+ -12.889378547668457
+ ],
+ [
+ "▁delegation",
+ -12.889449119567871
+ ],
+ [
+ "▁со",
+ -12.889464378356934
+ ],
+ [
+ "oversized",
+ -12.889477729797363
+ ],
+ [
+ "▁Motto",
+ -12.889481544494629
+ ],
+ [
+ "1860",
+ -12.889788627624512
+ ],
+ [
+ "▁defective",
+ -12.889803886413574
+ ],
+ [
+ "brewing",
+ -12.889852523803711
+ ],
+ [
+ "linguistic",
+ -12.890243530273438
+ ],
+ [
+ "▁Hopkins",
+ -12.890265464782715
+ ],
+ [
+ "▁(2012)",
+ -12.89030933380127
+ ],
+ [
+ "crease",
+ -12.890436172485352
+ ],
+ [
+ "▁Versicherungs",
+ -12.89052677154541
+ ],
+ [
+ "▁Noble",
+ -12.890752792358398
+ ],
+ [
+ "▁Bekannt",
+ -12.890896797180176
+ ],
+ [
+ "▁vorstellen",
+ -12.89095401763916
+ ],
+ [
+ "▁suburban",
+ -12.890970230102539
+ ],
+ [
+ "DAC",
+ -12.890995025634766
+ ],
+ [
+ "▁scatter",
+ -12.89103889465332
+ ],
+ [
+ "▁Artificial",
+ -12.8910551071167
+ ],
+ [
+ "▁reactor",
+ -12.891073226928711
+ ],
+ [
+ "▁modelling",
+ -12.89108943939209
+ ],
+ [
+ "▁Holder",
+ -12.891148567199707
+ ],
+ [
+ "athon",
+ -12.891149520874023
+ ],
+ [
+ "147",
+ -12.891190528869629
+ ],
+ [
+ "▁stagn",
+ -12.891257286071777
+ ],
+ [
+ "ARY",
+ -12.891261100769043
+ ],
+ [
+ "Space",
+ -12.89126968383789
+ ],
+ [
+ "▁Gibson",
+ -12.891718864440918
+ ],
+ [
+ "▁Investigator",
+ -12.89173698425293
+ ],
+ [
+ "▁1914",
+ -12.891818046569824
+ ],
+ [
+ "▁Muhammad",
+ -12.891868591308594
+ ],
+ [
+ "▁shove",
+ -12.892073631286621
+ ],
+ [
+ "▁erklären",
+ -12.892276763916016
+ ],
+ [
+ "▁abdomen",
+ -12.892277717590332
+ ],
+ [
+ "▁Mazda",
+ -12.892349243164062
+ ],
+ [
+ "▁hemo",
+ -12.892364501953125
+ ],
+ [
+ "National",
+ -12.892455101013184
+ ],
+ [
+ "starken",
+ -12.89267635345459
+ ],
+ [
+ "▁Cyprus",
+ -12.892683982849121
+ ],
+ [
+ "▁tread",
+ -12.892721176147461
+ ],
+ [
+ "▁sweetness",
+ -12.892725944519043
+ ],
+ [
+ "stunden",
+ -12.892790794372559
+ ],
+ [
+ "▁couverture",
+ -12.893059730529785
+ ],
+ [
+ "▁Successful",
+ -12.893060684204102
+ ],
+ [
+ "▁oublier",
+ -12.893171310424805
+ ],
+ [
+ "▁esential",
+ -12.893203735351562
+ ],
+ [
+ "estival",
+ -12.89321231842041
+ ],
+ [
+ "gnac",
+ -12.893280029296875
+ ],
+ [
+ "▁Basement",
+ -12.893457412719727
+ ],
+ [
+ "presumably",
+ -12.893497467041016
+ ],
+ [
+ "▁mourn",
+ -12.893561363220215
+ ],
+ [
+ "armée",
+ -12.893677711486816
+ ],
+ [
+ "148",
+ -12.893845558166504
+ ],
+ [
+ "▁residue",
+ -12.894006729125977
+ ],
+ [
+ "▁metalic",
+ -12.89404296875
+ ],
+ [
+ "▁Zell",
+ -12.89425277709961
+ ],
+ [
+ "Build",
+ -12.894280433654785
+ ],
+ [
+ "▁prevalence",
+ -12.894312858581543
+ ],
+ [
+ "▁wrestling",
+ -12.894312858581543
+ ],
+ [
+ "▁ascuns",
+ -12.894325256347656
+ ],
+ [
+ "Sacred",
+ -12.894340515136719
+ ],
+ [
+ "Tec",
+ -12.89438533782959
+ ],
+ [
+ "▁Kindergarten",
+ -12.894389152526855
+ ],
+ [
+ "bindung",
+ -12.894464492797852
+ ],
+ [
+ "▁ritm",
+ -12.894545555114746
+ ],
+ [
+ "▁triste",
+ -12.894651412963867
+ ],
+ [
+ "▁introdus",
+ -12.894758224487305
+ ],
+ [
+ "/2016",
+ -12.894824028015137
+ ],
+ [
+ "▁română",
+ -12.894899368286133
+ ],
+ [
+ "▁bibli",
+ -12.89490032196045
+ ],
+ [
+ "▁cigar",
+ -12.894913673400879
+ ],
+ [
+ "Rie",
+ -12.894990921020508
+ ],
+ [
+ "▁intentional",
+ -12.894999504089355
+ ],
+ [
+ "▁cuprins",
+ -12.895098686218262
+ ],
+ [
+ "remarkably",
+ -12.895129203796387
+ ],
+ [
+ "▁printemps",
+ -12.895133972167969
+ ],
+ [
+ "▁declining",
+ -12.895171165466309
+ ],
+ [
+ "Magazin",
+ -12.89552116394043
+ ],
+ [
+ "▁săptămână",
+ -12.895537376403809
+ ],
+ [
+ "▁vérifier",
+ -12.895549774169922
+ ],
+ [
+ "▁Speise",
+ -12.895584106445312
+ ],
+ [
+ "▁reteta",
+ -12.8956298828125
+ ],
+ [
+ "heed",
+ -12.895772933959961
+ ],
+ [
+ "▁Compliance",
+ -12.895946502685547
+ ],
+ [
+ "▁embroidery",
+ -12.895946502685547
+ ],
+ [
+ "cried",
+ -12.896025657653809
+ ],
+ [
+ "▁(„",
+ -12.896282196044922
+ ],
+ [
+ "▁heck",
+ -12.89629077911377
+ ],
+ [
+ "▁sadness",
+ -12.896501541137695
+ ],
+ [
+ "▁impulse",
+ -12.896585464477539
+ ],
+ [
+ "ATH",
+ -12.896740913391113
+ ],
+ [
+ "▁lavender",
+ -12.896773338317871
+ ],
+ [
+ "uiesc",
+ -12.896790504455566
+ ],
+ [
+ "▁Disorder",
+ -12.896876335144043
+ ],
+ [
+ "stroke",
+ -12.896991729736328
+ ],
+ [
+ "▁piaţ",
+ -12.8970365524292
+ ],
+ [
+ "ournée",
+ -12.897049903869629
+ ],
+ [
+ "▁Barnes",
+ -12.8971586227417
+ ],
+ [
+ "▁scăzut",
+ -12.897172927856445
+ ],
+ [
+ "▁équipements",
+ -12.89725112915039
+ ],
+ [
+ "OND",
+ -12.897375106811523
+ ],
+ [
+ "▁Compet",
+ -12.897424697875977
+ ],
+ [
+ "▁Bestell",
+ -12.89748477935791
+ ],
+ [
+ "▁immédiatement",
+ -12.897587776184082
+ ],
+ [
+ "aparut",
+ -12.89759635925293
+ ],
+ [
+ "▁rainfall",
+ -12.897882461547852
+ ],
+ [
+ "oreille",
+ -12.89797306060791
+ ],
+ [
+ "▁ministère",
+ -12.898014068603516
+ ],
+ [
+ "iris",
+ -12.898140907287598
+ ],
+ [
+ "dyna",
+ -12.898279190063477
+ ],
+ [
+ "drücken",
+ -12.898343086242676
+ ],
+ [
+ "▁détect",
+ -12.89834976196289
+ ],
+ [
+ "▁fonctionnalité",
+ -12.89840030670166
+ ],
+ [
+ "▁imbalance",
+ -12.89840030670166
+ ],
+ [
+ "▁unpredictable",
+ -12.89840030670166
+ ],
+ [
+ "▁literar",
+ -12.89846134185791
+ ],
+ [
+ "▁Windsor",
+ -12.898472785949707
+ ],
+ [
+ "▁Unlimited",
+ -12.898481369018555
+ ],
+ [
+ "colour",
+ -12.898674964904785
+ ],
+ [
+ "▁Portfolio",
+ -12.898810386657715
+ ],
+ [
+ "149",
+ -12.898883819580078
+ ],
+ [
+ "volution",
+ -12.898890495300293
+ ],
+ [
+ "▁folgende",
+ -12.899078369140625
+ ],
+ [
+ "▁arbitration",
+ -12.899105072021484
+ ],
+ [
+ "kicking",
+ -12.89913558959961
+ ],
+ [
+ "zügig",
+ -12.89923095703125
+ ],
+ [
+ "▁1941",
+ -12.899311065673828
+ ],
+ [
+ "▁Drake",
+ -12.89955997467041
+ ],
+ [
+ "▁ausführlich",
+ -12.899630546569824
+ ],
+ [
+ "▁chaussure",
+ -12.899630546569824
+ ],
+ [
+ "▁intestinal",
+ -12.89976692199707
+ ],
+ [
+ "▁pilgrim",
+ -12.900040626525879
+ ],
+ [
+ "▁Bark",
+ -12.900142669677734
+ ],
+ [
+ "between",
+ -12.900157928466797
+ ],
+ [
+ "disposed",
+ -12.900175094604492
+ ],
+ [
+ "▁Dylan",
+ -12.900218963623047
+ ],
+ [
+ "ств",
+ -12.900253295898438
+ ],
+ [
+ "NOR",
+ -12.900287628173828
+ ],
+ [
+ "traces",
+ -12.90038776397705
+ ],
+ [
+ "▁moindre",
+ -12.900500297546387
+ ],
+ [
+ "▁$10,000",
+ -12.900552749633789
+ ],
+ [
+ "212",
+ -12.900599479675293
+ ],
+ [
+ "wusste",
+ -12.900659561157227
+ ],
+ [
+ "▁predictable",
+ -12.900671005249023
+ ],
+ [
+ "poţi",
+ -12.900679588317871
+ ],
+ [
+ "▁Celsius",
+ -12.900860786437988
+ ],
+ [
+ "gebunden",
+ -12.90086841583252
+ ],
+ [
+ "▁Legacy",
+ -12.900891304016113
+ ],
+ [
+ "movers",
+ -12.90090274810791
+ ],
+ [
+ "▁concret",
+ -12.90098762512207
+ ],
+ [
+ "▁simpla",
+ -12.901050567626953
+ ],
+ [
+ "rechnet",
+ -12.901103973388672
+ ],
+ [
+ "▁certainty",
+ -12.901144981384277
+ ],
+ [
+ "entrepreneurship",
+ -12.901153564453125
+ ],
+ [
+ "kohl",
+ -12.901289939880371
+ ],
+ [
+ "▁curte",
+ -12.901311874389648
+ ],
+ [
+ "▁Forbes",
+ -12.901411056518555
+ ],
+ [
+ "▁Zusatz",
+ -12.901535987854004
+ ],
+ [
+ "blending",
+ -12.90163803100586
+ ],
+ [
+ "▁variat",
+ -12.901642799377441
+ ],
+ [
+ "▁galaxy",
+ -12.90168285369873
+ ],
+ [
+ "▁safari",
+ -12.90168571472168
+ ],
+ [
+ "▁municipalities",
+ -12.9017972946167
+ ],
+ [
+ "▁Drept",
+ -12.90180778503418
+ ],
+ [
+ "aufnahme",
+ -12.902128219604492
+ ],
+ [
+ "▁endorse",
+ -12.902223587036133
+ ],
+ [
+ "einrichtung",
+ -12.902244567871094
+ ],
+ [
+ "Sync",
+ -12.902270317077637
+ ],
+ [
+ "abide",
+ -12.902323722839355
+ ],
+ [
+ "brushed",
+ -12.902350425720215
+ ],
+ [
+ "▁actiune",
+ -12.902410507202148
+ ],
+ [
+ "quaint",
+ -12.902498245239258
+ ],
+ [
+ "▁volatility",
+ -12.902504920959473
+ ],
+ [
+ "▁repetitive",
+ -12.902505874633789
+ ],
+ [
+ "▁découvr",
+ -12.902560234069824
+ ],
+ [
+ "Totodat",
+ -12.902585983276367
+ ],
+ [
+ "▁românesc",
+ -12.902682304382324
+ ],
+ [
+ "▁tempting",
+ -12.902772903442383
+ ],
+ [
+ "thesis",
+ -12.902947425842285
+ ],
+ [
+ "secure",
+ -12.903013229370117
+ ],
+ [
+ "delt",
+ -12.903019905090332
+ ],
+ [
+ "▁şef",
+ -12.903167724609375
+ ],
+ [
+ "▁epidemic",
+ -12.903326988220215
+ ],
+ [
+ "▁Appliance",
+ -12.903327941894531
+ ],
+ [
+ "cearcă",
+ -12.903331756591797
+ ],
+ [
+ "▁lodging",
+ -12.903361320495605
+ ],
+ [
+ "▁photographed",
+ -12.903507232666016
+ ],
+ [
+ "geschlagen",
+ -12.903794288635254
+ ],
+ [
+ "▁Methodist",
+ -12.90380859375
+ ],
+ [
+ "▁Transit",
+ -12.90389347076416
+ ],
+ [
+ "▁Länder",
+ -12.903934478759766
+ ],
+ [
+ "villa",
+ -12.903986930847168
+ ],
+ [
+ "▁toilette",
+ -12.904031753540039
+ ],
+ [
+ "anno",
+ -12.904074668884277
+ ],
+ [
+ "▁Aufnahme",
+ -12.904091835021973
+ ],
+ [
+ "▁Coral",
+ -12.904099464416504
+ ],
+ [
+ "pourraient",
+ -12.904129981994629
+ ],
+ [
+ "▁digestion",
+ -12.904245376586914
+ ],
+ [
+ "▁Vacation",
+ -12.904274940490723
+ ],
+ [
+ "▁Rugby",
+ -12.904275894165039
+ ],
+ [
+ "MIC",
+ -12.904311180114746
+ ],
+ [
+ "▁choc",
+ -12.904417991638184
+ ],
+ [
+ "2002",
+ -12.904492378234863
+ ],
+ [
+ "gestion",
+ -12.904674530029297
+ ],
+ [
+ "▁Zoom",
+ -12.904745101928711
+ ],
+ [
+ "essor",
+ -12.904763221740723
+ ],
+ [
+ "weighed",
+ -12.904793739318848
+ ],
+ [
+ "▁dispus",
+ -12.904987335205078
+ ],
+ [
+ "▁redemption",
+ -12.90502643585205
+ ],
+ [
+ "▁plaster",
+ -12.905071258544922
+ ],
+ [
+ "▁Quilt",
+ -12.90507698059082
+ ],
+ [
+ "▁teritoriul",
+ -12.905088424682617
+ ],
+ [
+ "ndern",
+ -12.905097961425781
+ ],
+ [
+ "▁expired",
+ -12.905105590820312
+ ],
+ [
+ "▁Tribunal",
+ -12.905122756958008
+ ],
+ [
+ "occupation",
+ -12.9052152633667
+ ],
+ [
+ "▁woodland",
+ -12.905248641967773
+ ],
+ [
+ "vieux",
+ -12.905254364013672
+ ],
+ [
+ "▁Midland",
+ -12.905465126037598
+ ],
+ [
+ "gât",
+ -12.90571117401123
+ ],
+ [
+ "électricité",
+ -12.905800819396973
+ ],
+ [
+ "▁vanzare",
+ -12.905811309814453
+ ],
+ [
+ "biologi",
+ -12.905961036682129
+ ],
+ [
+ "▁vive",
+ -12.906060218811035
+ ],
+ [
+ "▁Alarm",
+ -12.906097412109375
+ ],
+ [
+ "▁experiență",
+ -12.9061279296875
+ ],
+ [
+ "▁Loch",
+ -12.906133651733398
+ ],
+ [
+ "▁Pedro",
+ -12.906194686889648
+ ],
+ [
+ "▁detergent",
+ -12.906217575073242
+ ],
+ [
+ "language",
+ -12.906554222106934
+ ],
+ [
+ "▁sedan",
+ -12.906655311584473
+ ],
+ [
+ "▁Brady",
+ -12.906736373901367
+ ],
+ [
+ "▁compus",
+ -12.906976699829102
+ ],
+ [
+ "▁landfill",
+ -12.906982421875
+ ],
+ [
+ "giu",
+ -12.907039642333984
+ ],
+ [
+ "beziehung",
+ -12.9070405960083
+ ],
+ [
+ "▁picior",
+ -12.907184600830078
+ ],
+ [
+ "ALI",
+ -12.907235145568848
+ ],
+ [
+ "▁Commander",
+ -12.907256126403809
+ ],
+ [
+ "EPS",
+ -12.907303810119629
+ ],
+ [
+ "▁Textil",
+ -12.907320022583008
+ ],
+ [
+ "▁industria",
+ -12.907339096069336
+ ],
+ [
+ "lox",
+ -12.907365798950195
+ ],
+ [
+ "▁eclectic",
+ -12.907453536987305
+ ],
+ [
+ "▁gracious",
+ -12.907477378845215
+ ],
+ [
+ "Uniunea",
+ -12.907525062561035
+ ],
+ [
+ "bps",
+ -12.90754222869873
+ ],
+ [
+ "▁entertained",
+ -12.907634735107422
+ ],
+ [
+ "depinde",
+ -12.907767295837402
+ ],
+ [
+ "▁daylight",
+ -12.907893180847168
+ ],
+ [
+ "▁résistance",
+ -12.907995223999023
+ ],
+ [
+ "ARN",
+ -12.908194541931152
+ ],
+ [
+ "▁unavailable",
+ -12.908201217651367
+ ],
+ [
+ "Curtea",
+ -12.908390045166016
+ ],
+ [
+ "▁pores",
+ -12.908502578735352
+ ],
+ [
+ "▁Tonight",
+ -12.908649444580078
+ ],
+ [
+ "▁datori",
+ -12.90869426727295
+ ],
+ [
+ "▁gezielt",
+ -12.908703804016113
+ ],
+ [
+ "▁rupture",
+ -12.90875244140625
+ ],
+ [
+ "▁disput",
+ -12.908848762512207
+ ],
+ [
+ "▁sonstige",
+ -12.908895492553711
+ ],
+ [
+ "▁Ordnung",
+ -12.90910816192627
+ ],
+ [
+ "▁beschrieben",
+ -12.909114837646484
+ ],
+ [
+ "▁Rainbow",
+ -12.90911865234375
+ ],
+ [
+ "▁Werkzeug",
+ -12.909136772155762
+ ],
+ [
+ "GIN",
+ -12.909354209899902
+ ],
+ [
+ "facilitating",
+ -12.909490585327148
+ ],
+ [
+ "hunt",
+ -12.90955638885498
+ ],
+ [
+ "▁Serving",
+ -12.909673690795898
+ ],
+ [
+ "Writ",
+ -12.909692764282227
+ ],
+ [
+ "requisite",
+ -12.909798622131348
+ ],
+ [
+ "▁Kerry",
+ -12.90989875793457
+ ],
+ [
+ "▁riesig",
+ -12.909957885742188
+ ],
+ [
+ "▁Healing",
+ -12.91030502319336
+ ],
+ [
+ "▁1954",
+ -12.910365104675293
+ ],
+ [
+ "▁mousse",
+ -12.910428047180176
+ ],
+ [
+ "▁Positive",
+ -12.910764694213867
+ ],
+ [
+ "embodie",
+ -12.910772323608398
+ ],
+ [
+ "▁penetrate",
+ -12.910774230957031
+ ],
+ [
+ "endorsed",
+ -12.910882949829102
+ ],
+ [
+ "▁situatia",
+ -12.910927772521973
+ ],
+ [
+ "▁Unity",
+ -12.911083221435547
+ ],
+ [
+ "142",
+ -12.911102294921875
+ ],
+ [
+ "▁farmhouse",
+ -12.911138534545898
+ ],
+ [
+ "▁Handbook",
+ -12.911368370056152
+ ],
+ [
+ "▁symbolic",
+ -12.911378860473633
+ ],
+ [
+ "pristine",
+ -12.911439895629883
+ ],
+ [
+ "moitié",
+ -12.911595344543457
+ ],
+ [
+ "▁Sessions",
+ -12.912017822265625
+ ],
+ [
+ "technisch",
+ -12.912116050720215
+ ],
+ [
+ "▁lesquel",
+ -12.912148475646973
+ ],
+ [
+ "▁electronically",
+ -12.912208557128906
+ ],
+ [
+ "▁modificat",
+ -12.912240982055664
+ ],
+ [
+ "▁adjoin",
+ -12.912242889404297
+ ],
+ [
+ "actualité",
+ -12.912256240844727
+ ],
+ [
+ "vati",
+ -12.91229248046875
+ ],
+ [
+ "VENT",
+ -12.912299156188965
+ ],
+ [
+ "▁salsa",
+ -12.912333488464355
+ ],
+ [
+ "acupunctur",
+ -12.912424087524414
+ ],
+ [
+ "▁Opportunity",
+ -12.912424087524414
+ ],
+ [
+ "▁Inspection",
+ -12.912425994873047
+ ],
+ [
+ "▁vereinbart",
+ -12.912425994873047
+ ],
+ [
+ "▁Residents",
+ -12.912426948547363
+ ],
+ [
+ "▁perennial",
+ -12.91242790222168
+ ],
+ [
+ "CHAN",
+ -12.912555694580078
+ ],
+ [
+ "Search",
+ -12.912572860717773
+ ],
+ [
+ "UTE",
+ -12.912696838378906
+ ],
+ [
+ "▁Lens",
+ -12.912703514099121
+ ],
+ [
+ "▁Banner",
+ -12.91281509399414
+ ],
+ [
+ "aménagement",
+ -12.912839889526367
+ ],
+ [
+ "▁Decision",
+ -12.91286849975586
+ ],
+ [
+ "▁ferr",
+ -12.912869453430176
+ ],
+ [
+ "▁Transformation",
+ -12.912878036499023
+ ],
+ [
+ "▁Stamm",
+ -12.912955284118652
+ ],
+ [
+ "▁Galerie",
+ -12.913003921508789
+ ],
+ [
+ "onny",
+ -12.913126945495605
+ ],
+ [
+ "▁caption",
+ -12.913195610046387
+ ],
+ [
+ "▁viitorul",
+ -12.91323471069336
+ ],
+ [
+ "▁professionelle",
+ -12.913281440734863
+ ],
+ [
+ "drepturile",
+ -12.913294792175293
+ ],
+ [
+ "ylon",
+ -12.913345336914062
+ ],
+ [
+ "Société",
+ -12.913387298583984
+ ],
+ [
+ "AIS",
+ -12.913456916809082
+ ],
+ [
+ "March",
+ -12.91350269317627
+ ],
+ [
+ "▁Rav",
+ -12.91357707977295
+ ],
+ [
+ "▁1946",
+ -12.913691520690918
+ ],
+ [
+ "accompagnement",
+ -12.913713455200195
+ ],
+ [
+ "Liviu",
+ -12.913716316223145
+ ],
+ [
+ "▁Appeal",
+ -12.913826942443848
+ ],
+ [
+ "▁sentir",
+ -12.913952827453613
+ ],
+ [
+ "▁Indigenous",
+ -12.914087295532227
+ ],
+ [
+ "▁wizard",
+ -12.914087295532227
+ ],
+ [
+ "▁collateral",
+ -12.914127349853516
+ ],
+ [
+ "▁Proof",
+ -12.914324760437012
+ ],
+ [
+ "▁prze",
+ -12.914398193359375
+ ],
+ [
+ "▁obținut",
+ -12.91450309753418
+ ],
+ [
+ "COP",
+ -12.914629936218262
+ ],
+ [
+ "▁obiect",
+ -12.914681434631348
+ ],
+ [
+ "▁isolate",
+ -12.914685249328613
+ ],
+ [
+ "▁nieder",
+ -12.914793014526367
+ ],
+ [
+ "TECH",
+ -12.914953231811523
+ ],
+ [
+ "▁Sharing",
+ -12.914998054504395
+ ],
+ [
+ "Ideally",
+ -12.915008544921875
+ ],
+ [
+ "▁naked",
+ -12.915059089660645
+ ],
+ [
+ "horaire",
+ -12.915130615234375
+ ],
+ [
+ "▁prelucrare",
+ -12.915180206298828
+ ],
+ [
+ "▁forcément",
+ -12.915349006652832
+ ],
+ [
+ "▁ESPN",
+ -12.915403366088867
+ ],
+ [
+ "▁southwest",
+ -12.9154634475708
+ ],
+ [
+ "▁Timber",
+ -12.915682792663574
+ ],
+ [
+ "kleidung",
+ -12.915748596191406
+ ],
+ [
+ "MJ",
+ -12.915854454040527
+ ],
+ [
+ "Ped",
+ -12.915889739990234
+ ],
+ [
+ "▁lymph",
+ -12.916181564331055
+ ],
+ [
+ "wärme",
+ -12.916399002075195
+ ],
+ [
+ "▁Olivia",
+ -12.916610717773438
+ ],
+ [
+ "Ziua",
+ -12.916705131530762
+ ],
+ [
+ "reihe",
+ -12.916747093200684
+ ],
+ [
+ "▁selfish",
+ -12.916752815246582
+ ],
+ [
+ "▁geography",
+ -12.916814804077148
+ ],
+ [
+ "▁etaj",
+ -12.916924476623535
+ ],
+ [
+ "▁acquis",
+ -12.91698932647705
+ ],
+ [
+ "▁rejoin",
+ -12.91701602935791
+ ],
+ [
+ "7.1",
+ -12.917097091674805
+ ],
+ [
+ "▁paix",
+ -12.91713809967041
+ ],
+ [
+ "tirer",
+ -12.917284965515137
+ ],
+ [
+ "▁clase",
+ -12.91745662689209
+ ],
+ [
+ "▁blink",
+ -12.917572021484375
+ ],
+ [
+ "▁Interface",
+ -12.917611122131348
+ ],
+ [
+ "nado",
+ -12.917655944824219
+ ],
+ [
+ "RIT",
+ -12.91777515411377
+ ],
+ [
+ "ESC",
+ -12.918120384216309
+ ],
+ [
+ "▁carving",
+ -12.918190002441406
+ ],
+ [
+ "▁articolul",
+ -12.918194770812988
+ ],
+ [
+ "▁wreath",
+ -12.918258666992188
+ ],
+ [
+ "▁propaganda",
+ -12.918266296386719
+ ],
+ [
+ "▁Pair",
+ -12.918267250061035
+ ],
+ [
+ "▁pamant",
+ -12.91831111907959
+ ],
+ [
+ "▁venituri",
+ -12.918357849121094
+ ],
+ [
+ "rtz",
+ -12.91835880279541
+ ],
+ [
+ "uddle",
+ -12.918529510498047
+ ],
+ [
+ "uille",
+ -12.918543815612793
+ ],
+ [
+ "▁embed",
+ -12.918654441833496
+ ],
+ [
+ "0.05",
+ -12.918655395507812
+ ],
+ [
+ "▁Brighton",
+ -12.918718338012695
+ ],
+ [
+ "estens",
+ -12.918742179870605
+ ],
+ [
+ "▁occupational",
+ -12.918862342834473
+ ],
+ [
+ "ем",
+ -12.918890953063965
+ ],
+ [
+ "wünsche",
+ -12.919081687927246
+ ],
+ [
+ "▁Poetry",
+ -12.91909408569336
+ ],
+ [
+ "▁visualize",
+ -12.919109344482422
+ ],
+ [
+ "Across",
+ -12.919121742248535
+ ],
+ [
+ "▁essentielle",
+ -12.919123649597168
+ ],
+ [
+ "beratung",
+ -12.919143676757812
+ ],
+ [
+ "▁Guidelines",
+ -12.91919231414795
+ ],
+ [
+ "▁Fehl",
+ -12.919198036193848
+ ],
+ [
+ "▁liberty",
+ -12.91921329498291
+ ],
+ [
+ "▁Investigation",
+ -12.91922378540039
+ ],
+ [
+ "▁sunrise",
+ -12.919266700744629
+ ],
+ [
+ "▁12:00",
+ -12.919541358947754
+ ],
+ [
+ "venind",
+ -12.919583320617676
+ ],
+ [
+ "▁lotion",
+ -12.919655799865723
+ ],
+ [
+ "conscious",
+ -12.91968822479248
+ ],
+ [
+ "logists",
+ -12.91973876953125
+ ],
+ [
+ "▁judecător",
+ -12.919893264770508
+ ],
+ [
+ "▁Ecuador",
+ -12.919928550720215
+ ],
+ [
+ "▁ambulance",
+ -12.91994857788086
+ ],
+ [
+ "▁Already",
+ -12.920026779174805
+ ],
+ [
+ "▁eröffnet",
+ -12.920090675354004
+ ],
+ [
+ "▁naval",
+ -12.92010498046875
+ ],
+ [
+ "▁imposibil",
+ -12.92011547088623
+ ],
+ [
+ "▁Merry",
+ -12.92011833190918
+ ],
+ [
+ "▁Duncan",
+ -12.920272827148438
+ ],
+ [
+ "▁léger",
+ -12.9203519821167
+ ],
+ [
+ "▁delta",
+ -12.920391082763672
+ ],
+ [
+ "▁Machinery",
+ -12.920578002929688
+ ],
+ [
+ "▁craftsmanship",
+ -12.920766830444336
+ ],
+ [
+ "▁angezeigt",
+ -12.9207763671875
+ ],
+ [
+ "▁formidable",
+ -12.9207763671875
+ ],
+ [
+ "▁Startup",
+ -12.920878410339355
+ ],
+ [
+ "venus",
+ -12.920969009399414
+ ],
+ [
+ "▁tannin",
+ -12.921019554138184
+ ],
+ [
+ "collaborating",
+ -12.921128273010254
+ ],
+ [
+ "▁abrupt",
+ -12.921152114868164
+ ],
+ [
+ "emergence",
+ -12.921171188354492
+ ],
+ [
+ "Dienstleistungen",
+ -12.921197891235352
+ ],
+ [
+ "▁liefert",
+ -12.921217918395996
+ ],
+ [
+ "engagement",
+ -12.921222686767578
+ ],
+ [
+ "▁maximise",
+ -12.921304702758789
+ ],
+ [
+ "modeled",
+ -12.9214448928833
+ ],
+ [
+ "▁crane",
+ -12.92148208618164
+ ],
+ [
+ "▁effortless",
+ -12.921540260314941
+ ],
+ [
+ "▁Buffet",
+ -12.92160701751709
+ ],
+ [
+ "8000",
+ -12.921648979187012
+ ],
+ [
+ "▁Überblick",
+ -12.921687126159668
+ ],
+ [
+ "micro",
+ -12.921981811523438
+ ],
+ [
+ "▁vergleichen",
+ -12.92204475402832
+ ],
+ [
+ "143",
+ -12.922080993652344
+ ],
+ [
+ "5.6",
+ -12.922094345092773
+ ],
+ [
+ "▁odata",
+ -12.922131538391113
+ ],
+ [
+ "▁interviu",
+ -12.922162055969238
+ ],
+ [
+ "▁poliţi",
+ -12.922375679016113
+ ],
+ [
+ "plated",
+ -12.922383308410645
+ ],
+ [
+ "Roman",
+ -12.922406196594238
+ ],
+ [
+ "▁satisfactory",
+ -12.922453880310059
+ ],
+ [
+ "▁unanimous",
+ -12.922459602355957
+ ],
+ [
+ "▁întâln",
+ -12.922464370727539
+ ],
+ [
+ "nonsense",
+ -12.922558784484863
+ ],
+ [
+ "▁HOW",
+ -12.922616004943848
+ ],
+ [
+ "prezinta",
+ -12.922639846801758
+ ],
+ [
+ "▁măsura",
+ -12.9226655960083
+ ],
+ [
+ "▁Fuji",
+ -12.92275619506836
+ ],
+ [
+ "▁Meaning",
+ -12.92278003692627
+ ],
+ [
+ "aspiring",
+ -12.922850608825684
+ ],
+ [
+ "▁Suceava",
+ -12.922863006591797
+ ],
+ [
+ "arba",
+ -12.922983169555664
+ ],
+ [
+ "pressive",
+ -12.922988891601562
+ ],
+ [
+ "▁creek",
+ -12.92301082611084
+ ],
+ [
+ "trakt",
+ -12.923023223876953
+ ],
+ [
+ "▁fluffy",
+ -12.923303604125977
+ ],
+ [
+ "▁bateau",
+ -12.923371315002441
+ ],
+ [
+ "ме",
+ -12.923545837402344
+ ],
+ [
+ "UNG",
+ -12.923609733581543
+ ],
+ [
+ "motifs",
+ -12.923907279968262
+ ],
+ [
+ "Type",
+ -12.923958778381348
+ ],
+ [
+ "perçu",
+ -12.924132347106934
+ ],
+ [
+ "singurul",
+ -12.924139022827148
+ ],
+ [
+ "▁(2011)",
+ -12.92418384552002
+ ],
+ [
+ "▁hemp",
+ -12.924263954162598
+ ],
+ [
+ "betroffenen",
+ -12.92431640625
+ ],
+ [
+ "▁sermon",
+ -12.924369812011719
+ ],
+ [
+ "AID",
+ -12.924545288085938
+ ],
+ [
+ "3.7",
+ -12.924627304077148
+ ],
+ [
+ "▁heiß",
+ -12.92463207244873
+ ],
+ [
+ "▁bolnav",
+ -12.924982070922852
+ ],
+ [
+ "First",
+ -12.924995422363281
+ ],
+ [
+ "▁interrupt",
+ -12.925040245056152
+ ],
+ [
+ "phag",
+ -12.925106048583984
+ ],
+ [
+ "235",
+ -12.925201416015625
+ ],
+ [
+ "▁discoveries",
+ -12.925262451171875
+ ],
+ [
+ "▁Wellington",
+ -12.925263404846191
+ ],
+ [
+ "▁wechseln",
+ -12.925298690795898
+ ],
+ [
+ "▁strategically",
+ -12.925379753112793
+ ],
+ [
+ "▁iphone",
+ -12.925440788269043
+ ],
+ [
+ "geteilt",
+ -12.925646781921387
+ ],
+ [
+ "generative",
+ -12.925748825073242
+ ],
+ [
+ "▁Monroe",
+ -12.925806045532227
+ ],
+ [
+ "▁Execut",
+ -12.925863265991211
+ ],
+ [
+ "▁knitting",
+ -12.925931930541992
+ ],
+ [
+ "▁Couple",
+ -12.925939559936523
+ ],
+ [
+ "▁Shade",
+ -12.926020622253418
+ ],
+ [
+ "▁Taj",
+ -12.926060676574707
+ ],
+ [
+ "950",
+ -12.926077842712402
+ ],
+ [
+ "boiled",
+ -12.92609977722168
+ ],
+ [
+ "▁mixes",
+ -12.926130294799805
+ ],
+ [
+ "betroffene",
+ -12.926156044006348
+ ],
+ [
+ "▁continuation",
+ -12.926169395446777
+ ],
+ [
+ "▁begleitet",
+ -12.926226615905762
+ ],
+ [
+ "▁numerical",
+ -12.926281929016113
+ ],
+ [
+ "▁(2013)",
+ -12.92630386352539
+ ],
+ [
+ "▁nourish",
+ -12.926399230957031
+ ],
+ [
+ "oricar",
+ -12.926485061645508
+ ],
+ [
+ "focus",
+ -12.926486015319824
+ ],
+ [
+ "▁Crazy",
+ -12.926651000976562
+ ],
+ [
+ "▁ascend",
+ -12.926671028137207
+ ],
+ [
+ "▁vinde",
+ -12.926855087280273
+ ],
+ [
+ "roar",
+ -12.926874160766602
+ ],
+ [
+ "Vac",
+ -12.926929473876953
+ ],
+ [
+ "▁Zuschauer",
+ -12.927068710327148
+ ],
+ [
+ "izeze",
+ -12.927179336547852
+ ],
+ [
+ "▁Mindest",
+ -12.92721939086914
+ ],
+ [
+ "lingual",
+ -12.927229881286621
+ ],
+ [
+ "▁violet",
+ -12.927264213562012
+ ],
+ [
+ "▁Opfer",
+ -12.927299499511719
+ ],
+ [
+ "ARS",
+ -12.927431106567383
+ ],
+ [
+ "4.7",
+ -12.92744255065918
+ ],
+ [
+ "millennial",
+ -12.927492141723633
+ ],
+ [
+ "▁striv",
+ -12.927639961242676
+ ],
+ [
+ "▁bishop",
+ -12.927680015563965
+ ],
+ [
+ "▁Durham",
+ -12.927708625793457
+ ],
+ [
+ "opathic",
+ -12.927817344665527
+ ],
+ [
+ "Where",
+ -12.927999496459961
+ ],
+ [
+ "▁Rider",
+ -12.928030014038086
+ ],
+ [
+ "▁Reid",
+ -12.928030967712402
+ ],
+ [
+ "stumbled",
+ -12.928156852722168
+ ],
+ [
+ "deep",
+ -12.92827320098877
+ ],
+ [
+ "▁11:00",
+ -12.928340911865234
+ ],
+ [
+ "▁Essex",
+ -12.928380966186523
+ ],
+ [
+ "▁Analyst",
+ -12.928397178649902
+ ],
+ [
+ "feel",
+ -12.928546905517578
+ ],
+ [
+ "▁rave",
+ -12.928601264953613
+ ],
+ [
+ "▁Eddie",
+ -12.928631782531738
+ ],
+ [
+ "▁communiqué",
+ -12.928756713867188
+ ],
+ [
+ "[/",
+ -12.928791046142578
+ ],
+ [
+ "▁Tho",
+ -12.929011344909668
+ ],
+ [
+ "ffentlichkeit",
+ -12.929019927978516
+ ],
+ [
+ "instrument",
+ -12.929126739501953
+ ],
+ [
+ "▁metropolitan",
+ -12.929179191589355
+ ],
+ [
+ "▁experienţ",
+ -12.929181098937988
+ ],
+ [
+ "East",
+ -12.929198265075684
+ ],
+ [
+ "Compared",
+ -12.929434776306152
+ ],
+ [
+ "worn",
+ -12.929484367370605
+ ],
+ [
+ "berufliche",
+ -12.92966365814209
+ ],
+ [
+ "▁Umstände",
+ -12.929710388183594
+ ],
+ [
+ "individuellen",
+ -12.929901123046875
+ ],
+ [
+ "siehe",
+ -12.929912567138672
+ ],
+ [
+ "▁sfarsit",
+ -12.929969787597656
+ ],
+ [
+ "▁Strength",
+ -12.929999351501465
+ ],
+ [
+ "▁prejudice",
+ -12.930024147033691
+ ],
+ [
+ "▁shutdown",
+ -12.930159568786621
+ ],
+ [
+ "chatting",
+ -12.93022346496582
+ ],
+ [
+ "▁Gerne",
+ -12.930227279663086
+ ],
+ [
+ "▁Yum",
+ -12.930305480957031
+ ],
+ [
+ "▁coastline",
+ -12.930387496948242
+ ],
+ [
+ "▁headboard",
+ -12.930623054504395
+ ],
+ [
+ "▁politische",
+ -12.930768966674805
+ ],
+ [
+ "Sub",
+ -12.930838584899902
+ ],
+ [
+ "▁Henderson",
+ -12.930870056152344
+ ],
+ [
+ "▁astonishing",
+ -12.930870056152344
+ ],
+ [
+ "▁Dresden",
+ -12.930871963500977
+ ],
+ [
+ "▁strawberry",
+ -12.93088436126709
+ ],
+ [
+ "prenez",
+ -12.930889129638672
+ ],
+ [
+ "▁Monaco",
+ -12.930912971496582
+ ],
+ [
+ "▁empowered",
+ -12.930953025817871
+ ],
+ [
+ "fäl",
+ -12.93109130859375
+ ],
+ [
+ "▁creier",
+ -12.931120872497559
+ ],
+ [
+ "▁Equ",
+ -12.931300163269043
+ ],
+ [
+ "▁Selling",
+ -12.931379318237305
+ ],
+ [
+ "▁$35",
+ -12.931483268737793
+ ],
+ [
+ "konto",
+ -12.931503295898438
+ ],
+ [
+ "▁Procedure",
+ -12.931715965270996
+ ],
+ [
+ "▁reduziert",
+ -12.931715965270996
+ ],
+ [
+ "▁royalty",
+ -12.931740760803223
+ ],
+ [
+ "wyn",
+ -12.931756019592285
+ ],
+ [
+ "▁Unfall",
+ -12.932141304016113
+ ],
+ [
+ "NAT",
+ -12.932161331176758
+ ],
+ [
+ "▁grafic",
+ -12.93251895904541
+ ],
+ [
+ "▁Collective",
+ -12.932563781738281
+ ],
+ [
+ "▁Computing",
+ -12.932564735412598
+ ],
+ [
+ "▁Established",
+ -12.932594299316406
+ ],
+ [
+ "▁zest",
+ -12.932598114013672
+ ],
+ [
+ "venez",
+ -12.932611465454102
+ ],
+ [
+ "follow",
+ -12.9326171875
+ ],
+ [
+ "▁Motivation",
+ -12.932640075683594
+ ],
+ [
+ "▁dictator",
+ -12.932755470275879
+ ],
+ [
+ "whichever",
+ -12.93281078338623
+ ],
+ [
+ "▁întâmpl",
+ -12.93293285369873
+ ],
+ [
+ "Flüchtling",
+ -12.932987213134766
+ ],
+ [
+ "EMI",
+ -12.933015823364258
+ ],
+ [
+ "404",
+ -12.933019638061523
+ ],
+ [
+ "ICK",
+ -12.93302059173584
+ ],
+ [
+ "emplacement",
+ -12.933191299438477
+ ],
+ [
+ "complete",
+ -12.933349609375
+ ],
+ [
+ "advising",
+ -12.933412551879883
+ ],
+ [
+ "▁Administrative",
+ -12.933481216430664
+ ],
+ [
+ "▁deviation",
+ -12.933496475219727
+ ],
+ [
+ "▁experienț",
+ -12.933500289916992
+ ],
+ [
+ "lethor",
+ -12.933996200561523
+ ],
+ [
+ "▁compress",
+ -12.934081077575684
+ ],
+ [
+ "rival",
+ -12.934173583984375
+ ],
+ [
+ "reprendre",
+ -12.934186935424805
+ ],
+ [
+ "ugi",
+ -12.934266090393066
+ ],
+ [
+ "▁Invitation",
+ -12.934267044067383
+ ],
+ [
+ "▁retina",
+ -12.934332847595215
+ ],
+ [
+ "▁farther",
+ -12.934335708618164
+ ],
+ [
+ "▁fenêtre",
+ -12.934799194335938
+ ],
+ [
+ "6-7",
+ -12.934815406799316
+ ],
+ [
+ "zhou",
+ -12.934834480285645
+ ],
+ [
+ "▁Piano",
+ -12.934840202331543
+ ],
+ [
+ "▁Congrats",
+ -12.935114860534668
+ ],
+ [
+ "▁Configur",
+ -12.935131072998047
+ ],
+ [
+ "▁superficial",
+ -12.935179710388184
+ ],
+ [
+ "▁melting",
+ -12.935315132141113
+ ],
+ [
+ "▁raspunde",
+ -12.935626983642578
+ ],
+ [
+ "▁drip",
+ -12.93564224243164
+ ],
+ [
+ "östlich",
+ -12.9358491897583
+ ],
+ [
+ "189",
+ -12.935925483703613
+ ],
+ [
+ "▁Ludwig",
+ -12.935959815979004
+ ],
+ [
+ "▁keto",
+ -12.935985565185547
+ ],
+ [
+ "▁Bogdan",
+ -12.936013221740723
+ ],
+ [
+ "▁contracted",
+ -12.936029434204102
+ ],
+ [
+ "▁revive",
+ -12.936100006103516
+ ],
+ [
+ "▁cristal",
+ -12.936232566833496
+ ],
+ [
+ "▁mailbox",
+ -12.936257362365723
+ ],
+ [
+ "președintele",
+ -12.936559677124023
+ ],
+ [
+ "▁seekers",
+ -12.936627388000488
+ ],
+ [
+ "func",
+ -12.936904907226562
+ ],
+ [
+ "▁Markus",
+ -12.93691349029541
+ ],
+ [
+ "Unter",
+ -12.936923027038574
+ ],
+ [
+ "▁übertragen",
+ -12.937003135681152
+ ],
+ [
+ "▁adaptive",
+ -12.937024116516113
+ ],
+ [
+ "caster",
+ -12.937051773071289
+ ],
+ [
+ "▁geek",
+ -12.937164306640625
+ ],
+ [
+ "▁réservation",
+ -12.937236785888672
+ ],
+ [
+ "▁irritation",
+ -12.937240600585938
+ ],
+ [
+ "▁HDMI",
+ -12.937346458435059
+ ],
+ [
+ "Seeing",
+ -12.937485694885254
+ ],
+ [
+ "▁genul",
+ -12.937569618225098
+ ],
+ [
+ "▁catastrophe",
+ -12.937662124633789
+ ],
+ [
+ "▁Tweet",
+ -12.937665939331055
+ ],
+ [
+ "TZ",
+ -12.937729835510254
+ ],
+ [
+ "▁credible",
+ -12.937946319580078
+ ],
+ [
+ "▁cobor",
+ -12.938064575195312
+ ],
+ [
+ "▁realizeaz",
+ -12.938159942626953
+ ],
+ [
+ "journal",
+ -12.938274383544922
+ ],
+ [
+ "▁shaking",
+ -12.938532829284668
+ ],
+ [
+ "3-6",
+ -12.938572883605957
+ ],
+ [
+ "▁beneficiaz",
+ -12.938605308532715
+ ],
+ [
+ "▁Frankreich",
+ -12.938633918762207
+ ],
+ [
+ "committing",
+ -12.9386568069458
+ ],
+ [
+ "AMS",
+ -12.938835144042969
+ ],
+ [
+ "▁Feli",
+ -12.939007759094238
+ ],
+ [
+ "▁Producer",
+ -12.939023971557617
+ ],
+ [
+ "▁übrig",
+ -12.93940544128418
+ ],
+ [
+ "gemeinde",
+ -12.939593315124512
+ ],
+ [
+ "should",
+ -12.939799308776855
+ ],
+ [
+ "▁neurons",
+ -12.939799308776855
+ ],
+ [
+ "▁Agenda",
+ -12.939833641052246
+ ],
+ [
+ "▁hashtag",
+ -12.939896583557129
+ ],
+ [
+ "▁confortabil",
+ -12.939897537231445
+ ],
+ [
+ "520",
+ -12.940008163452148
+ ],
+ [
+ "bonded",
+ -12.940033912658691
+ ],
+ [
+ "▁următoare",
+ -12.940191268920898
+ ],
+ [
+ "▁volatile",
+ -12.940223693847656
+ ],
+ [
+ "infamous",
+ -12.940225601196289
+ ],
+ [
+ "seară",
+ -12.940229415893555
+ ],
+ [
+ "▁Sorge",
+ -12.940346717834473
+ ],
+ [
+ "▁Beiträge",
+ -12.940420150756836
+ ],
+ [
+ "▁îndeplin",
+ -12.940449714660645
+ ],
+ [
+ "gespräch",
+ -12.940649032592773
+ ],
+ [
+ "▁joueur",
+ -12.940701484680176
+ ],
+ [
+ "▁outsourcing",
+ -12.940701484680176
+ ],
+ [
+ "▁Guvernul",
+ -12.940814018249512
+ ],
+ [
+ "6-2",
+ -12.940818786621094
+ ],
+ [
+ "▁prioritize",
+ -12.941068649291992
+ ],
+ [
+ "▁duminică",
+ -12.941076278686523
+ ],
+ [
+ "▁resignation",
+ -12.941076278686523
+ ],
+ [
+ "▁Converter",
+ -12.941079139709473
+ ],
+ [
+ "hereby",
+ -12.941155433654785
+ ],
+ [
+ "▁stresses",
+ -12.941299438476562
+ ],
+ [
+ "▁brun",
+ -12.941415786743164
+ ],
+ [
+ "▁elev",
+ -12.941423416137695
+ ],
+ [
+ "▁Skip",
+ -12.941479682922363
+ ],
+ [
+ "540",
+ -12.941499710083008
+ ],
+ [
+ "TURE",
+ -12.941603660583496
+ ],
+ [
+ "▁Lynch",
+ -12.941635131835938
+ ],
+ [
+ "▁preveni",
+ -12.941643714904785
+ ],
+ [
+ "compatible",
+ -12.941692352294922
+ ],
+ [
+ "surveyed",
+ -12.941702842712402
+ ],
+ [
+ "▁Ausnahme",
+ -12.941713333129883
+ ],
+ [
+ "▁medicul",
+ -12.941812515258789
+ ],
+ [
+ "▁subtil",
+ -12.941865921020508
+ ],
+ [
+ "▁Quali",
+ -12.941890716552734
+ ],
+ [
+ "▁techno",
+ -12.941900253295898
+ ],
+ [
+ "presently",
+ -12.94193172454834
+ ],
+ [
+ "▁Müller",
+ -12.941934585571289
+ ],
+ [
+ "DIRECT",
+ -12.941937446594238
+ ],
+ [
+ "schuld",
+ -12.941944122314453
+ ],
+ [
+ "▁Bloomberg",
+ -12.941994667053223
+ ],
+ [
+ "feuer",
+ -12.942181587219238
+ ],
+ [
+ "▁Pharmacy",
+ -12.942270278930664
+ ],
+ [
+ "▁Schnitt",
+ -12.942301750183105
+ ],
+ [
+ "186",
+ -12.942333221435547
+ ],
+ [
+ "peaks",
+ -12.942355155944824
+ ],
+ [
+ "▁Gemeinsam",
+ -12.94235897064209
+ ],
+ [
+ "▁récemment",
+ -12.94235897064209
+ ],
+ [
+ "▁Pascal",
+ -12.942490577697754
+ ],
+ [
+ "filmed",
+ -12.942523956298828
+ ],
+ [
+ "RCA",
+ -12.942548751831055
+ ],
+ [
+ "▁virtuelle",
+ -12.942622184753418
+ ],
+ [
+ "▁dotat",
+ -12.942630767822266
+ ],
+ [
+ "logisch",
+ -12.942717552185059
+ ],
+ [
+ "▁Luck",
+ -12.943005561828613
+ ],
+ [
+ "cosy",
+ -12.943132400512695
+ ],
+ [
+ "▁Awareness",
+ -12.943216323852539
+ ],
+ [
+ "▁gesetzlich",
+ -12.943263053894043
+ ],
+ [
+ "padded",
+ -12.943306922912598
+ ],
+ [
+ "▁Lotus",
+ -12.943395614624023
+ ],
+ [
+ "urging",
+ -12.9434175491333
+ ],
+ [
+ "▁mushroom",
+ -12.943426132202148
+ ],
+ [
+ "▁adultes",
+ -12.943527221679688
+ ],
+ [
+ "▁Coca",
+ -12.943571090698242
+ ],
+ [
+ "▁recev",
+ -12.943586349487305
+ ],
+ [
+ "▁mantra",
+ -12.943610191345215
+ ],
+ [
+ "▁practise",
+ -12.943644523620605
+ ],
+ [
+ "▁acceler",
+ -12.943663597106934
+ ],
+ [
+ "bolster",
+ -12.943756103515625
+ ],
+ [
+ "▁compressed",
+ -12.943818092346191
+ ],
+ [
+ "TIN",
+ -12.943899154663086
+ ],
+ [
+ "▁aromatic",
+ -12.944236755371094
+ ],
+ [
+ "geleitet",
+ -12.944408416748047
+ ],
+ [
+ "▁fibr",
+ -12.944443702697754
+ ],
+ [
+ "exécut",
+ -12.94444751739502
+ ],
+ [
+ "▁unconscious",
+ -12.94456958770752
+ ],
+ [
+ "HAR",
+ -12.944607734680176
+ ],
+ [
+ "▁Gregory",
+ -12.944661140441895
+ ],
+ [
+ "▁Manila",
+ -12.944738388061523
+ ],
+ [
+ "ozitate",
+ -12.944756507873535
+ ],
+ [
+ "exemplary",
+ -12.944803237915039
+ ],
+ [
+ "éventuel",
+ -12.944906234741211
+ ],
+ [
+ "▁Craciun",
+ -12.944930076599121
+ ],
+ [
+ "▁tehnologii",
+ -12.944931030273438
+ ],
+ [
+ "▁Despre",
+ -12.945138931274414
+ ],
+ [
+ "▁1917",
+ -12.945141792297363
+ ],
+ [
+ "▁upfront",
+ -12.945146560668945
+ ],
+ [
+ "▁Iulia",
+ -12.945280075073242
+ ],
+ [
+ "▁erwähnt",
+ -12.945359230041504
+ ],
+ [
+ "▁magnesium",
+ -12.945359230041504
+ ],
+ [
+ "▁descriptive",
+ -12.94536304473877
+ ],
+ [
+ "▁consumul",
+ -12.945364952087402
+ ],
+ [
+ "▁10-15",
+ -12.945423126220703
+ ],
+ [
+ "▁erfüllen",
+ -12.945611953735352
+ ],
+ [
+ "gig",
+ -12.945657730102539
+ ],
+ [
+ "430",
+ -12.945765495300293
+ ],
+ [
+ "▁Migration",
+ -12.945789337158203
+ ],
+ [
+ "bră",
+ -12.94579029083252
+ ],
+ [
+ "▁réforme",
+ -12.945863723754883
+ ],
+ [
+ "▁york",
+ -12.94610595703125
+ ],
+ [
+ "dritten",
+ -12.946109771728516
+ ],
+ [
+ "cumva",
+ -12.946182250976562
+ ],
+ [
+ "▁Alumni",
+ -12.946218490600586
+ ],
+ [
+ "▁Ceramic",
+ -12.946222305297852
+ ],
+ [
+ "▁rappelle",
+ -12.946236610412598
+ ],
+ [
+ "▁pianist",
+ -12.946248054504395
+ ],
+ [
+ "twisted",
+ -12.946306228637695
+ ],
+ [
+ "earned",
+ -12.946432113647461
+ ],
+ [
+ "▁Hose",
+ -12.946514129638672
+ ],
+ [
+ "156",
+ -12.946610450744629
+ ],
+ [
+ "▁Salmon",
+ -12.946687698364258
+ ],
+ [
+ "Level",
+ -12.946913719177246
+ ],
+ [
+ "▁swirl",
+ -12.947052001953125
+ ],
+ [
+ "erfahrung",
+ -12.947061538696289
+ ],
+ [
+ "▁liabilities",
+ -12.947078704833984
+ ],
+ [
+ "praxis",
+ -12.9470853805542
+ ],
+ [
+ "IPO",
+ -12.947089195251465
+ ],
+ [
+ "▁screaming",
+ -12.947092056274414
+ ],
+ [
+ "emphasized",
+ -12.947200775146484
+ ],
+ [
+ "DEA",
+ -12.947260856628418
+ ],
+ [
+ "▁dermatolog",
+ -12.947351455688477
+ ],
+ [
+ "▁pacate",
+ -12.947498321533203
+ ],
+ [
+ "▁ansamblu",
+ -12.947507858276367
+ ],
+ [
+ "▁beteiligt",
+ -12.947509765625
+ ],
+ [
+ "▁Needles",
+ -12.947574615478516
+ ],
+ [
+ "▁organisiert",
+ -12.947607040405273
+ ],
+ [
+ "Pacific",
+ -12.947639465332031
+ ],
+ [
+ "actual",
+ -12.947823524475098
+ ],
+ [
+ "prindere",
+ -12.94801139831543
+ ],
+ [
+ "▁Indoor",
+ -12.948348045349121
+ ],
+ [
+ "▁Gewalt",
+ -12.948431015014648
+ ],
+ [
+ "▁rezid",
+ -12.948507308959961
+ ],
+ [
+ "censor",
+ -12.948522567749023
+ ],
+ [
+ "▁unlawful",
+ -12.94882869720459
+ ],
+ [
+ "▁Explain",
+ -12.948873519897461
+ ],
+ [
+ "▁Flame",
+ -12.948897361755371
+ ],
+ [
+ "▁brachte",
+ -12.948941230773926
+ ],
+ [
+ "▁Mustang",
+ -12.94899845123291
+ ],
+ [
+ "ectomy",
+ -12.949044227600098
+ ],
+ [
+ "▁deliberate",
+ -12.949064254760742
+ ],
+ [
+ "▁sparkle",
+ -12.949225425720215
+ ],
+ [
+ "▁inchis",
+ -12.94926929473877
+ ],
+ [
+ "▁Cristian",
+ -12.949289321899414
+ ],
+ [
+ "▁facture",
+ -12.949291229248047
+ ],
+ [
+ "▁Grundstück",
+ -12.949292182922363
+ ],
+ [
+ "außerhalb",
+ -12.949300765991211
+ ],
+ [
+ "coast",
+ -12.949321746826172
+ ],
+ [
+ "anilor",
+ -12.949396133422852
+ ],
+ [
+ "255",
+ -12.94952392578125
+ ],
+ [
+ "nterdisciplinary",
+ -12.949576377868652
+ ],
+ [
+ "▁Isabel",
+ -12.949655532836914
+ ],
+ [
+ "▁Städte",
+ -12.949701309204102
+ ],
+ [
+ "▁cicl",
+ -12.949837684631348
+ ],
+ [
+ "▁Zeug",
+ -12.949905395507812
+ ],
+ [
+ "▁Muskel",
+ -12.949951171875
+ ],
+ [
+ "▁indirectly",
+ -12.950051307678223
+ ],
+ [
+ "▁Vorbereitung",
+ -12.950093269348145
+ ],
+ [
+ "MMA",
+ -12.95012378692627
+ ],
+ [
+ "▁pudding",
+ -12.950197219848633
+ ],
+ [
+ "rax",
+ -12.950389862060547
+ ],
+ [
+ "▁Stimmung",
+ -12.95052433013916
+ ],
+ [
+ "▁hierarchy",
+ -12.95052433013916
+ ],
+ [
+ "partie",
+ -12.950597763061523
+ ],
+ [
+ "▁elevate",
+ -12.950685501098633
+ ],
+ [
+ "▁Persian",
+ -12.950690269470215
+ ],
+ [
+ "forensic",
+ -12.95077896118164
+ ],
+ [
+ "Become",
+ -12.950854301452637
+ ],
+ [
+ "leicht",
+ -12.9508695602417
+ ],
+ [
+ "▁staging",
+ -12.950942039489746
+ ],
+ [
+ "▁fühlt",
+ -12.950965881347656
+ ],
+ [
+ "fenster",
+ -12.950979232788086
+ ],
+ [
+ "▁unbelievable",
+ -12.951089859008789
+ ],
+ [
+ "„",
+ -12.951260566711426
+ ],
+ [
+ "▁Guatemala",
+ -12.951387405395508
+ ],
+ [
+ "LET",
+ -12.95141315460205
+ ],
+ [
+ "▁buff",
+ -12.951454162597656
+ ],
+ [
+ "▁Primul",
+ -12.951626777648926
+ ],
+ [
+ "▁mainland",
+ -12.951702117919922
+ ],
+ [
+ "campus",
+ -12.951923370361328
+ ],
+ [
+ "▁gefällt",
+ -12.952075958251953
+ ],
+ [
+ "BAN",
+ -12.952153205871582
+ ],
+ [
+ "finish",
+ -12.952229499816895
+ ],
+ [
+ "accustomed",
+ -12.952251434326172
+ ],
+ [
+ "▁Businesses",
+ -12.95234203338623
+ ],
+ [
+ "▁întreb",
+ -12.95239543914795
+ ],
+ [
+ "▁recomandă",
+ -12.952425956726074
+ ],
+ [
+ "▁pellet",
+ -12.952474594116211
+ ],
+ [
+ "▁GST",
+ -12.952507972717285
+ ],
+ [
+ "SEA",
+ -12.952601432800293
+ ],
+ [
+ "▁categorie",
+ -12.952631950378418
+ ],
+ [
+ "▁convainc",
+ -12.95268440246582
+ ],
+ [
+ "▁considéré",
+ -12.952739715576172
+ ],
+ [
+ "rois",
+ -12.952853202819824
+ ],
+ [
+ "▁thrust",
+ -12.952898979187012
+ ],
+ [
+ "ijk",
+ -12.953001022338867
+ ],
+ [
+ "gefüllt",
+ -12.953118324279785
+ ],
+ [
+ "▁situatii",
+ -12.953327178955078
+ ],
+ [
+ "▁Jacksonville",
+ -12.95337200164795
+ ],
+ [
+ "▁bakery",
+ -12.953473091125488
+ ],
+ [
+ "▁Accident",
+ -12.953554153442383
+ ],
+ [
+ "▁urmeaza",
+ -12.953572273254395
+ ],
+ [
+ "▁crib",
+ -12.953593254089355
+ ],
+ [
+ "getroffen",
+ -12.953707695007324
+ ],
+ [
+ "Based",
+ -12.953877449035645
+ ],
+ [
+ "Including",
+ -12.95398235321045
+ ],
+ [
+ "▁Morocco",
+ -12.95398235321045
+ ],
+ [
+ "▁casserole",
+ -12.95398235321045
+ ],
+ [
+ "▁enquiry",
+ -12.953983306884766
+ ],
+ [
+ "▁pahar",
+ -12.954017639160156
+ ],
+ [
+ "▁Unternehmer",
+ -12.954025268554688
+ ],
+ [
+ "électro",
+ -12.954068183898926
+ ],
+ [
+ "Marie",
+ -12.95413589477539
+ ],
+ [
+ "▁Sno",
+ -12.954153060913086
+ ],
+ [
+ "▁prostate",
+ -12.954168319702148
+ ],
+ [
+ "▁Wallace",
+ -12.95426082611084
+ ],
+ [
+ "empre",
+ -12.954402923583984
+ ],
+ [
+ "▁Multumesc",
+ -12.954415321350098
+ ],
+ [
+ "White",
+ -12.954675674438477
+ ],
+ [
+ "brief",
+ -12.954751014709473
+ ],
+ [
+ "▁kitten",
+ -12.954751014709473
+ ],
+ [
+ "füh",
+ -12.954780578613281
+ ],
+ [
+ "▁mankind",
+ -12.954821586608887
+ ],
+ [
+ "ENE",
+ -12.95483112335205
+ ],
+ [
+ "▁Ethics",
+ -12.954848289489746
+ ],
+ [
+ "▁Realty",
+ -12.954946517944336
+ ],
+ [
+ "▁Emerg",
+ -12.954988479614258
+ ],
+ [
+ "7-8",
+ -12.955055236816406
+ ],
+ [
+ "museum",
+ -12.955096244812012
+ ],
+ [
+ "BRE",
+ -12.95518970489502
+ ],
+ [
+ "▁kilometri",
+ -12.955282211303711
+ ],
+ [
+ "oyaume",
+ -12.955286026000977
+ ],
+ [
+ "▁Cambodia",
+ -12.955288887023926
+ ],
+ [
+ "▁bruit",
+ -12.955304145812988
+ ],
+ [
+ "▁sépar",
+ -12.955334663391113
+ ],
+ [
+ "mastered",
+ -12.9554443359375
+ ],
+ [
+ "shake",
+ -12.955608367919922
+ ],
+ [
+ "▁liaison",
+ -12.955718994140625
+ ],
+ [
+ "▁Boulder",
+ -12.955719947814941
+ ],
+ [
+ "▁tortilla",
+ -12.955720901489258
+ ],
+ [
+ "▁Fokus",
+ -12.955731391906738
+ ],
+ [
+ "▁Blair",
+ -12.95573902130127
+ ],
+ [
+ "▁disturbance",
+ -12.955775260925293
+ ],
+ [
+ "geladen",
+ -12.955843925476074
+ ],
+ [
+ "▁sunscreen",
+ -12.955886840820312
+ ],
+ [
+ "▁reuș",
+ -12.955896377563477
+ ],
+ [
+ "▁Braun",
+ -12.956155776977539
+ ],
+ [
+ "▁existente",
+ -12.956157684326172
+ ],
+ [
+ "stift",
+ -12.956242561340332
+ ],
+ [
+ "▁preot",
+ -12.956387519836426
+ ],
+ [
+ "▁doved",
+ -12.956445693969727
+ ],
+ [
+ "sexual",
+ -12.956488609313965
+ ],
+ [
+ "meanwhile",
+ -12.956583976745605
+ ],
+ [
+ "▁legislature",
+ -12.956583976745605
+ ],
+ [
+ "▁vermeiden",
+ -12.956583976745605
+ ],
+ [
+ "▁inequality",
+ -12.95687484741211
+ ],
+ [
+ "▁turc",
+ -12.956881523132324
+ ],
+ [
+ "ви",
+ -12.95698070526123
+ ],
+ [
+ "▁Kontrolle",
+ -12.95702075958252
+ ],
+ [
+ "▁Ursache",
+ -12.95704174041748
+ ],
+ [
+ "▁confess",
+ -12.95704174041748
+ ],
+ [
+ "▁poetic",
+ -12.957109451293945
+ ],
+ [
+ "attention",
+ -12.957236289978027
+ ],
+ [
+ "textured",
+ -12.957386016845703
+ ],
+ [
+ "GES",
+ -12.957586288452148
+ ],
+ [
+ "6-4",
+ -12.957637786865234
+ ],
+ [
+ "Ray",
+ -12.957696914672852
+ ],
+ [
+ "chromat",
+ -12.957745552062988
+ ],
+ [
+ "▁insightful",
+ -12.957775115966797
+ ],
+ [
+ "▁Navigation",
+ -12.957887649536133
+ ],
+ [
+ "▁destiny",
+ -12.957887649536133
+ ],
+ [
+ "▁ergeben",
+ -12.957892417907715
+ ],
+ [
+ "▁versteh",
+ -12.958090782165527
+ ],
+ [
+ "301",
+ -12.958209037780762
+ ],
+ [
+ "▁Exterior",
+ -12.958321571350098
+ ],
+ [
+ "église",
+ -12.958322525024414
+ ],
+ [
+ "▁Failure",
+ -12.958322525024414
+ ],
+ [
+ "▁Patricia",
+ -12.958324432373047
+ ],
+ [
+ "▁geschützt",
+ -12.958328247070312
+ ],
+ [
+ "intrarea",
+ -12.95833969116211
+ ],
+ [
+ "▁Forward",
+ -12.958368301391602
+ ],
+ [
+ "▁Portrait",
+ -12.95844841003418
+ ],
+ [
+ "▁enregistré",
+ -12.958480834960938
+ ],
+ [
+ "▁wagon",
+ -12.958620071411133
+ ],
+ [
+ "stealing",
+ -12.958879470825195
+ ],
+ [
+ "▁Numero",
+ -12.958880424499512
+ ],
+ [
+ "▁tradui",
+ -12.958986282348633
+ ],
+ [
+ "▁klassische",
+ -12.959033966064453
+ ],
+ [
+ "▁profitieren",
+ -12.959043502807617
+ ],
+ [
+ "▁laboratories",
+ -12.95919132232666
+ ],
+ [
+ "▁reconnaissance",
+ -12.95919132232666
+ ],
+ [
+ "ку",
+ -12.959314346313477
+ ],
+ [
+ "▁Petersburg",
+ -12.959359169006348
+ ],
+ [
+ "▁fertility",
+ -12.959421157836914
+ ],
+ [
+ "▁Understand",
+ -12.959516525268555
+ ],
+ [
+ "dehors",
+ -12.959746360778809
+ ],
+ [
+ "▁Knox",
+ -12.959762573242188
+ ],
+ [
+ "software",
+ -12.959797859191895
+ ],
+ [
+ "▁Celebration",
+ -12.959823608398438
+ ],
+ [
+ "4.6",
+ -12.959897994995117
+ ],
+ [
+ "quino",
+ -12.959930419921875
+ ],
+ [
+ "▁endeavour",
+ -12.960073471069336
+ ],
+ [
+ "▁temptation",
+ -12.960136413574219
+ ],
+ [
+ "▁Registry",
+ -12.96035385131836
+ ],
+ [
+ "IMP",
+ -12.960502624511719
+ ],
+ [
+ "bedingt",
+ -12.960625648498535
+ ],
+ [
+ "▁$60",
+ -12.960846900939941
+ ],
+ [
+ "▁Kriterien",
+ -12.96093463897705
+ ],
+ [
+ "▁strawberries",
+ -12.960943222045898
+ ],
+ [
+ "▁conspiracy",
+ -12.96094799041748
+ ],
+ [
+ "▁pouch",
+ -12.960976600646973
+ ],
+ [
+ "▁Alexandria",
+ -12.961017608642578
+ ],
+ [
+ "▁Mick",
+ -12.961102485656738
+ ],
+ [
+ "extra",
+ -12.961114883422852
+ ],
+ [
+ "▁Operator",
+ -12.961151123046875
+ ],
+ [
+ "enduring",
+ -12.96132755279541
+ ],
+ [
+ "▁smash",
+ -12.961359024047852
+ ],
+ [
+ "Euro",
+ -12.961360931396484
+ ],
+ [
+ "▁Nouvelle",
+ -12.961370468139648
+ ],
+ [
+ "▁Raspberry",
+ -12.961370468139648
+ ],
+ [
+ "▁präsentieren",
+ -12.961380004882812
+ ],
+ [
+ "▁electrician",
+ -12.961404800415039
+ ],
+ [
+ "▁cheerful",
+ -12.961472511291504
+ ],
+ [
+ "▁chargé",
+ -12.961508750915527
+ ],
+ [
+ "▁Diskussion",
+ -12.961511611938477
+ ],
+ [
+ "▁surpass",
+ -12.961604118347168
+ ],
+ [
+ "▁Acces",
+ -12.961701393127441
+ ],
+ [
+ "tausend",
+ -12.961771011352539
+ ],
+ [
+ "▁vigorous",
+ -12.961808204650879
+ ],
+ [
+ "▁tava",
+ -12.961810111999512
+ ],
+ [
+ "CHO",
+ -12.96193790435791
+ ],
+ [
+ "▁1951",
+ -12.961941719055176
+ ],
+ [
+ "▁Umsatz",
+ -12.962019920349121
+ ],
+ [
+ "▁slavery",
+ -12.962055206298828
+ ],
+ [
+ "travel",
+ -12.962294578552246
+ ],
+ [
+ "▁correspondent",
+ -12.962297439575195
+ ],
+ [
+ "▁$150",
+ -12.962307929992676
+ ],
+ [
+ "▁stärker",
+ -12.962594985961914
+ ],
+ [
+ "Alb",
+ -12.96264362335205
+ ],
+ [
+ "▁Lopez",
+ -12.962682723999023
+ ],
+ [
+ "▁longueur",
+ -12.962767601013184
+ ],
+ [
+ "▁successive",
+ -12.962772369384766
+ ],
+ [
+ "▁(2015)",
+ -12.96278190612793
+ ],
+ [
+ "teig",
+ -12.962790489196777
+ ],
+ [
+ "custom",
+ -12.962944984436035
+ ],
+ [
+ "TIM",
+ -12.963099479675293
+ ],
+ [
+ "▁Escape",
+ -12.963174819946289
+ ],
+ [
+ "▁Sekunden",
+ -12.963349342346191
+ ],
+ [
+ "tiré",
+ -12.963444709777832
+ ],
+ [
+ "▁chantier",
+ -12.963489532470703
+ ],
+ [
+ "▁saturated",
+ -12.963555335998535
+ ],
+ [
+ "▁confrontation",
+ -12.963804244995117
+ ],
+ [
+ "▁biography",
+ -12.963805198669434
+ ],
+ [
+ "zuerst",
+ -12.9639892578125
+ ],
+ [
+ "▁rencontré",
+ -12.963991165161133
+ ],
+ [
+ "▁harmless",
+ -12.96412181854248
+ ],
+ [
+ "Branche",
+ -12.964139938354492
+ ],
+ [
+ "▁QR",
+ -12.964380264282227
+ ],
+ [
+ "▁Ereignis",
+ -12.964430809020996
+ ],
+ [
+ "▁verkaufen",
+ -12.96444320678711
+ ],
+ [
+ "0:00",
+ -12.96451187133789
+ ],
+ [
+ "Association",
+ -12.96469783782959
+ ],
+ [
+ "▁Santiago",
+ -12.964865684509277
+ ],
+ [
+ "Control",
+ -12.964993476867676
+ ],
+ [
+ "▁Angriff",
+ -12.9650297164917
+ ],
+ [
+ "lase",
+ -12.96505069732666
+ ],
+ [
+ "▁sfaturi",
+ -12.965224266052246
+ ],
+ [
+ "▁Comprehensive",
+ -12.965304374694824
+ ],
+ [
+ "▁Shepherd",
+ -12.965304374694824
+ ],
+ [
+ "▁exponential",
+ -12.965304374694824
+ ],
+ [
+ "▁penetration",
+ -12.965304374694824
+ ],
+ [
+ "▁comble",
+ -12.965394973754883
+ ],
+ [
+ "ionar",
+ -12.965557098388672
+ ],
+ [
+ "slept",
+ -12.965563774108887
+ ],
+ [
+ "▁Spice",
+ -12.965633392333984
+ ],
+ [
+ "mAh",
+ -12.965688705444336
+ ],
+ [
+ "▁Vertreter",
+ -12.965747833251953
+ ],
+ [
+ "fehler",
+ -12.965752601623535
+ ],
+ [
+ "▁Scroll",
+ -12.96599292755127
+ ],
+ [
+ "▁WARRANT",
+ -12.966179847717285
+ ],
+ [
+ "▁minimise",
+ -12.966326713562012
+ ],
+ [
+ "▁Dept",
+ -12.966474533081055
+ ],
+ [
+ "▁urinar",
+ -12.96661376953125
+ ],
+ [
+ "établir",
+ -12.966619491577148
+ ],
+ [
+ "verhältnis",
+ -12.966713905334473
+ ],
+ [
+ "▁glowing",
+ -12.966979026794434
+ ],
+ [
+ "kulturelle",
+ -12.966984748840332
+ ],
+ [
+ "▁Pediatric",
+ -12.967057228088379
+ ],
+ [
+ "▁inconvenience",
+ -12.967057228088379
+ ],
+ [
+ "Antoine",
+ -12.967121124267578
+ ],
+ [
+ "▁Heck",
+ -12.967164993286133
+ ],
+ [
+ "▁couches",
+ -12.967265129089355
+ ],
+ [
+ "▁1938",
+ -12.967331886291504
+ ],
+ [
+ "maybe",
+ -12.967333793640137
+ ],
+ [
+ "ETA",
+ -12.9673433303833
+ ],
+ [
+ "▁solaire",
+ -12.96748161315918
+ ],
+ [
+ "▁Zürich",
+ -12.967495918273926
+ ],
+ [
+ "computer",
+ -12.967545509338379
+ ],
+ [
+ "milk",
+ -12.96756362915039
+ ],
+ [
+ "он",
+ -12.967585563659668
+ ],
+ [
+ "modalitate",
+ -12.967608451843262
+ ],
+ [
+ "spanning",
+ -12.967655181884766
+ ],
+ [
+ "▁Crypto",
+ -12.96774959564209
+ ],
+ [
+ "▁Spotify",
+ -12.967935562133789
+ ],
+ [
+ "mycin",
+ -12.967944145202637
+ ],
+ [
+ "▁similarities",
+ -12.96811294555664
+ ],
+ [
+ "▁eclipse",
+ -12.968377113342285
+ ],
+ [
+ "Map",
+ -12.968610763549805
+ ],
+ [
+ "double",
+ -12.96861743927002
+ ],
+ [
+ "corporate",
+ -12.968734741210938
+ ],
+ [
+ "▁Hindi",
+ -12.968853950500488
+ ],
+ [
+ "battling",
+ -12.968866348266602
+ ],
+ [
+ "▁habituel",
+ -12.969098091125488
+ ],
+ [
+ "▁Transition",
+ -12.969196319580078
+ ],
+ [
+ "▁luptă",
+ -12.96920394897461
+ ],
+ [
+ "▁trainee",
+ -12.969219207763672
+ ],
+ [
+ "LIS",
+ -12.96922492980957
+ ],
+ [
+ "▁Vatican",
+ -12.969254493713379
+ ],
+ [
+ "Archived",
+ -12.9692964553833
+ ],
+ [
+ "Connect",
+ -12.969305038452148
+ ],
+ [
+ "▁prealabil",
+ -12.969307899475098
+ ],
+ [
+ "▁Chambre",
+ -12.969327926635742
+ ],
+ [
+ "stuhl",
+ -12.969440460205078
+ ],
+ [
+ "▁arrivé",
+ -12.969557762145996
+ ],
+ [
+ "▁Urteil",
+ -12.969575881958008
+ ],
+ [
+ "▁scrutiny",
+ -12.969818115234375
+ ],
+ [
+ "▁memoir",
+ -12.969854354858398
+ ],
+ [
+ "▁innovant",
+ -12.9699068069458
+ ],
+ [
+ "▁sublime",
+ -12.969943046569824
+ ],
+ [
+ "children",
+ -12.970004081726074
+ ],
+ [
+ "▁Handwerk",
+ -12.970056533813477
+ ],
+ [
+ "▁campuses",
+ -12.970268249511719
+ ],
+ [
+ "▁durabil",
+ -12.970502853393555
+ ],
+ [
+ "▁immersive",
+ -12.970632553100586
+ ],
+ [
+ "▁Magnet",
+ -12.970732688903809
+ ],
+ [
+ "läufe",
+ -12.970808029174805
+ ],
+ [
+ "▁Techno",
+ -12.970837593078613
+ ],
+ [
+ "MAP",
+ -12.9710693359375
+ ],
+ [
+ "7.2",
+ -12.971145629882812
+ ],
+ [
+ "▁Schwimm",
+ -12.971181869506836
+ ],
+ [
+ "BOOK",
+ -12.971186637878418
+ ],
+ [
+ "188",
+ -12.971441268920898
+ ],
+ [
+ "▁Supervisor",
+ -12.971498489379883
+ ],
+ [
+ "prévue",
+ -12.971691131591797
+ ],
+ [
+ "needed",
+ -12.971813201904297
+ ],
+ [
+ "▁creditors",
+ -12.971822738647461
+ ],
+ [
+ "▁brin",
+ -12.971837043762207
+ ],
+ [
+ "▁Neck",
+ -12.971900939941406
+ ],
+ [
+ "▁Salut",
+ -12.971988677978516
+ ],
+ [
+ "▁despair",
+ -12.972105979919434
+ ],
+ [
+ "▁Sauce",
+ -12.972261428833008
+ ],
+ [
+ "▁Westminster",
+ -12.972335815429688
+ ],
+ [
+ "▁langfristig",
+ -12.972335815429688
+ ],
+ [
+ "▁northeast",
+ -12.972365379333496
+ ],
+ [
+ "▁încercat",
+ -12.972399711608887
+ ],
+ [
+ "▁nausea",
+ -12.972408294677734
+ ],
+ [
+ "▁Paypal",
+ -12.972440719604492
+ ],
+ [
+ "▁Arrow",
+ -12.972469329833984
+ ],
+ [
+ "▁Travis",
+ -12.972633361816406
+ ],
+ [
+ "(2009)",
+ -12.972713470458984
+ ],
+ [
+ "▁Rising",
+ -12.972719192504883
+ ],
+ [
+ "termes",
+ -12.973097801208496
+ ],
+ [
+ "Australie",
+ -12.973154067993164
+ ],
+ [
+ "▁scarf",
+ -12.973187446594238
+ ],
+ [
+ "klassischen",
+ -12.97337818145752
+ ],
+ [
+ "▁boug",
+ -12.973466873168945
+ ],
+ [
+ "DOT",
+ -12.97360610961914
+ ],
+ [
+ "▁Trink",
+ -12.97361946105957
+ ],
+ [
+ "▁bestätigt",
+ -12.97365951538086
+ ],
+ [
+ "▁officiel",
+ -12.97370433807373
+ ],
+ [
+ "Produkt",
+ -12.973873138427734
+ ],
+ [
+ "DNA",
+ -12.974140167236328
+ ],
+ [
+ "▁*******",
+ -12.97426700592041
+ ],
+ [
+ "GAR",
+ -12.974271774291992
+ ],
+ [
+ "therapeut",
+ -12.974377632141113
+ ],
+ [
+ "187",
+ -12.974420547485352
+ ],
+ [
+ "▁Louisville",
+ -12.974493026733398
+ ],
+ [
+ "▁geöffnet",
+ -12.97462272644043
+ ],
+ [
+ "Watch",
+ -12.974640846252441
+ ],
+ [
+ "85%",
+ -12.974678993225098
+ ],
+ [
+ "▁Candida",
+ -12.974698066711426
+ ],
+ [
+ "▁Kathy",
+ -12.974703788757324
+ ],
+ [
+ "▁Animation",
+ -12.974711418151855
+ ],
+ [
+ "planung",
+ -12.974715232849121
+ ],
+ [
+ "woche",
+ -12.974730491638184
+ ],
+ [
+ "Video",
+ -12.974966049194336
+ ],
+ [
+ "▁Automation",
+ -12.97507095336914
+ ],
+ [
+ "▁foliage",
+ -12.97507381439209
+ ],
+ [
+ "▁evenimentului",
+ -12.975175857543945
+ ],
+ [
+ "SEN",
+ -12.975362777709961
+ ],
+ [
+ "▁Dialog",
+ -12.975372314453125
+ ],
+ [
+ "▁ZIP",
+ -12.975372314453125
+ ],
+ [
+ "▁vieții",
+ -12.97537612915039
+ ],
+ [
+ "▁passionné",
+ -12.975425720214844
+ ],
+ [
+ "▁WOW",
+ -12.97544002532959
+ ],
+ [
+ "ectiv",
+ -12.975464820861816
+ ],
+ [
+ "▁vorbesc",
+ -12.975482940673828
+ ],
+ [
+ "▁computational",
+ -12.975533485412598
+ ],
+ [
+ "▁idiot",
+ -12.97557258605957
+ ],
+ [
+ "▁stigma",
+ -12.97567081451416
+ ],
+ [
+ "▁multumesc",
+ -12.975870132446289
+ ],
+ [
+ "▁sărbători",
+ -12.975870132446289
+ ],
+ [
+ "▁Advantage",
+ -12.975906372070312
+ ],
+ [
+ "▁alegeri",
+ -12.976024627685547
+ ],
+ [
+ "▁philosopher",
+ -12.976031303405762
+ ],
+ [
+ "RIE",
+ -12.976117134094238
+ ],
+ [
+ "refundable",
+ -12.976221084594727
+ ],
+ [
+ "▁Sofia",
+ -12.97623348236084
+ ],
+ [
+ "▁încheiat",
+ -12.976313591003418
+ ],
+ [
+ "meilleures",
+ -12.976473808288574
+ ],
+ [
+ "critical",
+ -12.976744651794434
+ ],
+ [
+ "▁cavity",
+ -12.976766586303711
+ ],
+ [
+ "▁ressort",
+ -12.976792335510254
+ ],
+ [
+ "strong",
+ -12.976798057556152
+ ],
+ [
+ "▁Backup",
+ -12.976948738098145
+ ],
+ [
+ "▁Zeitraum",
+ -12.977023124694824
+ ],
+ [
+ "▁Szene",
+ -12.977027893066406
+ ],
+ [
+ "▁Candle",
+ -12.977173805236816
+ ],
+ [
+ "▁ciocolat",
+ -12.977198600769043
+ ],
+ [
+ "etched",
+ -12.977227210998535
+ ],
+ [
+ "ан",
+ -12.977302551269531
+ ],
+ [
+ "▁Anchor",
+ -12.977365493774414
+ ],
+ [
+ "equate",
+ -12.977470397949219
+ ],
+ [
+ "▁bulg",
+ -12.977476119995117
+ ],
+ [
+ "▁motorist",
+ -12.977524757385254
+ ],
+ [
+ "träglich",
+ -12.977736473083496
+ ],
+ [
+ "please",
+ -12.977936744689941
+ ],
+ [
+ "different",
+ -12.978011131286621
+ ],
+ [
+ "▁Accel",
+ -12.97813606262207
+ ],
+ [
+ "Proiectul",
+ -12.97829818725586
+ ],
+ [
+ "▁cabbage",
+ -12.97852897644043
+ ],
+ [
+ "▁télécharger",
+ -12.97852897644043
+ ],
+ [
+ "▁Presentation",
+ -12.97856330871582
+ ],
+ [
+ "▁Struktur",
+ -12.978621482849121
+ ],
+ [
+ "bücher",
+ -12.978650093078613
+ ],
+ [
+ "▁flatter",
+ -12.978672981262207
+ ],
+ [
+ "emprunt",
+ -12.979074478149414
+ ],
+ [
+ "▁oriental",
+ -12.979111671447754
+ ],
+ [
+ "▁Turnier",
+ -12.979166984558105
+ ],
+ [
+ "brücke",
+ -12.97917366027832
+ ],
+ [
+ "▁légumes",
+ -12.979416847229004
+ ],
+ [
+ "gerechnet",
+ -12.979595184326172
+ ],
+ [
+ "flooded",
+ -12.979621887207031
+ ],
+ [
+ "LER",
+ -12.979679107666016
+ ],
+ [
+ "üben",
+ -12.97973918914795
+ ],
+ [
+ "internaute",
+ -12.979888916015625
+ ],
+ [
+ "▁Austausch",
+ -12.979935646057129
+ ],
+ [
+ "gefordert",
+ -12.980034828186035
+ ],
+ [
+ "▁adoptat",
+ -12.980277061462402
+ ],
+ [
+ "▁erinnern",
+ -12.980305671691895
+ ],
+ [
+ "▁dolphin",
+ -12.980307579040527
+ ],
+ [
+ "▁Parkinson",
+ -12.980308532714844
+ ],
+ [
+ "büro",
+ -12.980310440063477
+ ],
+ [
+ "▁Crest",
+ -12.980368614196777
+ ],
+ [
+ "▁Ikea",
+ -12.980437278747559
+ ],
+ [
+ "▁ecologic",
+ -12.980470657348633
+ ],
+ [
+ "mplă",
+ -12.98065185546875
+ ],
+ [
+ "▁șef",
+ -12.980655670166016
+ ],
+ [
+ "coop",
+ -12.980868339538574
+ ],
+ [
+ "▁Carson",
+ -12.980900764465332
+ ],
+ [
+ "▁uşor",
+ -12.981054306030273
+ ],
+ [
+ "▁exert",
+ -12.981070518493652
+ ],
+ [
+ "▁countertop",
+ -12.981114387512207
+ ],
+ [
+ "ntended",
+ -12.981136322021484
+ ],
+ [
+ "▁Civic",
+ -12.981313705444336
+ ],
+ [
+ "▁attentes",
+ -12.98133373260498
+ ],
+ [
+ "gesetzlichen",
+ -12.981356620788574
+ ],
+ [
+ "frischen",
+ -12.981475830078125
+ ],
+ [
+ "▁Bottle",
+ -12.981636047363281
+ ],
+ [
+ "▁cautare",
+ -12.982080459594727
+ ],
+ [
+ "▁waterfront",
+ -12.982226371765137
+ ],
+ [
+ "▁centerpiece",
+ -12.982312202453613
+ ],
+ [
+ "▁Castel",
+ -12.982441902160645
+ ],
+ [
+ "510",
+ -12.98270034790039
+ ],
+ [
+ "capped",
+ -12.982709884643555
+ ],
+ [
+ "▁mattresses",
+ -12.982850074768066
+ ],
+ [
+ "▁readiness",
+ -12.982865333557129
+ ],
+ [
+ "diag",
+ -12.982970237731934
+ ],
+ [
+ "▁geändert",
+ -12.982980728149414
+ ],
+ [
+ "▁complained",
+ -12.983051300048828
+ ],
+ [
+ "▁diary",
+ -12.983073234558105
+ ],
+ [
+ "▁ceremonies",
+ -12.983144760131836
+ ],
+ [
+ "▁următor",
+ -12.983181953430176
+ ],
+ [
+ "▁Engel",
+ -12.983270645141602
+ ],
+ [
+ "▁disconnect",
+ -12.9832763671875
+ ],
+ [
+ "▁Silvi",
+ -12.983282089233398
+ ],
+ [
+ "▁eingerichtet",
+ -12.9834566116333
+ ],
+ [
+ "medizin",
+ -12.983512878417969
+ ],
+ [
+ "▁majestic",
+ -12.983869552612305
+ ],
+ [
+ "▁Random",
+ -12.983943939208984
+ ],
+ [
+ "▁Equity",
+ -12.984046936035156
+ ],
+ [
+ "▁Echipa",
+ -12.984111785888672
+ ],
+ [
+ "са",
+ -12.984163284301758
+ ],
+ [
+ "316",
+ -12.984179496765137
+ ],
+ [
+ "▁Formation",
+ -12.984183311462402
+ ],
+ [
+ "inland",
+ -12.98421859741211
+ ],
+ [
+ "appuy",
+ -12.984301567077637
+ ],
+ [
+ "TAN",
+ -12.984481811523438
+ ],
+ [
+ "slipped",
+ -12.984918594360352
+ ],
+ [
+ "Certains",
+ -12.985247611999512
+ ],
+ [
+ "▁Silber",
+ -12.98525333404541
+ ],
+ [
+ "▁reçoi",
+ -12.985257148742676
+ ],
+ [
+ "▁Monthly",
+ -12.985323905944824
+ ],
+ [
+ "calculating",
+ -12.985494613647461
+ ],
+ [
+ "▁scratches",
+ -12.98554515838623
+ ],
+ [
+ "▁concurrence",
+ -12.985654830932617
+ ],
+ [
+ "▁Stärke",
+ -12.985662460327148
+ ],
+ [
+ "▁intermediar",
+ -12.985751152038574
+ ],
+ [
+ "▁erlebt",
+ -12.98579216003418
+ ],
+ [
+ "gesellschaftlich",
+ -12.986037254333496
+ ],
+ [
+ "▁Volk",
+ -12.986041069030762
+ ],
+ [
+ "▁Ansprüche",
+ -12.986101150512695
+ ],
+ [
+ "▁cumulative",
+ -12.986103057861328
+ ],
+ [
+ "▁Randy",
+ -12.986183166503906
+ ],
+ [
+ "▁instituții",
+ -12.98622989654541
+ ],
+ [
+ "together",
+ -12.986489295959473
+ ],
+ [
+ "▁Sap",
+ -12.986539840698242
+ ],
+ [
+ "▁modificari",
+ -12.986551284790039
+ ],
+ [
+ "▁erosion",
+ -12.986572265625
+ ],
+ [
+ "▁wicked",
+ -12.986577033996582
+ ],
+ [
+ "soaked",
+ -12.986613273620605
+ ],
+ [
+ "▁cellar",
+ -12.9866361618042
+ ],
+ [
+ "ignoring",
+ -12.986726760864258
+ ],
+ [
+ "▁scarce",
+ -12.986815452575684
+ ],
+ [
+ "ueuse",
+ -12.98697280883789
+ ],
+ [
+ "▁bibliothèque",
+ -12.986995697021484
+ ],
+ [
+ "critères",
+ -12.987017631530762
+ ],
+ [
+ "▁overlay",
+ -12.987166404724121
+ ],
+ [
+ "IPA",
+ -12.98737907409668
+ ],
+ [
+ "director",
+ -12.987393379211426
+ ],
+ [
+ "▁Krishna",
+ -12.987444877624512
+ ],
+ [
+ "▁methodologies",
+ -12.987451553344727
+ ],
+ [
+ "iocese",
+ -12.987513542175293
+ ],
+ [
+ "▁saucepan",
+ -12.987713813781738
+ ],
+ [
+ "184",
+ -12.987948417663574
+ ],
+ [
+ "275",
+ -12.987981796264648
+ ],
+ [
+ "▁précieu",
+ -12.988165855407715
+ ],
+ [
+ "▁academy",
+ -12.9883394241333
+ ],
+ [
+ "460",
+ -12.988438606262207
+ ],
+ [
+ "ERN",
+ -12.988679885864258
+ ],
+ [
+ "▁emoti",
+ -12.988725662231445
+ ],
+ [
+ "▁télévision",
+ -12.988823890686035
+ ],
+ [
+ "EDIT",
+ -12.988901138305664
+ ],
+ [
+ "▁Valeri",
+ -12.989045143127441
+ ],
+ [
+ "▁Charity",
+ -12.98911190032959
+ ],
+ [
+ "Voilà",
+ -12.989297866821289
+ ],
+ [
+ "▁lipsit",
+ -12.989356994628906
+ ],
+ [
+ "▁unleash",
+ -12.989373207092285
+ ],
+ [
+ "▁suferit",
+ -12.989506721496582
+ ],
+ [
+ "▁Lifestyle",
+ -12.98953914642334
+ ],
+ [
+ "▁Edel",
+ -12.989603996276855
+ ],
+ [
+ "▁Derek",
+ -12.989643096923828
+ ],
+ [
+ "▁Manga",
+ -12.989801406860352
+ ],
+ [
+ "▁increment",
+ -12.989990234375
+ ],
+ [
+ "▁plötzlich",
+ -12.990133285522461
+ ],
+ [
+ "▁5:30",
+ -12.990208625793457
+ ],
+ [
+ "▁Republicii",
+ -12.990246772766113
+ ],
+ [
+ "▁capitalism",
+ -12.990293502807617
+ ],
+ [
+ "ROW",
+ -12.990510940551758
+ ],
+ [
+ "▁Paar",
+ -12.990523338317871
+ ],
+ [
+ "allée",
+ -12.99057674407959
+ ],
+ [
+ "▁motto",
+ -12.990610122680664
+ ],
+ [
+ "Schäden",
+ -12.990630149841309
+ ],
+ [
+ "▁£10",
+ -12.99063491821289
+ ],
+ [
+ "RIP",
+ -12.990728378295898
+ ],
+ [
+ "courir",
+ -12.990761756896973
+ ],
+ [
+ "rocky",
+ -12.990944862365723
+ ],
+ [
+ "▁Sunshine",
+ -12.991031646728516
+ ],
+ [
+ "▁chimney",
+ -12.991044998168945
+ ],
+ [
+ "▁préfér",
+ -12.991153717041016
+ ],
+ [
+ "▁relaxare",
+ -12.991189956665039
+ ],
+ [
+ "▁colabora",
+ -12.99134349822998
+ ],
+ [
+ "liefer",
+ -12.99142837524414
+ ],
+ [
+ "▁ordentlich",
+ -12.991486549377441
+ ],
+ [
+ "▁dauerhaft",
+ -12.991535186767578
+ ],
+ [
+ "kammer",
+ -12.991572380065918
+ ],
+ [
+ "▁Basket",
+ -12.991579055786133
+ ],
+ [
+ "Site",
+ -12.991657257080078
+ ],
+ [
+ "▁Regina",
+ -12.991716384887695
+ ],
+ [
+ "▁simulate",
+ -12.991868019104004
+ ],
+ [
+ "▁wrestle",
+ -12.991939544677734
+ ],
+ [
+ "wertig",
+ -12.991986274719238
+ ],
+ [
+ "▁Christie",
+ -12.992018699645996
+ ],
+ [
+ "download",
+ -12.992033004760742
+ ],
+ [
+ "▁torch",
+ -12.992213249206543
+ ],
+ [
+ "riya",
+ -12.992216110229492
+ ],
+ [
+ "▁Grie",
+ -12.992247581481934
+ ],
+ [
+ "bitten",
+ -12.992356300354004
+ ],
+ [
+ "▁spezialisiert",
+ -12.99238109588623
+ ],
+ [
+ "▁Parade",
+ -12.992408752441406
+ ],
+ [
+ "▁migraine",
+ -12.992830276489258
+ ],
+ [
+ "▁Armstrong",
+ -12.992846488952637
+ ],
+ [
+ "▁cutie",
+ -12.9928560256958
+ ],
+ [
+ "▁bullying",
+ -12.992889404296875
+ ],
+ [
+ "▁Estonia",
+ -12.99293041229248
+ ],
+ [
+ "▁harvested",
+ -12.992948532104492
+ ],
+ [
+ "▁Hunger",
+ -12.992971420288086
+ ],
+ [
+ "▁frapp",
+ -12.992999076843262
+ ],
+ [
+ "REM",
+ -12.993117332458496
+ ],
+ [
+ "sensor",
+ -12.993189811706543
+ ],
+ [
+ "▁GREAT",
+ -12.993293762207031
+ ],
+ [
+ "▁thyroid",
+ -12.993302345275879
+ ],
+ [
+ "▁mărturi",
+ -12.993335723876953
+ ],
+ [
+ "ocupă",
+ -12.993809700012207
+ ],
+ [
+ "▁Wealth",
+ -12.993812561035156
+ ],
+ [
+ "▁convins",
+ -12.993841171264648
+ ],
+ [
+ "141",
+ -12.993876457214355
+ ],
+ [
+ "▁vingt",
+ -12.993901252746582
+ ],
+ [
+ "▁revel",
+ -12.994054794311523
+ ],
+ [
+ "▁Adri",
+ -12.994083404541016
+ ],
+ [
+ "▁remix",
+ -12.994207382202148
+ ],
+ [
+ "▁fermentation",
+ -12.99425220489502
+ ],
+ [
+ "▁achiziti",
+ -12.994352340698242
+ ],
+ [
+ "dream",
+ -12.994426727294922
+ ],
+ [
+ "▁contemporan",
+ -12.994632720947266
+ ],
+ [
+ "▁youngsters",
+ -12.994685173034668
+ ],
+ [
+ "▁Hartford",
+ -12.994745254516602
+ ],
+ [
+ "▁Wagen",
+ -12.994988441467285
+ ],
+ [
+ "▁Celebr",
+ -12.995214462280273
+ ],
+ [
+ "leveraging",
+ -12.99527645111084
+ ],
+ [
+ "▁Iasi",
+ -12.99549674987793
+ ],
+ [
+ "tackling",
+ -12.9955415725708
+ ],
+ [
+ "▁intrinsic",
+ -12.995553970336914
+ ],
+ [
+ "▁Macedon",
+ -12.995603561401367
+ ],
+ [
+ "NIA",
+ -12.995784759521484
+ ],
+ [
+ "▁bliss",
+ -12.995905876159668
+ ],
+ [
+ "▁gradual",
+ -12.995908737182617
+ ],
+ [
+ "▁inregistrat",
+ -12.995981216430664
+ ],
+ [
+ "▁volleyball",
+ -12.995986938476562
+ ],
+ [
+ "▁offiziell",
+ -12.996054649353027
+ ],
+ [
+ "▁carré",
+ -12.99611759185791
+ ],
+ [
+ "Mostly",
+ -12.996174812316895
+ ],
+ [
+ "▁Harley",
+ -12.996193885803223
+ ],
+ [
+ "▁locati",
+ -12.996216773986816
+ ],
+ [
+ "▁Klo",
+ -12.996223449707031
+ ],
+ [
+ "▁Equal",
+ -12.996238708496094
+ ],
+ [
+ "▁citat",
+ -12.996369361877441
+ ],
+ [
+ "▁argint",
+ -12.996478080749512
+ ],
+ [
+ "prüft",
+ -12.996528625488281
+ ],
+ [
+ "▁Fence",
+ -12.996600151062012
+ ],
+ [
+ "positive",
+ -12.996988296508789
+ ],
+ [
+ "▁Kaz",
+ -12.997245788574219
+ ],
+ [
+ "▁distortion",
+ -12.997342109680176
+ ],
+ [
+ "▁sâmbătă",
+ -12.997342109680176
+ ],
+ [
+ "▁frontière",
+ -12.997346878051758
+ ],
+ [
+ "▁revanch",
+ -12.997394561767578
+ ],
+ [
+ "▁Held",
+ -12.997465133666992
+ ],
+ [
+ "▁Hobb",
+ -12.99776554107666
+ ],
+ [
+ "▁reuşit",
+ -12.997796058654785
+ ],
+ [
+ "deem",
+ -12.997880935668945
+ ],
+ [
+ "▁dorint",
+ -12.997902870178223
+ ],
+ [
+ "▁Anlagen",
+ -12.997908592224121
+ ],
+ [
+ "▁cheval",
+ -12.997973442077637
+ ],
+ [
+ "630",
+ -12.99806022644043
+ ],
+ [
+ "▁implementare",
+ -12.99808406829834
+ ],
+ [
+ "▁curator",
+ -12.99821662902832
+ ],
+ [
+ "▁legislator",
+ -12.998247146606445
+ ],
+ [
+ "▁potassium",
+ -12.998247146606445
+ ],
+ [
+ "▁veterinarian",
+ -12.998247146606445
+ ],
+ [
+ "▁domenii",
+ -12.998273849487305
+ ],
+ [
+ "▁revue",
+ -12.998310089111328
+ ],
+ [
+ "Vielen",
+ -12.998333930969238
+ ],
+ [
+ "africain",
+ -12.998570442199707
+ ],
+ [
+ "before",
+ -12.998680114746094
+ ],
+ [
+ "▁Bestandteil",
+ -12.998702049255371
+ ],
+ [
+ "▁(2010)",
+ -12.998767852783203
+ ],
+ [
+ "▁Arlington",
+ -12.999153137207031
+ ],
+ [
+ "▁Gründung",
+ -12.999153137207031
+ ],
+ [
+ "▁Sprinkle",
+ -12.999153137207031
+ ],
+ [
+ "▁Princeton",
+ -12.999186515808105
+ ],
+ [
+ "chirurg",
+ -12.999228477478027
+ ],
+ [
+ "▁laissé",
+ -12.999357223510742
+ ],
+ [
+ "whoever",
+ -12.999384880065918
+ ],
+ [
+ "▁pasture",
+ -12.999431610107422
+ ],
+ [
+ "ajute",
+ -12.999436378479004
+ ],
+ [
+ "▁joyful",
+ -12.999494552612305
+ ],
+ [
+ "etapa",
+ -12.999905586242676
+ ],
+ [
+ "ESP",
+ -13.000017166137695
+ ],
+ [
+ "▁Iohannis",
+ -13.000059127807617
+ ],
+ [
+ "▁10:30",
+ -13.000127792358398
+ ],
+ [
+ "▁Kingston",
+ -13.000140190124512
+ ],
+ [
+ "▁contender",
+ -13.000164031982422
+ ],
+ [
+ "▁Damage",
+ -13.000177383422852
+ ],
+ [
+ "▁schreibt",
+ -13.000482559204102
+ ],
+ [
+ "sstisch",
+ -13.000631332397461
+ ],
+ [
+ "Associated",
+ -13.00072956085205
+ ],
+ [
+ "▁disposable",
+ -13.000782012939453
+ ],
+ [
+ "veranstaltung",
+ -13.00096607208252
+ ],
+ [
+ "▁puppet",
+ -13.00100040435791
+ ],
+ [
+ "pong",
+ -13.001093864440918
+ ],
+ [
+ "▁Chronicle",
+ -13.001176834106445
+ ],
+ [
+ "222",
+ -13.001286506652832
+ ],
+ [
+ "intuit",
+ -13.001396179199219
+ ],
+ [
+ "inscrire",
+ -13.001429557800293
+ ],
+ [
+ "▁speeches",
+ -13.001431465148926
+ ],
+ [
+ "▁Eingang",
+ -13.001775741577148
+ ],
+ [
+ "▁Adidas",
+ -13.001875877380371
+ ],
+ [
+ "▁cemetery",
+ -13.001877784729004
+ ],
+ [
+ "▁juicy",
+ -13.001885414123535
+ ],
+ [
+ "▁wertvolle",
+ -13.0018892288208
+ ],
+ [
+ "▁militari",
+ -13.001917839050293
+ ],
+ [
+ "China",
+ -13.00196361541748
+ ],
+ [
+ "ecția",
+ -13.002041816711426
+ ],
+ [
+ "luster",
+ -13.002063751220703
+ ],
+ [
+ "auftrag",
+ -13.00234317779541
+ ],
+ [
+ "▁Marius",
+ -13.002523422241211
+ ],
+ [
+ "▁crossover",
+ -13.002555847167969
+ ],
+ [
+ "▁enthusiast",
+ -13.002555847167969
+ ],
+ [
+ "▁cantitate",
+ -13.002630233764648
+ ],
+ [
+ "▁animat",
+ -13.002634048461914
+ ],
+ [
+ "Park",
+ -13.002793312072754
+ ],
+ [
+ "▁unchanged",
+ -13.00279426574707
+ ],
+ [
+ "russia",
+ -13.00281810760498
+ ],
+ [
+ "instant",
+ -13.002833366394043
+ ],
+ [
+ "ţiunea",
+ -13.002835273742676
+ ],
+ [
+ "▁franchi",
+ -13.002920150756836
+ ],
+ [
+ "▁mobiliz",
+ -13.002963066101074
+ ],
+ [
+ "athlet",
+ -13.003013610839844
+ ],
+ [
+ "▁Cardio",
+ -13.0031099319458
+ ],
+ [
+ "▁supus",
+ -13.003119468688965
+ ],
+ [
+ "▁Griff",
+ -13.003137588500977
+ ],
+ [
+ "flakes",
+ -13.003217697143555
+ ],
+ [
+ "soluble",
+ -13.003250122070312
+ ],
+ [
+ "Known",
+ -13.003693580627441
+ ],
+ [
+ "leaking",
+ -13.003741264343262
+ ],
+ [
+ "▁Holocaust",
+ -13.004148483276367
+ ],
+ [
+ "gift",
+ -13.004197120666504
+ ],
+ [
+ "▁tradiţi",
+ -13.004359245300293
+ ],
+ [
+ "▁southeast",
+ -13.004498481750488
+ ],
+ [
+ "▁correspondant",
+ -13.00460147857666
+ ],
+ [
+ "Isaiah",
+ -13.004603385925293
+ ],
+ [
+ "▁diagonal",
+ -13.004606246948242
+ ],
+ [
+ "▁Probabil",
+ -13.004680633544922
+ ],
+ [
+ "▁dégust",
+ -13.004791259765625
+ ],
+ [
+ "▁Naval",
+ -13.004802703857422
+ ],
+ [
+ "▁cultivation",
+ -13.004839897155762
+ ],
+ [
+ "▁Vertrieb",
+ -13.004849433898926
+ ],
+ [
+ "▁pony",
+ -13.004854202270508
+ ],
+ [
+ "▁Throw",
+ -13.0050048828125
+ ],
+ [
+ "little",
+ -13.005010604858398
+ ],
+ [
+ "▁remarque",
+ -13.005074501037598
+ ],
+ [
+ "▁parcare",
+ -13.005085945129395
+ ],
+ [
+ "3.8",
+ -13.00518798828125
+ ],
+ [
+ "▁renunt",
+ -13.005330085754395
+ ],
+ [
+ "▁Rewards",
+ -13.005487442016602
+ ],
+ [
+ "▁Thur",
+ -13.005496978759766
+ ],
+ [
+ "▁underestimate",
+ -13.005515098571777
+ ],
+ [
+ "▁frankly",
+ -13.005516052246094
+ ],
+ [
+ "Bretagne",
+ -13.005517959594727
+ ],
+ [
+ "axial",
+ -13.005537986755371
+ ],
+ [
+ "▁identities",
+ -13.0055570602417
+ ],
+ [
+ "▁Harvest",
+ -13.00561237335205
+ ],
+ [
+ "▁skippe",
+ -13.00561237335205
+ ],
+ [
+ "▁Boutique",
+ -13.005670547485352
+ ],
+ [
+ "▁intuition",
+ -13.005746841430664
+ ],
+ [
+ "▁Rotary",
+ -13.00581169128418
+ ],
+ [
+ "▁SERVICE",
+ -13.005875587463379
+ ],
+ [
+ "▁refill",
+ -13.005915641784668
+ ],
+ [
+ "▁arcade",
+ -13.006060600280762
+ ],
+ [
+ "▁komme",
+ -13.006386756896973
+ ],
+ [
+ "▁irrelevant",
+ -13.006427764892578
+ ],
+ [
+ "▁Sortiment",
+ -13.006429672241211
+ ],
+ [
+ "▁scriitor",
+ -13.006488800048828
+ ],
+ [
+ "▁clicked",
+ -13.006516456604004
+ ],
+ [
+ "▁ciel",
+ -13.006610870361328
+ ],
+ [
+ "▁Caesar",
+ -13.00680160522461
+ ],
+ [
+ "hound",
+ -13.006803512573242
+ ],
+ [
+ "whipped",
+ -13.006843566894531
+ ],
+ [
+ "licate",
+ -13.006867408752441
+ ],
+ [
+ "▁formatting",
+ -13.006986618041992
+ ],
+ [
+ "▁mosaic",
+ -13.007028579711914
+ ],
+ [
+ "(2017)",
+ -13.007122039794922
+ ],
+ [
+ "777",
+ -13.007257461547852
+ ],
+ [
+ "▁Messenger",
+ -13.007342338562012
+ ],
+ [
+ "dulci",
+ -13.007369041442871
+ ],
+ [
+ "▁(2016)",
+ -13.007420539855957
+ ],
+ [
+ "▁popcorn",
+ -13.007425308227539
+ ],
+ [
+ "▁Presidential",
+ -13.007497787475586
+ ],
+ [
+ "▁brokerage",
+ -13.007564544677734
+ ],
+ [
+ "dachte",
+ -13.00762939453125
+ ],
+ [
+ "verkauf",
+ -13.00768756866455
+ ],
+ [
+ "▁pomme",
+ -13.007721900939941
+ ],
+ [
+ "▁fret",
+ -13.007822036743164
+ ],
+ [
+ "▁revere",
+ -13.007894515991211
+ ],
+ [
+ "▁Canvas",
+ -13.008092880249023
+ ],
+ [
+ "▁Nottingham",
+ -13.008255004882812
+ ],
+ [
+ "▁Refuge",
+ -13.008257865905762
+ ],
+ [
+ "▁injustice",
+ -13.008259773254395
+ ],
+ [
+ "▁External",
+ -13.008264541625977
+ ],
+ [
+ "dincolo",
+ -13.008304595947266
+ ],
+ [
+ "directing",
+ -13.008511543273926
+ ],
+ [
+ "▁Toulouse",
+ -13.008710861206055
+ ],
+ [
+ "▁cheltuieli",
+ -13.008746147155762
+ ],
+ [
+ "▁distrus",
+ -13.008816719055176
+ ],
+ [
+ "impôt",
+ -13.008912086486816
+ ],
+ [
+ "landschaft",
+ -13.008964538574219
+ ],
+ [
+ "passion",
+ -13.00897216796875
+ ],
+ [
+ "▁Hobby",
+ -13.009099006652832
+ ],
+ [
+ "significant",
+ -13.009115219116211
+ ],
+ [
+ "▁Guinea",
+ -13.009209632873535
+ ],
+ [
+ "pecializing",
+ -13.009237289428711
+ ],
+ [
+ "pozitie",
+ -13.009245872497559
+ ],
+ [
+ "bourne",
+ -13.009295463562012
+ ],
+ [
+ "▁mâini",
+ -13.00933837890625
+ ],
+ [
+ "▁CFR",
+ -13.009395599365234
+ ],
+ [
+ "▁Konflikt",
+ -13.009626388549805
+ ],
+ [
+ "▁Vodafone",
+ -13.009626388549805
+ ],
+ [
+ "OUG",
+ -13.009681701660156
+ ],
+ [
+ "▁Übersicht",
+ -13.009735107421875
+ ],
+ [
+ "negotiated",
+ -13.009903907775879
+ ],
+ [
+ "▁gliss",
+ -13.010042190551758
+ ],
+ [
+ "▁Kapital",
+ -13.010111808776855
+ ],
+ [
+ "QC",
+ -13.0101318359375
+ ],
+ [
+ "▁gentleman",
+ -13.01024341583252
+ ],
+ [
+ "Inde",
+ -13.010514259338379
+ ],
+ [
+ "▁immensely",
+ -13.010639190673828
+ ],
+ [
+ "Business",
+ -13.010702133178711
+ ],
+ [
+ "▁04/2",
+ -13.010882377624512
+ ],
+ [
+ "societatea",
+ -13.010973930358887
+ ],
+ [
+ "fluoxetine",
+ -13.011000633239746
+ ],
+ [
+ "▁Wachstum",
+ -13.011000633239746
+ ],
+ [
+ "▁récit",
+ -13.011011123657227
+ ],
+ [
+ "▁Preisvergleich",
+ -13.011034965515137
+ ],
+ [
+ "▁Mohammed",
+ -13.011460304260254
+ ],
+ [
+ "gefangen",
+ -13.011462211608887
+ ],
+ [
+ "▁calibration",
+ -13.011608123779297
+ ],
+ [
+ "bekam",
+ -13.011728286743164
+ ],
+ [
+ "▁FUN",
+ -13.011758804321289
+ ],
+ [
+ "wasting",
+ -13.011839866638184
+ ],
+ [
+ "▁prosper",
+ -13.011862754821777
+ ],
+ [
+ "▁Afghan",
+ -13.011919021606445
+ ],
+ [
+ "▁Heroes",
+ -13.011921882629395
+ ],
+ [
+ "▁VMware",
+ -13.011927604675293
+ ],
+ [
+ "exception",
+ -13.011969566345215
+ ],
+ [
+ "▁înlocui",
+ -13.01244831085205
+ ],
+ [
+ "Neu",
+ -13.01246452331543
+ ],
+ [
+ "initiation",
+ -13.01250171661377
+ ],
+ [
+ "▁Peel",
+ -13.01281452178955
+ ],
+ [
+ "▁cunoaste",
+ -13.012836456298828
+ ],
+ [
+ "▁menschliche",
+ -13.012849807739258
+ ],
+ [
+ "▁poarta",
+ -13.012852668762207
+ ],
+ [
+ "▁congestion",
+ -13.012930870056152
+ ],
+ [
+ "▁îmbunătăț",
+ -13.013103485107422
+ ],
+ [
+ "EUR",
+ -13.013171195983887
+ ],
+ [
+ "▁sushi",
+ -13.01326847076416
+ ],
+ [
+ "Jährige",
+ -13.01329517364502
+ ],
+ [
+ "espoir",
+ -13.013423919677734
+ ],
+ [
+ "inspected",
+ -13.013444900512695
+ ],
+ [
+ "▁etape",
+ -13.013677597045898
+ ],
+ [
+ "▁pharmacist",
+ -13.013754844665527
+ ],
+ [
+ "flect",
+ -13.013840675354004
+ ],
+ [
+ "Changing",
+ -13.013932228088379
+ ],
+ [
+ "▁radiant",
+ -13.014046669006348
+ ],
+ [
+ "Daddy",
+ -13.014275550842285
+ ],
+ [
+ "▁categorii",
+ -13.014360427856445
+ ],
+ [
+ "quête",
+ -13.014628410339355
+ ],
+ [
+ "▁skincare",
+ -13.014657020568848
+ ],
+ [
+ "hébergement",
+ -13.014674186706543
+ ],
+ [
+ "840",
+ -13.01477336883545
+ ],
+ [
+ "awaiting",
+ -13.014822006225586
+ ],
+ [
+ "▁murdered",
+ -13.014841079711914
+ ],
+ [
+ "▁proficient",
+ -13.014863967895508
+ ],
+ [
+ "▁chauffe",
+ -13.014899253845215
+ ],
+ [
+ "▁contur",
+ -13.014937400817871
+ ],
+ [
+ "▁rejoindre",
+ -13.015145301818848
+ ],
+ [
+ "▁foloseste",
+ -13.01521110534668
+ ],
+ [
+ "▁Grup",
+ -13.01535701751709
+ ],
+ [
+ "152",
+ -13.01541519165039
+ ],
+ [
+ "▁workspace",
+ -13.015438079833984
+ ],
+ [
+ "▁primitive",
+ -13.015546798706055
+ ],
+ [
+ "▁Ginger",
+ -13.015557289123535
+ ],
+ [
+ "▁chemotherapy",
+ -13.015595436096191
+ ],
+ [
+ "▁platinum",
+ -13.015596389770508
+ ],
+ [
+ "▁sarcina",
+ -13.01559829711914
+ ],
+ [
+ "▁revival",
+ -13.015820503234863
+ ],
+ [
+ "▁Meditation",
+ -13.016111373901367
+ ],
+ [
+ "▁Vogel",
+ -13.0161714553833
+ ],
+ [
+ "IMA",
+ -13.016359329223633
+ ],
+ [
+ "▁handset",
+ -13.016486167907715
+ ],
+ [
+ "▁Nachmittag",
+ -13.01651668548584
+ ],
+ [
+ "▁déchets",
+ -13.016517639160156
+ ],
+ [
+ "▁Cornwall",
+ -13.0165433883667
+ ],
+ [
+ "▁Curry",
+ -13.016605377197266
+ ],
+ [
+ "▁cuplu",
+ -13.016607284545898
+ ],
+ [
+ "▁Birth",
+ -13.016822814941406
+ ],
+ [
+ "forward",
+ -13.016936302185059
+ ],
+ [
+ "Dezvoltare",
+ -13.016977310180664
+ ],
+ [
+ "▁irgendwie",
+ -13.016980171203613
+ ],
+ [
+ "▁erzielt",
+ -13.016993522644043
+ ],
+ [
+ "LOS",
+ -13.01700496673584
+ ],
+ [
+ "▁overload",
+ -13.01708984375
+ ],
+ [
+ "▁repay",
+ -13.01713752746582
+ ],
+ [
+ "urlaub",
+ -13.017155647277832
+ ],
+ [
+ "7.0",
+ -13.01716423034668
+ ],
+ [
+ "▁Wheat",
+ -13.01748275756836
+ ],
+ [
+ "▁degrab",
+ -13.017488479614258
+ ],
+ [
+ "▁Brock",
+ -13.017491340637207
+ ],
+ [
+ "▁inhabit",
+ -13.0176362991333
+ ],
+ [
+ "▁Speech",
+ -13.017834663391113
+ ],
+ [
+ "directional",
+ -13.017862319946289
+ ],
+ [
+ "▁Mandel",
+ -13.017909049987793
+ ],
+ [
+ "▁erscheinen",
+ -13.01791763305664
+ ],
+ [
+ "consciously",
+ -13.018059730529785
+ ],
+ [
+ "▁sunet",
+ -13.0182523727417
+ ],
+ [
+ "▁stole",
+ -13.018259048461914
+ ],
+ [
+ "▁Utilis",
+ -13.018349647521973
+ ],
+ [
+ "▁obstruction",
+ -13.01852798461914
+ ],
+ [
+ "▁mindfulness",
+ -13.0186767578125
+ ],
+ [
+ "partnering",
+ -13.01868724822998
+ ],
+ [
+ "CSI",
+ -13.018819808959961
+ ],
+ [
+ "204",
+ -13.01905632019043
+ ],
+ [
+ "▁squirrel",
+ -13.019286155700684
+ ],
+ [
+ "▁Rwanda",
+ -13.01975154876709
+ ],
+ [
+ "▁hunters",
+ -13.019850730895996
+ ],
+ [
+ "▁revitaliz",
+ -13.02022647857666
+ ],
+ [
+ "▁avansat",
+ -13.020232200622559
+ ],
+ [
+ "▁Yamaha",
+ -13.020294189453125
+ ],
+ [
+ "foto",
+ -13.020435333251953
+ ],
+ [
+ "▁Vegan",
+ -13.020469665527344
+ ],
+ [
+ "▁pitched",
+ -13.02053165435791
+ ],
+ [
+ "▁Vortrag",
+ -13.020540237426758
+ ],
+ [
+ "traditional",
+ -13.020809173583984
+ ],
+ [
+ "offrent",
+ -13.021024703979492
+ ],
+ [
+ "▁Expression",
+ -13.021315574645996
+ ],
+ [
+ "▁apprécié",
+ -13.021354675292969
+ ],
+ [
+ "▁Christina",
+ -13.021408081054688
+ ],
+ [
+ "eilig",
+ -13.021464347839355
+ ],
+ [
+ "▁verhindern",
+ -13.021599769592285
+ ],
+ [
+ "culturii",
+ -13.021607398986816
+ ],
+ [
+ "Aşa",
+ -13.021703720092773
+ ],
+ [
+ "▁enamel",
+ -13.021756172180176
+ ],
+ [
+ "▁fördern",
+ -13.021771430969238
+ ],
+ [
+ "▁acheté",
+ -13.021798133850098
+ ],
+ [
+ "▁eventuell",
+ -13.021842956542969
+ ],
+ [
+ "▁Sino",
+ -13.021873474121094
+ ],
+ [
+ "▁totodat",
+ -13.022008895874023
+ ],
+ [
+ "accelerated",
+ -13.022202491760254
+ ],
+ [
+ "▁strengthened",
+ -13.02245044708252
+ ],
+ [
+ "corro",
+ -13.022482872009277
+ ],
+ [
+ "4,5",
+ -13.02253246307373
+ ],
+ [
+ "▁Beverly",
+ -13.022533416748047
+ ],
+ [
+ "ulevard",
+ -13.022615432739258
+ ],
+ [
+ "▁hamper",
+ -13.022644996643066
+ ],
+ [
+ "▁Tempe",
+ -13.02268123626709
+ ],
+ [
+ "▁Yacht",
+ -13.022799491882324
+ ],
+ [
+ "▁LGBT",
+ -13.022871017456055
+ ],
+ [
+ "▁fingertips",
+ -13.022991180419922
+ ],
+ [
+ "▁Auftraggeber",
+ -13.02299976348877
+ ],
+ [
+ "▁harbour",
+ -13.0230131149292
+ ],
+ [
+ "blew",
+ -13.0230712890625
+ ],
+ [
+ "▁ideology",
+ -13.023115158081055
+ ],
+ [
+ "▁covenant",
+ -13.023170471191406
+ ],
+ [
+ "▁faction",
+ -13.023419380187988
+ ],
+ [
+ "▁animé",
+ -13.023481369018555
+ ],
+ [
+ "energie",
+ -13.023515701293945
+ ],
+ [
+ "iterführende",
+ -13.02369499206543
+ ],
+ [
+ "▁MAI",
+ -13.023784637451172
+ ],
+ [
+ "▁pluie",
+ -13.023905754089355
+ ],
+ [
+ "▁cathedral",
+ -13.023919105529785
+ ],
+ [
+ "▁chiropractic",
+ -13.023919105529785
+ ],
+ [
+ "monies",
+ -13.023968696594238
+ ],
+ [
+ "▁contraction",
+ -13.024054527282715
+ ],
+ [
+ "pvc",
+ -13.024202346801758
+ ],
+ [
+ "staff",
+ -13.024209022521973
+ ],
+ [
+ "BIT",
+ -13.024216651916504
+ ],
+ [
+ "EET",
+ -13.024514198303223
+ ],
+ [
+ "▁sanction",
+ -13.024575233459473
+ ],
+ [
+ "▁Reiki",
+ -13.024709701538086
+ ],
+ [
+ "Trying",
+ -13.024772644042969
+ ],
+ [
+ "▁endangered",
+ -13.024847984313965
+ ],
+ [
+ "▁Emperor",
+ -13.024849891662598
+ ],
+ [
+ "▁empfi",
+ -13.024909973144531
+ ],
+ [
+ "animation",
+ -13.024998664855957
+ ],
+ [
+ "207",
+ -13.025029182434082
+ ],
+ [
+ "separating",
+ -13.02512264251709
+ ],
+ [
+ "▁lucrative",
+ -13.025148391723633
+ ],
+ [
+ "▁ortho",
+ -13.02524185180664
+ ],
+ [
+ "variété",
+ -13.025266647338867
+ ],
+ [
+ "hésit",
+ -13.025287628173828
+ ],
+ [
+ "nuances",
+ -13.025289535522461
+ ],
+ [
+ "▁$250",
+ -13.025394439697266
+ ],
+ [
+ "▁drumuri",
+ -13.025435447692871
+ ],
+ [
+ "▁unsafe",
+ -13.025446891784668
+ ],
+ [
+ "▁1943",
+ -13.025477409362793
+ ],
+ [
+ "▁automatique",
+ -13.025524139404297
+ ],
+ [
+ "billed",
+ -13.025585174560547
+ ],
+ [
+ "▁rectangle",
+ -13.02578067779541
+ ],
+ [
+ "▁Spannung",
+ -13.025781631469727
+ ],
+ [
+ "▁dévoil",
+ -13.025790214538574
+ ],
+ [
+ "▁perimeter",
+ -13.02580738067627
+ ],
+ [
+ "▁imaginative",
+ -13.02581787109375
+ ],
+ [
+ "actifs",
+ -13.025851249694824
+ ],
+ [
+ "neuve",
+ -13.0259428024292
+ ],
+ [
+ "leagă",
+ -13.026269912719727
+ ],
+ [
+ "gehende",
+ -13.026700973510742
+ ],
+ [
+ "▁Gorgeous",
+ -13.026708602905273
+ ],
+ [
+ "▁impeccable",
+ -13.026708602905273
+ ],
+ [
+ "▁Curtain",
+ -13.026718139648438
+ ],
+ [
+ "▁presume",
+ -13.026731491088867
+ ],
+ [
+ "surpassed",
+ -13.02687931060791
+ ],
+ [
+ "schiff",
+ -13.026927947998047
+ ],
+ [
+ "Allied",
+ -13.02699089050293
+ ],
+ [
+ "fanden",
+ -13.027080535888672
+ ],
+ [
+ "▁célébr",
+ -13.027174949645996
+ ],
+ [
+ "▁phénomène",
+ -13.027174949645996
+ ],
+ [
+ "▁Powell",
+ -13.027413368225098
+ ],
+ [
+ "jean",
+ -13.027631759643555
+ ],
+ [
+ "▁peculiar",
+ -13.027640342712402
+ ],
+ [
+ "▁Antarctic",
+ -13.027641296386719
+ ],
+ [
+ "▁gradient",
+ -13.027663230895996
+ ],
+ [
+ "▁brainstorm",
+ -13.027704238891602
+ ],
+ [
+ "échapp",
+ -13.027726173400879
+ ],
+ [
+ "Bot",
+ -13.027738571166992
+ ],
+ [
+ "cita",
+ -13.027743339538574
+ ],
+ [
+ "▁lumber",
+ -13.027752876281738
+ ],
+ [
+ "weichen",
+ -13.027852058410645
+ ],
+ [
+ "▁Halte",
+ -13.028024673461914
+ ],
+ [
+ "▁noștri",
+ -13.028107643127441
+ ],
+ [
+ "construction",
+ -13.028165817260742
+ ],
+ [
+ "DOC",
+ -13.028236389160156
+ ],
+ [
+ "▁aluat",
+ -13.028319358825684
+ ],
+ [
+ "streamlined",
+ -13.028462409973145
+ ],
+ [
+ "Bio",
+ -13.028494834899902
+ ],
+ [
+ "▁nutritious",
+ -13.028573036193848
+ ],
+ [
+ "▁délicat",
+ -13.0286283493042
+ ],
+ [
+ "▁sticla",
+ -13.028656959533691
+ ],
+ [
+ "OVE",
+ -13.028721809387207
+ ],
+ [
+ "▁panneau",
+ -13.028793334960938
+ ],
+ [
+ "▁hetero",
+ -13.028801918029785
+ ],
+ [
+ "▁annul",
+ -13.028839111328125
+ ],
+ [
+ "IDA",
+ -13.028935432434082
+ ],
+ [
+ "▁pitches",
+ -13.028960227966309
+ ],
+ [
+ "▁Edmonton",
+ -13.029040336608887
+ ],
+ [
+ "mediated",
+ -13.029136657714844
+ ],
+ [
+ "AFP",
+ -13.029139518737793
+ ],
+ [
+ "▁Tibetan",
+ -13.029228210449219
+ ],
+ [
+ "intégration",
+ -13.02934455871582
+ ],
+ [
+ "▁Rox",
+ -13.0294771194458
+ ],
+ [
+ "energia",
+ -13.02950668334961
+ ],
+ [
+ "▁reconnaît",
+ -13.029509544372559
+ ],
+ [
+ "▁ține",
+ -13.029525756835938
+ ],
+ [
+ "▁ignition",
+ -13.029534339904785
+ ],
+ [
+ "Foarte",
+ -13.029541015625
+ ],
+ [
+ "▁HOME",
+ -13.029545783996582
+ ],
+ [
+ "▁MLB",
+ -13.029545783996582
+ ],
+ [
+ "▁Wähle",
+ -13.029590606689453
+ ],
+ [
+ "▁Merkel",
+ -13.029658317565918
+ ],
+ [
+ "poarte",
+ -13.029664993286133
+ ],
+ [
+ "ALT",
+ -13.02979850769043
+ ],
+ [
+ "jenigen",
+ -13.029985427856445
+ ],
+ [
+ "▁conflit",
+ -13.029987335205078
+ ],
+ [
+ "▁buckle",
+ -13.029996871948242
+ ],
+ [
+ "▁cacao",
+ -13.030035018920898
+ ],
+ [
+ "▁représentation",
+ -13.030076026916504
+ ],
+ [
+ "incepand",
+ -13.030267715454102
+ ],
+ [
+ "▁Carroll",
+ -13.030306816101074
+ ],
+ [
+ "▁clientilor",
+ -13.030370712280273
+ ],
+ [
+ "▁immunity",
+ -13.030441284179688
+ ],
+ [
+ "oût",
+ -13.03044319152832
+ ],
+ [
+ "▁Witch",
+ -13.030488014221191
+ ],
+ [
+ "▁Wolfgang",
+ -13.030532836914062
+ ],
+ [
+ "▁prudent",
+ -13.030701637268066
+ ],
+ [
+ "fotograf",
+ -13.03084945678711
+ ],
+ [
+ "paar",
+ -13.030871391296387
+ ],
+ [
+ "ergeti",
+ -13.030927658081055
+ ],
+ [
+ "▁empowerment",
+ -13.031112670898438
+ ],
+ [
+ "▁Admir",
+ -13.03122329711914
+ ],
+ [
+ "▁complémentaire",
+ -13.031340599060059
+ ],
+ [
+ "▁angepasst",
+ -13.031376838684082
+ ],
+ [
+ "▁flirt",
+ -13.031376838684082
+ ],
+ [
+ "▁elektronische",
+ -13.031388282775879
+ ],
+ [
+ "▁stereotype",
+ -13.03140640258789
+ ],
+ [
+ "SIL",
+ -13.031465530395508
+ ],
+ [
+ "▁Realtor",
+ -13.031471252441406
+ ],
+ [
+ "Edit",
+ -13.031774520874023
+ ],
+ [
+ "requête",
+ -13.03181266784668
+ ],
+ [
+ "▁Herstellung",
+ -13.031815528869629
+ ],
+ [
+ "▁cyst",
+ -13.031947135925293
+ ],
+ [
+ "syndic",
+ -13.031994819641113
+ ],
+ [
+ "leni",
+ -13.032007217407227
+ ],
+ [
+ "▁fringe",
+ -13.032020568847656
+ ],
+ [
+ "▁Jardin",
+ -13.032032012939453
+ ],
+ [
+ "▁Vezi",
+ -13.032052993774414
+ ],
+ [
+ "▁Ausstattung",
+ -13.032312393188477
+ ],
+ [
+ "▁glide",
+ -13.032590866088867
+ ],
+ [
+ "▁Andere",
+ -13.032758712768555
+ ],
+ [
+ "▁Haftung",
+ -13.032781600952148
+ ],
+ [
+ "maßnahmen",
+ -13.032788276672363
+ ],
+ [
+ "▁recommandé",
+ -13.032790184020996
+ ],
+ [
+ "▁nave",
+ -13.032793998718262
+ ],
+ [
+ "viziune",
+ -13.033051490783691
+ ],
+ [
+ "▁stimulus",
+ -13.033098220825195
+ ],
+ [
+ "faulty",
+ -13.0331449508667
+ ],
+ [
+ "▁vicinity",
+ -13.033249855041504
+ ],
+ [
+ "▁turnaround",
+ -13.033445358276367
+ ],
+ [
+ "stammt",
+ -13.033846855163574
+ ],
+ [
+ "▁problemlos",
+ -13.033856391906738
+ ],
+ [
+ "▁Establish",
+ -13.03415298461914
+ ],
+ [
+ "▁Silva",
+ -13.034172058105469
+ ],
+ [
+ "▁muzică",
+ -13.034187316894531
+ ],
+ [
+ "▁theatrical",
+ -13.03421401977539
+ ],
+ [
+ "▁braid",
+ -13.034242630004883
+ ],
+ [
+ "▁blieb",
+ -13.034276962280273
+ ],
+ [
+ "158",
+ -13.034296989440918
+ ],
+ [
+ "▁ignorance",
+ -13.034330368041992
+ ],
+ [
+ "onset",
+ -13.034416198730469
+ ],
+ [
+ "zeitlich",
+ -13.034523963928223
+ ],
+ [
+ "▁Sink",
+ -13.034523963928223
+ ],
+ [
+ "▁caractéris",
+ -13.034594535827637
+ ],
+ [
+ "▁kreative",
+ -13.03465747833252
+ ],
+ [
+ "behörde",
+ -13.034677505493164
+ ],
+ [
+ "repairing",
+ -13.034680366516113
+ ],
+ [
+ "▁tumble",
+ -13.034757614135742
+ ],
+ [
+ "zione",
+ -13.034871101379395
+ ],
+ [
+ "▁Evil",
+ -13.03494644165039
+ ],
+ [
+ "▁popping",
+ -13.034952163696289
+ ],
+ [
+ "▁mutant",
+ -13.035025596618652
+ ],
+ [
+ "emme",
+ -13.035030364990234
+ ],
+ [
+ "▁Pleasant",
+ -13.035125732421875
+ ],
+ [
+ "▁appetizer",
+ -13.035125732421875
+ ],
+ [
+ "▁PLEASE",
+ -13.035126686096191
+ ],
+ [
+ "▁physiological",
+ -13.035128593444824
+ ],
+ [
+ "▁Facility",
+ -13.035131454467773
+ ],
+ [
+ "▁quirky",
+ -13.035131454467773
+ ],
+ [
+ "▁colectiv",
+ -13.035154342651367
+ ],
+ [
+ "151",
+ -13.035181999206543
+ ],
+ [
+ "August",
+ -13.03531551361084
+ ],
+ [
+ "▁Jewelry",
+ -13.035327911376953
+ ],
+ [
+ "▁ziar",
+ -13.035481452941895
+ ],
+ [
+ "▁puissant",
+ -13.035489082336426
+ ],
+ [
+ "▁Argument",
+ -13.035595893859863
+ ],
+ [
+ "▁Betracht",
+ -13.035621643066406
+ ],
+ [
+ "▁TRANS",
+ -13.035636901855469
+ ],
+ [
+ "Exception",
+ -13.036011695861816
+ ],
+ [
+ "nosti",
+ -13.036083221435547
+ ],
+ [
+ "▁Geographic",
+ -13.036155700683594
+ ],
+ [
+ "amazingly",
+ -13.036173820495605
+ ],
+ [
+ "▁météo",
+ -13.036181449890137
+ ],
+ [
+ "streit",
+ -13.036314010620117
+ ],
+ [
+ "▁idle",
+ -13.036439895629883
+ ],
+ [
+ "179",
+ -13.036441802978516
+ ],
+ [
+ "▁Bremen",
+ -13.036534309387207
+ ],
+ [
+ "▁Kläger",
+ -13.03653621673584
+ ],
+ [
+ "▁Grammy",
+ -13.036598205566406
+ ],
+ [
+ "▁Philosophy",
+ -13.036613464355469
+ ],
+ [
+ "▁utilizeaz",
+ -13.036779403686523
+ ],
+ [
+ "Accord",
+ -13.036897659301758
+ ],
+ [
+ "▁USDA",
+ -13.036986351013184
+ ],
+ [
+ "Continuing",
+ -13.037010192871094
+ ],
+ [
+ "geschenk",
+ -13.037178039550781
+ ],
+ [
+ "kredit",
+ -13.037248611450195
+ ],
+ [
+ "Laugh",
+ -13.037297248840332
+ ],
+ [
+ "oaring",
+ -13.037406921386719
+ ],
+ [
+ "▁Richter",
+ -13.037460327148438
+ ],
+ [
+ "▁Figur",
+ -13.037938117980957
+ ],
+ [
+ "▁inconsistent",
+ -13.037947654724121
+ ],
+ [
+ "cresterea",
+ -13.038069725036621
+ ],
+ [
+ "▁regeneration",
+ -13.038130760192871
+ ],
+ [
+ "speaking",
+ -13.03818416595459
+ ],
+ [
+ "▁nasal",
+ -13.03824234008789
+ ],
+ [
+ "▁partagé",
+ -13.038259506225586
+ ],
+ [
+ "▁Warranty",
+ -13.038419723510742
+ ],
+ [
+ "▁Mueller",
+ -13.038501739501953
+ ],
+ [
+ "formează",
+ -13.038734436035156
+ ],
+ [
+ "hundert",
+ -13.038745880126953
+ ],
+ [
+ "gemeldet",
+ -13.038893699645996
+ ],
+ [
+ "▁excursions",
+ -13.038912773132324
+ ],
+ [
+ "▁linii",
+ -13.039066314697266
+ ],
+ [
+ "gefährlich",
+ -13.039067268371582
+ ],
+ [
+ "▁schema",
+ -13.03907299041748
+ ],
+ [
+ "nişte",
+ -13.039131164550781
+ ],
+ [
+ "▁roadway",
+ -13.039132118225098
+ ],
+ [
+ "▁regression",
+ -13.039135932922363
+ ],
+ [
+ "▁mână",
+ -13.039366722106934
+ ],
+ [
+ "5.3",
+ -13.039373397827148
+ ],
+ [
+ "▁Spät",
+ -13.039734840393066
+ ],
+ [
+ "▁stubborn",
+ -13.039833068847656
+ ],
+ [
+ "efectele",
+ -13.040030479431152
+ ],
+ [
+ "▁atenţi",
+ -13.040136337280273
+ ],
+ [
+ "▁dovedit",
+ -13.04018497467041
+ ],
+ [
+ "▁Agile",
+ -13.040190696716309
+ ],
+ [
+ "denying",
+ -13.04023265838623
+ ],
+ [
+ "fluss",
+ -13.040620803833008
+ ],
+ [
+ "▁Calvin",
+ -13.04066276550293
+ ],
+ [
+ "Sculpt",
+ -13.04083251953125
+ ],
+ [
+ "égalité",
+ -13.040884971618652
+ ],
+ [
+ "ticket",
+ -13.040977478027344
+ ],
+ [
+ "marketed",
+ -13.041044235229492
+ ],
+ [
+ "holic",
+ -13.041173934936523
+ ],
+ [
+ "▁eCommerce",
+ -13.041346549987793
+ ],
+ [
+ "▁Slip",
+ -13.041369438171387
+ ],
+ [
+ "▁degradation",
+ -13.041736602783203
+ ],
+ [
+ "écart",
+ -13.041742324829102
+ ],
+ [
+ "AGR",
+ -13.041807174682617
+ ],
+ [
+ "▁burglar",
+ -13.041837692260742
+ ],
+ [
+ "▁conjug",
+ -13.041903495788574
+ ],
+ [
+ "LLP",
+ -13.04194164276123
+ ],
+ [
+ "couvrir",
+ -13.041997909545898
+ ],
+ [
+ "▁Hearing",
+ -13.042001724243164
+ ],
+ [
+ "▁canton",
+ -13.042006492614746
+ ],
+ [
+ "▁sixteen",
+ -13.042068481445312
+ ],
+ [
+ "▁Verlust",
+ -13.042097091674805
+ ],
+ [
+ "allied",
+ -13.042268753051758
+ ],
+ [
+ "Performing",
+ -13.042393684387207
+ ],
+ [
+ "▁évoqu",
+ -13.042519569396973
+ ],
+ [
+ "▁bookstore",
+ -13.042574882507324
+ ],
+ [
+ "▁intrebari",
+ -13.042627334594727
+ ],
+ [
+ "▁Hyderabad",
+ -13.042668342590332
+ ],
+ [
+ "▁repertoire",
+ -13.042668342590332
+ ],
+ [
+ "▁cablu",
+ -13.042678833007812
+ ],
+ [
+ "▁Costume",
+ -13.04269790649414
+ ],
+ [
+ "▁Shannon",
+ -13.042713165283203
+ ],
+ [
+ "▁glossy",
+ -13.042800903320312
+ ],
+ [
+ "▁cible",
+ -13.042876243591309
+ ],
+ [
+ "Saint",
+ -13.042984008789062
+ ],
+ [
+ "▁Ultima",
+ -13.043042182922363
+ ],
+ [
+ "▁teint",
+ -13.0432767868042
+ ],
+ [
+ "▁envision",
+ -13.043477058410645
+ ],
+ [
+ "▁thinner",
+ -13.043478965759277
+ ],
+ [
+ "ис",
+ -13.043609619140625
+ ],
+ [
+ "▁bladder",
+ -13.043615341186523
+ ],
+ [
+ "▁Prairie",
+ -13.043618202209473
+ ],
+ [
+ "▁puppies",
+ -13.043633460998535
+ ],
+ [
+ "▁overweight",
+ -13.043729782104492
+ ],
+ [
+ "destined",
+ -13.043925285339355
+ ],
+ [
+ "▁addictive",
+ -13.043935775756836
+ ],
+ [
+ "▁posé",
+ -13.043993949890137
+ ],
+ [
+ "▁mecanism",
+ -13.044112205505371
+ ],
+ [
+ "▁chorus",
+ -13.044466972351074
+ ],
+ [
+ "weder",
+ -13.044528007507324
+ ],
+ [
+ "▁begrüß",
+ -13.044562339782715
+ ],
+ [
+ "▁unsuccessful",
+ -13.044562339782715
+ ],
+ [
+ "executing",
+ -13.044564247131348
+ ],
+ [
+ "▁metadata",
+ -13.044611930847168
+ ],
+ [
+ "traiter",
+ -13.044620513916016
+ ],
+ [
+ "▁borrowed",
+ -13.044649124145508
+ ],
+ [
+ "▁aeroport",
+ -13.044679641723633
+ ],
+ [
+ "▁Bibli",
+ -13.044761657714844
+ ],
+ [
+ "▁youthful",
+ -13.044902801513672
+ ],
+ [
+ "▁Herbert",
+ -13.044913291931152
+ ],
+ [
+ "client",
+ -13.04500961303711
+ ],
+ [
+ "merci",
+ -13.04520034790039
+ ],
+ [
+ "▁Beast",
+ -13.045210838317871
+ ],
+ [
+ "▁Entrepreneur",
+ -13.045230865478516
+ ],
+ [
+ "▁Gelände",
+ -13.045256614685059
+ ],
+ [
+ "▁Packers",
+ -13.045268058776855
+ ],
+ [
+ "formarea",
+ -13.045469284057617
+ ],
+ [
+ "▁Kündigung",
+ -13.045511245727539
+ ],
+ [
+ "▁verdient",
+ -13.045515060424805
+ ],
+ [
+ "▁solutie",
+ -13.045530319213867
+ ],
+ [
+ "figuration",
+ -13.045611381530762
+ ],
+ [
+ "voluntarily",
+ -13.045622825622559
+ ],
+ [
+ "Gregor",
+ -13.045742988586426
+ ],
+ [
+ "▁Uncle",
+ -13.04589557647705
+ ],
+ [
+ "tarifs",
+ -13.045907020568848
+ ],
+ [
+ "▁écologique",
+ -13.045987129211426
+ ],
+ [
+ "▁Investition",
+ -13.045991897583008
+ ],
+ [
+ "exemplar",
+ -13.046127319335938
+ ],
+ [
+ "▁prevede",
+ -13.046144485473633
+ ],
+ [
+ "▁waive",
+ -13.046147346496582
+ ],
+ [
+ "▁Legion",
+ -13.046156883239746
+ ],
+ [
+ "similar",
+ -13.046247482299805
+ ],
+ [
+ "▁shareholder",
+ -13.04626750946045
+ ],
+ [
+ "▁oyster",
+ -13.046476364135742
+ ],
+ [
+ "▁Lightning",
+ -13.046530723571777
+ ],
+ [
+ "experimenting",
+ -13.04662799835205
+ ],
+ [
+ "▁replies",
+ -13.04663372039795
+ ],
+ [
+ "80,000",
+ -13.046757698059082
+ ],
+ [
+ "▁adept",
+ -13.04692554473877
+ ],
+ [
+ "▁Crăciun",
+ -13.046935081481934
+ ],
+ [
+ "▁sanatos",
+ -13.046935081481934
+ ],
+ [
+ "305",
+ -13.04699993133545
+ ],
+ [
+ "specialised",
+ -13.047069549560547
+ ],
+ [
+ "▁drummer",
+ -13.047189712524414
+ ],
+ [
+ "Applicants",
+ -13.04741096496582
+ ],
+ [
+ "objekt",
+ -13.04741096496582
+ ],
+ [
+ "▁Fifth",
+ -13.047446250915527
+ ],
+ [
+ "rgic",
+ -13.047567367553711
+ ],
+ [
+ "theater",
+ -13.047635078430176
+ ],
+ [
+ "▁terminé",
+ -13.047852516174316
+ ],
+ [
+ "▁Englisch",
+ -13.047894477844238
+ ],
+ [
+ "▁Oradea",
+ -13.047898292541504
+ ],
+ [
+ "possesses",
+ -13.0479097366333
+ ],
+ [
+ "illiers",
+ -13.047986030578613
+ ],
+ [
+ "▁refurbish",
+ -13.048110961914062
+ ],
+ [
+ "graphie",
+ -13.04814338684082
+ ],
+ [
+ "▁Booth",
+ -13.048174858093262
+ ],
+ [
+ "▁Ausdruck",
+ -13.048192977905273
+ ],
+ [
+ "▁Marriage",
+ -13.048361778259277
+ ],
+ [
+ "▁knives",
+ -13.048362731933594
+ ],
+ [
+ "▁Relief",
+ -13.048368453979492
+ ],
+ [
+ "▁Clerk",
+ -13.048392295837402
+ ],
+ [
+ "wait",
+ -13.048501014709473
+ ],
+ [
+ "▁probablement",
+ -13.048698425292969
+ ],
+ [
+ "▁suplimentar",
+ -13.048701286315918
+ ],
+ [
+ "dollar",
+ -13.048797607421875
+ ],
+ [
+ "English",
+ -13.04898452758789
+ ],
+ [
+ "866",
+ -13.049300193786621
+ ],
+ [
+ "▁Savannah",
+ -13.049314498901367
+ ],
+ [
+ "▁aftermath",
+ -13.049318313598633
+ ],
+ [
+ "phé",
+ -13.04932689666748
+ ],
+ [
+ "▁Plum",
+ -13.049417495727539
+ ],
+ [
+ "264",
+ -13.049566268920898
+ ],
+ [
+ "2.000",
+ -13.049582481384277
+ ],
+ [
+ "niei",
+ -13.049603462219238
+ ],
+ [
+ "ATP",
+ -13.049803733825684
+ ],
+ [
+ "mila",
+ -13.04985523223877
+ ],
+ [
+ "▁glut",
+ -13.049887657165527
+ ],
+ [
+ "gotta",
+ -13.049891471862793
+ ],
+ [
+ "schütt",
+ -13.049893379211426
+ ],
+ [
+ "klick",
+ -13.049996376037598
+ ],
+ [
+ "whether",
+ -13.050090789794922
+ ],
+ [
+ "▁Wade",
+ -13.050163269042969
+ ],
+ [
+ "▁Riley",
+ -13.050280570983887
+ ],
+ [
+ "Chancellor",
+ -13.050288200378418
+ ],
+ [
+ "▁nebun",
+ -13.050300598144531
+ ],
+ [
+ "▁aufgebaut",
+ -13.050374984741211
+ ],
+ [
+ "steigt",
+ -13.050423622131348
+ ],
+ [
+ "▁entirety",
+ -13.050494194030762
+ ],
+ [
+ "▁telefoane",
+ -13.05074691772461
+ ],
+ [
+ "▁Roulette",
+ -13.050763130187988
+ ],
+ [
+ "1700",
+ -13.050787925720215
+ ],
+ [
+ "▁lycée",
+ -13.050856590270996
+ ],
+ [
+ "rotary",
+ -13.051128387451172
+ ],
+ [
+ "benefited",
+ -13.051170349121094
+ ],
+ [
+ "▁Bisericii",
+ -13.051220893859863
+ ],
+ [
+ "▁Rehabilitation",
+ -13.051220893859863
+ ],
+ [
+ "▁lithium",
+ -13.051228523254395
+ ],
+ [
+ "imposing",
+ -13.051279067993164
+ ],
+ [
+ "176",
+ -13.051329612731934
+ ],
+ [
+ "▁thunder",
+ -13.051527976989746
+ ],
+ [
+ "ăsesc",
+ -13.052000045776367
+ ],
+ [
+ "▁Einblick",
+ -13.052010536193848
+ ],
+ [
+ "oiled",
+ -13.052151679992676
+ ],
+ [
+ "SSA",
+ -13.052181243896484
+ ],
+ [
+ "apparition",
+ -13.05224609375
+ ],
+ [
+ "▁Impress",
+ -13.052273750305176
+ ],
+ [
+ "▁Aboriginal",
+ -13.052297592163086
+ ],
+ [
+ "loos",
+ -13.052383422851562
+ ],
+ [
+ "▁Bread",
+ -13.052440643310547
+ ],
+ [
+ "177",
+ -13.052619934082031
+ ],
+ [
+ "VERS",
+ -13.052638053894043
+ ],
+ [
+ "▁Respect",
+ -13.05271053314209
+ ],
+ [
+ "▁Practical",
+ -13.053047180175781
+ ],
+ [
+ "drafting",
+ -13.05306339263916
+ ],
+ [
+ "си",
+ -13.053099632263184
+ ],
+ [
+ "▁faza",
+ -13.053109169006348
+ ],
+ [
+ "▁sovereign",
+ -13.053123474121094
+ ],
+ [
+ "▁Untersuchung",
+ -13.05314826965332
+ ],
+ [
+ "▁Niveau",
+ -13.053154945373535
+ ],
+ [
+ "transport",
+ -13.053182601928711
+ ],
+ [
+ "▁downstream",
+ -13.053293228149414
+ ],
+ [
+ "▁Milton",
+ -13.053383827209473
+ ],
+ [
+ "▁knob",
+ -13.053390502929688
+ ],
+ [
+ "employeur",
+ -13.053499221801758
+ ],
+ [
+ "▁furnish",
+ -13.053544044494629
+ ],
+ [
+ "weather",
+ -13.053564071655273
+ ],
+ [
+ "LAB",
+ -13.053646087646484
+ ],
+ [
+ "166",
+ -13.053853988647461
+ ],
+ [
+ "▁salaire",
+ -13.053937911987305
+ ],
+ [
+ "▁Carnival",
+ -13.054088592529297
+ ],
+ [
+ "4-0",
+ -13.054168701171875
+ ],
+ [
+ "▁Angle",
+ -13.054291725158691
+ ],
+ [
+ "▁José",
+ -13.054399490356445
+ ],
+ [
+ "architecture",
+ -13.054475784301758
+ ],
+ [
+ "▁Sunset",
+ -13.054574966430664
+ ],
+ [
+ "▁Absolut",
+ -13.054694175720215
+ ],
+ [
+ "▁herrlich",
+ -13.05470085144043
+ ],
+ [
+ "12%",
+ -13.054703712463379
+ ],
+ [
+ "▁Indo",
+ -13.054823875427246
+ ],
+ [
+ "▁Komfort",
+ -13.055049896240234
+ ],
+ [
+ "▁acțiuni",
+ -13.05505084991455
+ ],
+ [
+ "energize",
+ -13.055085182189941
+ ],
+ [
+ "▁Warning",
+ -13.055171966552734
+ ],
+ [
+ "▁Sunny",
+ -13.055216789245605
+ ],
+ [
+ "▁razor",
+ -13.055489540100098
+ ],
+ [
+ "▁psychic",
+ -13.055490493774414
+ ],
+ [
+ "▁convivial",
+ -13.055525779724121
+ ],
+ [
+ "Voraussetzungen",
+ -13.05555534362793
+ ],
+ [
+ "IMO",
+ -13.055622100830078
+ ],
+ [
+ "opérateur",
+ -13.055743217468262
+ ],
+ [
+ "▁langjährige",
+ -13.05575942993164
+ ],
+ [
+ "▁Spanie",
+ -13.055901527404785
+ ],
+ [
+ "pulmonary",
+ -13.056004524230957
+ ],
+ [
+ "▁Bingo",
+ -13.056050300598145
+ ],
+ [
+ "▁confession",
+ -13.056096076965332
+ ],
+ [
+ "▁Petru",
+ -13.056100845336914
+ ],
+ [
+ "▁prerequisite",
+ -13.056164741516113
+ ],
+ [
+ "▁dodge",
+ -13.056352615356445
+ ],
+ [
+ "▁McN",
+ -13.056436538696289
+ ],
+ [
+ "▁originate",
+ -13.056577682495117
+ ],
+ [
+ "▁nettoy",
+ -13.056612014770508
+ ],
+ [
+ "▁$14",
+ -13.056645393371582
+ ],
+ [
+ "▁Bride",
+ -13.05669116973877
+ ],
+ [
+ "▁noisy",
+ -13.05673885345459
+ ],
+ [
+ "▁Worcester",
+ -13.056963920593262
+ ],
+ [
+ "▁Surrey",
+ -13.056982040405273
+ ],
+ [
+ "harmonis",
+ -13.057110786437988
+ ],
+ [
+ "▁représentant",
+ -13.057304382324219
+ ],
+ [
+ "organisée",
+ -13.057475090026855
+ ],
+ [
+ "truction",
+ -13.057513236999512
+ ],
+ [
+ "injected",
+ -13.057597160339355
+ ],
+ [
+ "▁Suzuki",
+ -13.057924270629883
+ ],
+ [
+ "▁japonais",
+ -13.057924270629883
+ ],
+ [
+ "▁turquoise",
+ -13.057924270629883
+ ],
+ [
+ "▁Peut",
+ -13.058004379272461
+ ],
+ [
+ "▁Sequ",
+ -13.058028221130371
+ ],
+ [
+ "slated",
+ -13.058037757873535
+ ],
+ [
+ "▁Alma",
+ -13.058215141296387
+ ],
+ [
+ "▁gebraucht",
+ -13.05827522277832
+ ],
+ [
+ "gängig",
+ -13.058281898498535
+ ],
+ [
+ "▁commis",
+ -13.058377265930176
+ ],
+ [
+ "ACS",
+ -13.05856990814209
+ ],
+ [
+ "pressure",
+ -13.058664321899414
+ ],
+ [
+ "cured",
+ -13.05874252319336
+ ],
+ [
+ "▁Jackie",
+ -13.058757781982422
+ ],
+ [
+ "▁Kashmir",
+ -13.05888557434082
+ ],
+ [
+ "▁recruited",
+ -13.059000968933105
+ ],
+ [
+ "▁vécu",
+ -13.059011459350586
+ ],
+ [
+ "▁opus",
+ -13.059052467346191
+ ],
+ [
+ "kWh",
+ -13.05927562713623
+ ],
+ [
+ "▁tapping",
+ -13.059292793273926
+ ],
+ [
+ "▁tehnologie",
+ -13.05931282043457
+ ],
+ [
+ "▁Gentle",
+ -13.059365272521973
+ ],
+ [
+ "▁bombard",
+ -13.059372901916504
+ ],
+ [
+ "▁caméra",
+ -13.059427261352539
+ ],
+ [
+ "züglich",
+ -13.059431076049805
+ ],
+ [
+ "▁bingo",
+ -13.059453010559082
+ ],
+ [
+ "private",
+ -13.059496879577637
+ ],
+ [
+ "▁mediator",
+ -13.059642791748047
+ ],
+ [
+ "▁carbohydrates",
+ -13.059847831726074
+ ],
+ [
+ "▁workmanship",
+ -13.059849739074707
+ ],
+ [
+ "▁Combat",
+ -13.059853553771973
+ ],
+ [
+ "▁Mickey",
+ -13.059901237487793
+ ],
+ [
+ "▁distressed",
+ -13.059908866882324
+ ],
+ [
+ "lucrează",
+ -13.059924125671387
+ ],
+ [
+ "treatment",
+ -13.06007194519043
+ ],
+ [
+ "▁Einwohner",
+ -13.060330390930176
+ ],
+ [
+ "▁glaze",
+ -13.060386657714844
+ ],
+ [
+ "scholarly",
+ -13.06043529510498
+ ],
+ [
+ "ROC",
+ -13.060750007629395
+ ],
+ [
+ "▁Darwin",
+ -13.060774803161621
+ ],
+ [
+ "drückt",
+ -13.060775756835938
+ ],
+ [
+ "▁treadmill",
+ -13.060819625854492
+ ],
+ [
+ "ntz",
+ -13.060830116271973
+ ],
+ [
+ "620",
+ -13.061087608337402
+ ],
+ [
+ "surface",
+ -13.061148643493652
+ ],
+ [
+ "▁vieţii",
+ -13.0612211227417
+ ],
+ [
+ "990",
+ -13.061296463012695
+ ],
+ [
+ "▁doigt",
+ -13.061341285705566
+ ],
+ [
+ "▁explor",
+ -13.061450004577637
+ ],
+ [
+ "▁asistent",
+ -13.061670303344727
+ ],
+ [
+ "coloriage",
+ -13.061734199523926
+ ],
+ [
+ "▁Martinez",
+ -13.061758041381836
+ ],
+ [
+ "▁antibodies",
+ -13.061775207519531
+ ],
+ [
+ "Schülerinnen",
+ -13.061779975891113
+ ],
+ [
+ "Honestly",
+ -13.06178092956543
+ ],
+ [
+ "grabbing",
+ -13.061871528625488
+ ],
+ [
+ "▁Cardiff",
+ -13.061897277832031
+ ],
+ [
+ "▁Trophy",
+ -13.062084197998047
+ ],
+ [
+ "▁pupil",
+ -13.062117576599121
+ ],
+ [
+ "▁invoke",
+ -13.062161445617676
+ ],
+ [
+ "bezüglich",
+ -13.062193870544434
+ ],
+ [
+ "Anschließend",
+ -13.062275886535645
+ ],
+ [
+ "perks",
+ -13.062360763549805
+ ],
+ [
+ "530",
+ -13.062373161315918
+ ],
+ [
+ "▁emblem",
+ -13.062431335449219
+ ],
+ [
+ "770",
+ -13.062543869018555
+ ],
+ [
+ "clairement",
+ -13.062590599060059
+ ],
+ [
+ "▁sublinia",
+ -13.062597274780273
+ ],
+ [
+ "▁1910",
+ -13.062719345092773
+ ],
+ [
+ "▁Embassy",
+ -13.062740325927734
+ ],
+ [
+ "▁Valencia",
+ -13.062740325927734
+ ],
+ [
+ "▁catastrophic",
+ -13.062740325927734
+ ],
+ [
+ "▁simulator",
+ -13.06274700164795
+ ],
+ [
+ "Pierre",
+ -13.062766075134277
+ ],
+ [
+ "▁doorstep",
+ -13.062806129455566
+ ],
+ [
+ "▁rallie",
+ -13.062881469726562
+ ],
+ [
+ "▁șans",
+ -13.062891960144043
+ ],
+ [
+ "▁crosses",
+ -13.06300163269043
+ ],
+ [
+ "▁zodi",
+ -13.06312084197998
+ ],
+ [
+ "Next",
+ -13.06314754486084
+ ],
+ [
+ "▁rebuilt",
+ -13.063152313232422
+ ],
+ [
+ "▁panorama",
+ -13.063222885131836
+ ],
+ [
+ "196",
+ -13.06324291229248
+ ],
+ [
+ "▁erinnert",
+ -13.06370735168457
+ ],
+ [
+ "lism",
+ -13.06371784210205
+ ],
+ [
+ "opened",
+ -13.06383228302002
+ ],
+ [
+ "▁breakout",
+ -13.064126014709473
+ ],
+ [
+ "▁mosque",
+ -13.064153671264648
+ ],
+ [
+ "boc",
+ -13.064507484436035
+ ],
+ [
+ "▁grout",
+ -13.064568519592285
+ ],
+ [
+ "▁Gather",
+ -13.064582824707031
+ ],
+ [
+ "▁vampire",
+ -13.06467342376709
+ ],
+ [
+ "▁tandem",
+ -13.064684867858887
+ ],
+ [
+ "▁pastra",
+ -13.064702033996582
+ ],
+ [
+ "▁lösen",
+ -13.064794540405273
+ ],
+ [
+ "▁discontinu",
+ -13.064826965332031
+ ],
+ [
+ "fuses",
+ -13.064885139465332
+ ],
+ [
+ "▁identitate",
+ -13.064947128295898
+ ],
+ [
+ "BAC",
+ -13.064964294433594
+ ],
+ [
+ "▁$100,000",
+ -13.065122604370117
+ ],
+ [
+ "Finder",
+ -13.06515121459961
+ ],
+ [
+ "▁Leicester",
+ -13.065157890319824
+ ],
+ [
+ "▁1933",
+ -13.065159797668457
+ ],
+ [
+ "informatiile",
+ -13.065234184265137
+ ],
+ [
+ "lädt",
+ -13.065309524536133
+ ],
+ [
+ "iggle",
+ -13.065399169921875
+ ],
+ [
+ "▁Discuss",
+ -13.065462112426758
+ ],
+ [
+ "distributing",
+ -13.065470695495605
+ ],
+ [
+ "▁disappoint",
+ -13.065475463867188
+ ],
+ [
+ "ecţia",
+ -13.065611839294434
+ ],
+ [
+ "▁condiment",
+ -13.065640449523926
+ ],
+ [
+ "▁Marriott",
+ -13.065642356872559
+ ],
+ [
+ "▁entspannt",
+ -13.065644264221191
+ ],
+ [
+ "arbitrary",
+ -13.06564998626709
+ ],
+ [
+ "rühren",
+ -13.06574821472168
+ ],
+ [
+ "Intensiv",
+ -13.065771102905273
+ ],
+ [
+ "eliminare",
+ -13.065895080566406
+ ],
+ [
+ "muster",
+ -13.06594467163086
+ ],
+ [
+ "▁komplexe",
+ -13.066130638122559
+ ],
+ [
+ "▁(2008)",
+ -13.066184997558594
+ ],
+ [
+ "absolument",
+ -13.066349029541016
+ ],
+ [
+ "aloo",
+ -13.066420555114746
+ ],
+ [
+ "cererea",
+ -13.06655216217041
+ ],
+ [
+ "▁imobiliar",
+ -13.066696166992188
+ ],
+ [
+ "▁paramount",
+ -13.066705703735352
+ ],
+ [
+ "▁Vince",
+ -13.066723823547363
+ ],
+ [
+ "pov",
+ -13.067076683044434
+ ],
+ [
+ "▁conveyor",
+ -13.067549705505371
+ ],
+ [
+ "▁Natalie",
+ -13.067583084106445
+ ],
+ [
+ "▁Comedy",
+ -13.067623138427734
+ ],
+ [
+ "Developing",
+ -13.0678129196167
+ ],
+ [
+ "disputed",
+ -13.067878723144531
+ ],
+ [
+ "164",
+ -13.067911148071289
+ ],
+ [
+ "▁Communist",
+ -13.067949295043945
+ ],
+ [
+ "▁Bahnhof",
+ -13.06806468963623
+ ],
+ [
+ "dokument",
+ -13.068145751953125
+ ],
+ [
+ "▁Somali",
+ -13.06828498840332
+ ],
+ [
+ "▁Strasbourg",
+ -13.068503379821777
+ ],
+ [
+ "▁Technician",
+ -13.068550109863281
+ ],
+ [
+ "▁subsidies",
+ -13.068633079528809
+ ],
+ [
+ "judeţul",
+ -13.068723678588867
+ ],
+ [
+ "▁bible",
+ -13.068769454956055
+ ],
+ [
+ "gefahren",
+ -13.068855285644531
+ ],
+ [
+ "▁literal",
+ -13.068882942199707
+ ],
+ [
+ "▁diminish",
+ -13.068940162658691
+ ],
+ [
+ "Sfântul",
+ -13.0689697265625
+ ],
+ [
+ "▁doreșt",
+ -13.068978309631348
+ ],
+ [
+ "▁Xiaomi",
+ -13.069036483764648
+ ],
+ [
+ "▁planète",
+ -13.069130897521973
+ ],
+ [
+ "▁LTD",
+ -13.069175720214844
+ ],
+ [
+ "▁Zugriff",
+ -13.069196701049805
+ ],
+ [
+ "beginn",
+ -13.06921672821045
+ ],
+ [
+ "▁Einführung",
+ -13.069294929504395
+ ],
+ [
+ "▁coronar",
+ -13.069393157958984
+ ],
+ [
+ "lomi",
+ -13.0693941116333
+ ],
+ [
+ "▁Accueil",
+ -13.0695219039917
+ ],
+ [
+ "scanned",
+ -13.069528579711914
+ ],
+ [
+ "▁Banque",
+ -13.06952953338623
+ ],
+ [
+ "▁réaction",
+ -13.069531440734863
+ ],
+ [
+ "▁Hoffman",
+ -13.069546699523926
+ ],
+ [
+ "▁merveille",
+ -13.069637298583984
+ ],
+ [
+ "navigating",
+ -13.069719314575195
+ ],
+ [
+ "schalten",
+ -13.06984806060791
+ ],
+ [
+ "▁ieşi",
+ -13.070136070251465
+ ],
+ [
+ "1-6",
+ -13.070175170898438
+ ],
+ [
+ "▁frustr",
+ -13.070670127868652
+ ],
+ [
+ "▁réfléchi",
+ -13.0709810256958
+ ],
+ [
+ "▁difuz",
+ -13.071100234985352
+ ],
+ [
+ "▁freue",
+ -13.07121753692627
+ ],
+ [
+ "besuch",
+ -13.071349143981934
+ ],
+ [
+ "153",
+ -13.071386337280273
+ ],
+ [
+ "▁butterflies",
+ -13.071467399597168
+ ],
+ [
+ "▁terrifying",
+ -13.071467399597168
+ ],
+ [
+ "▁încuraj",
+ -13.071468353271484
+ ],
+ [
+ "▁Château",
+ -13.071470260620117
+ ],
+ [
+ "▁contingent",
+ -13.071474075317383
+ ],
+ [
+ "▁abusive",
+ -13.0714750289917
+ ],
+ [
+ "▁SharePoint",
+ -13.07148551940918
+ ],
+ [
+ "▁skating",
+ -13.071573257446289
+ ],
+ [
+ "▁militaire",
+ -13.07166576385498
+ ],
+ [
+ "▁Vig",
+ -13.071690559387207
+ ],
+ [
+ "omics",
+ -13.071840286254883
+ ],
+ [
+ "▁Blockchain",
+ -13.07197093963623
+ ],
+ [
+ "▁principii",
+ -13.071975708007812
+ ],
+ [
+ "▁permitting",
+ -13.071979522705078
+ ],
+ [
+ "optimisation",
+ -13.072270393371582
+ ],
+ [
+ "▁maintien",
+ -13.072328567504883
+ ],
+ [
+ "▁Aluminum",
+ -13.072442054748535
+ ],
+ [
+ "▁Plymouth",
+ -13.072443008422852
+ ],
+ [
+ "▁Weiterbildung",
+ -13.072457313537598
+ ],
+ [
+ "▁Finanzierung",
+ -13.072505950927734
+ ],
+ [
+ "▁Kerala",
+ -13.072514533996582
+ ],
+ [
+ "insulated",
+ -13.072668075561523
+ ],
+ [
+ "▁loaf",
+ -13.072802543640137
+ ],
+ [
+ "▁Sammlung",
+ -13.072929382324219
+ ],
+ [
+ "▁îndepărt",
+ -13.072930335998535
+ ],
+ [
+ "▁Gewerbe",
+ -13.072942733764648
+ ],
+ [
+ "udel",
+ -13.072988510131836
+ ],
+ [
+ "▁coursework",
+ -13.073104858398438
+ ],
+ [
+ "▁Darstellung",
+ -13.073246002197266
+ ],
+ [
+ "▁indeplin",
+ -13.073433876037598
+ ],
+ [
+ "▁Gandhi",
+ -13.073434829711914
+ ],
+ [
+ "tossed",
+ -13.07361888885498
+ ],
+ [
+ "ewed",
+ -13.073844909667969
+ ],
+ [
+ "▁classement",
+ -13.073884963989258
+ ],
+ [
+ "▁Protestant",
+ -13.073905944824219
+ ],
+ [
+ "▁frumoasă",
+ -13.073905944824219
+ ],
+ [
+ "▁pantalon",
+ -13.073906898498535
+ ],
+ [
+ "▁rivet",
+ -13.073966979980469
+ ],
+ [
+ "▁Echt",
+ -13.0741605758667
+ ],
+ [
+ "erviciului",
+ -13.07421588897705
+ ],
+ [
+ "fabricated",
+ -13.074322700500488
+ ],
+ [
+ "Compania",
+ -13.074372291564941
+ ],
+ [
+ "▁juvenile",
+ -13.074394226074219
+ ],
+ [
+ "▁souligne",
+ -13.07444953918457
+ ],
+ [
+ "▁chrono",
+ -13.07447338104248
+ ],
+ [
+ "▁VII",
+ -13.074594497680664
+ ],
+ [
+ "▁Kirch",
+ -13.074714660644531
+ ],
+ [
+ "catcher",
+ -13.075014114379883
+ ],
+ [
+ "salv",
+ -13.075263023376465
+ ],
+ [
+ "▁Enforcement",
+ -13.075370788574219
+ ],
+ [
+ "▁Penguin",
+ -13.075410842895508
+ ],
+ [
+ "kowski",
+ -13.075465202331543
+ ],
+ [
+ "▁2:1",
+ -13.075470924377441
+ ],
+ [
+ "gesundheit",
+ -13.075475692749023
+ ],
+ [
+ "▁unveil",
+ -13.075519561767578
+ ],
+ [
+ "bending",
+ -13.075531959533691
+ ],
+ [
+ "▁conecta",
+ -13.075579643249512
+ ],
+ [
+ "▁faim",
+ -13.075885772705078
+ ],
+ [
+ "▁MacBook",
+ -13.075969696044922
+ ],
+ [
+ "versuch",
+ -13.07600212097168
+ ],
+ [
+ "▁regiuni",
+ -13.076029777526855
+ ],
+ [
+ "▁Willow",
+ -13.076184272766113
+ ],
+ [
+ "▁finanziell",
+ -13.076303482055664
+ ],
+ [
+ "▁nurturing",
+ -13.076354026794434
+ ],
+ [
+ "impuls",
+ -13.076370239257812
+ ],
+ [
+ "▁funktionieren",
+ -13.076371192932129
+ ],
+ [
+ "▁rezult",
+ -13.076554298400879
+ ],
+ [
+ "▁spui",
+ -13.076593399047852
+ ],
+ [
+ "▁walkway",
+ -13.076653480529785
+ ],
+ [
+ "▁Rauch",
+ -13.076708793640137
+ ],
+ [
+ "169",
+ -13.076793670654297
+ ],
+ [
+ "610",
+ -13.076863288879395
+ ],
+ [
+ "▁scazut",
+ -13.0773286819458
+ ],
+ [
+ "▁Garrett",
+ -13.077329635620117
+ ],
+ [
+ "▁necesită",
+ -13.077352523803711
+ ],
+ [
+ "Articolul",
+ -13.077364921569824
+ ],
+ [
+ "numită",
+ -13.077371597290039
+ ],
+ [
+ "Coastal",
+ -13.077383041381836
+ ],
+ [
+ "▁canned",
+ -13.077421188354492
+ ],
+ [
+ "▁Friendly",
+ -13.077499389648438
+ ],
+ [
+ "dissolved",
+ -13.0775728225708
+ ],
+ [
+ "seid",
+ -13.077674865722656
+ ],
+ [
+ "▁feminin",
+ -13.077685356140137
+ ],
+ [
+ "▁fetch",
+ -13.077710151672363
+ ],
+ [
+ "▁Accent",
+ -13.077767372131348
+ ],
+ [
+ "phrase",
+ -13.077771186828613
+ ],
+ [
+ "effekt",
+ -13.077775955200195
+ ],
+ [
+ "▁Progressive",
+ -13.077777862548828
+ ],
+ [
+ "▁canadien",
+ -13.077820777893066
+ ],
+ [
+ "iety",
+ -13.077839851379395
+ ],
+ [
+ "eignen",
+ -13.077984809875488
+ ],
+ [
+ "paraître",
+ -13.07812213897705
+ ],
+ [
+ "▁asylum",
+ -13.07833194732666
+ ],
+ [
+ "▁Albany",
+ -13.078362464904785
+ ],
+ [
+ "▁remis",
+ -13.078386306762695
+ ],
+ [
+ "▁Joyce",
+ -13.078664779663086
+ ],
+ [
+ "schätzt",
+ -13.078784942626953
+ ],
+ [
+ "▁begleiten",
+ -13.078801155090332
+ ],
+ [
+ "▁Siemens",
+ -13.079007148742676
+ ],
+ [
+ "▁schlimm",
+ -13.079061508178711
+ ],
+ [
+ "▁Libra",
+ -13.079254150390625
+ ],
+ [
+ "▁Composite",
+ -13.079290390014648
+ ],
+ [
+ "▁écr",
+ -13.079315185546875
+ ],
+ [
+ "disciplina",
+ -13.079379081726074
+ ],
+ [
+ "▁premature",
+ -13.079630851745605
+ ],
+ [
+ "▁scopuri",
+ -13.079681396484375
+ ],
+ [
+ "ffnung",
+ -13.079715728759766
+ ],
+ [
+ "7000",
+ -13.079726219177246
+ ],
+ [
+ "▁conséquent",
+ -13.079780578613281
+ ],
+ [
+ "▁côte",
+ -13.079787254333496
+ ],
+ [
+ "celul",
+ -13.079872131347656
+ ],
+ [
+ "▁fourteen",
+ -13.079940795898438
+ ],
+ [
+ "▁Riverside",
+ -13.080077171325684
+ ],
+ [
+ "gemacht",
+ -13.08013916015625
+ ],
+ [
+ "▁volcanic",
+ -13.080272674560547
+ ],
+ [
+ "▁Salesforce",
+ -13.080315589904785
+ ],
+ [
+ "▁Granite",
+ -13.080317497253418
+ ],
+ [
+ "▁Zentral",
+ -13.080329895019531
+ ],
+ [
+ "▁Female",
+ -13.080341339111328
+ ],
+ [
+ "▁culmin",
+ -13.08047103881836
+ ],
+ [
+ "▁urmatoare",
+ -13.080547332763672
+ ],
+ [
+ "toxicity",
+ -13.080560684204102
+ ],
+ [
+ "▁mâna",
+ -13.080678939819336
+ ],
+ [
+ "▁Umfang",
+ -13.080764770507812
+ ],
+ [
+ "▁Encore",
+ -13.08077621459961
+ ],
+ [
+ "▁Edgar",
+ -13.080831527709961
+ ],
+ [
+ "▁négoci",
+ -13.080852508544922
+ ],
+ [
+ "njeux",
+ -13.080873489379883
+ ],
+ [
+ "▁variance",
+ -13.080917358398438
+ ],
+ [
+ "▁Functional",
+ -13.080973625183105
+ ],
+ [
+ "172",
+ -13.081046104431152
+ ],
+ [
+ "▁dissolve",
+ -13.0811185836792
+ ],
+ [
+ "förderung",
+ -13.081188201904297
+ ],
+ [
+ "▁Brilliant",
+ -13.081254959106445
+ ],
+ [
+ "▁comprehension",
+ -13.081254959106445
+ ],
+ [
+ "▁soybean",
+ -13.081254959106445
+ ],
+ [
+ "▁standalone",
+ -13.081255912780762
+ ],
+ [
+ "▁Communi",
+ -13.081303596496582
+ ],
+ [
+ "▁ajut",
+ -13.081313133239746
+ ],
+ [
+ "▁lavish",
+ -13.081338882446289
+ ],
+ [
+ "Ouest",
+ -13.081384658813477
+ ],
+ [
+ "▁Maggie",
+ -13.081385612487793
+ ],
+ [
+ "▁evolutionary",
+ -13.081550598144531
+ ],
+ [
+ "bowel",
+ -13.081575393676758
+ ],
+ [
+ "▁glyco",
+ -13.081626892089844
+ ],
+ [
+ "▁Happi",
+ -13.081706047058105
+ ],
+ [
+ "organising",
+ -13.081710815429688
+ ],
+ [
+ "▁übernimm",
+ -13.081727027893066
+ ],
+ [
+ "▁snowboard",
+ -13.081793785095215
+ ],
+ [
+ "▁prévention",
+ -13.081830024719238
+ ],
+ [
+ "▁Celebrate",
+ -13.082160949707031
+ ],
+ [
+ "▁pottery",
+ -13.082254409790039
+ ],
+ [
+ "▁Outstanding",
+ -13.082328796386719
+ ],
+ [
+ "▁toamna",
+ -13.082331657409668
+ ],
+ [
+ "▁graceful",
+ -13.082548141479492
+ ],
+ [
+ "197",
+ -13.082559585571289
+ ],
+ [
+ "strecke",
+ -13.082598686218262
+ ],
+ [
+ "▁medizinische",
+ -13.082733154296875
+ ],
+ [
+ "216",
+ -13.082839965820312
+ ],
+ [
+ "▁prune",
+ -13.082868576049805
+ ],
+ [
+ "Pourtant",
+ -13.083000183105469
+ ],
+ [
+ "▁Difference",
+ -13.083224296569824
+ ],
+ [
+ "▁factura",
+ -13.083830833435059
+ ],
+ [
+ "Mass",
+ -13.084161758422852
+ ],
+ [
+ "▁Enhanc",
+ -13.084190368652344
+ ],
+ [
+ "upholstered",
+ -13.084209442138672
+ ],
+ [
+ "▁übernommen",
+ -13.084209442138672
+ ],
+ [
+ "▁mitigation",
+ -13.084210395812988
+ ],
+ [
+ "▁Hidden",
+ -13.084219932556152
+ ],
+ [
+ "▁Häuser",
+ -13.084234237670898
+ ],
+ [
+ "▁Pavel",
+ -13.084403991699219
+ ],
+ [
+ "▁congress",
+ -13.084512710571289
+ ],
+ [
+ "▁antibody",
+ -13.084598541259766
+ ],
+ [
+ "▁stitches",
+ -13.084811210632324
+ ],
+ [
+ "▁colonies",
+ -13.084820747375488
+ ],
+ [
+ "Into",
+ -13.084900856018066
+ ],
+ [
+ "▁démo",
+ -13.084924697875977
+ ],
+ [
+ "▁MVP",
+ -13.085041046142578
+ ],
+ [
+ "▁replay",
+ -13.085062026977539
+ ],
+ [
+ "▁usoara",
+ -13.08522891998291
+ ],
+ [
+ "▁Breast",
+ -13.085278511047363
+ ],
+ [
+ "ooney",
+ -13.085336685180664
+ ],
+ [
+ "▁außen",
+ -13.085663795471191
+ ],
+ [
+ "▁Motorola",
+ -13.085695266723633
+ ],
+ [
+ "▁spalat",
+ -13.08578109741211
+ ],
+ [
+ "euillez",
+ -13.086088180541992
+ ],
+ [
+ "▁jeunesse",
+ -13.086170196533203
+ ],
+ [
+ "▁pastoral",
+ -13.086174011230469
+ ],
+ [
+ "▁Sussex",
+ -13.086185455322266
+ ],
+ [
+ "▁stencil",
+ -13.08619213104248
+ ],
+ [
+ "▁organismului",
+ -13.086504936218262
+ ],
+ [
+ "seized",
+ -13.086649894714355
+ ],
+ [
+ "▁întrebare",
+ -13.086865425109863
+ ],
+ [
+ "cliquez",
+ -13.086874961853027
+ ],
+ [
+ "5.7",
+ -13.086984634399414
+ ],
+ [
+ "▁Yama",
+ -13.087080955505371
+ ],
+ [
+ "painted",
+ -13.08708667755127
+ ],
+ [
+ "▁Swimming",
+ -13.087176322937012
+ ],
+ [
+ "Rhythm",
+ -13.087202072143555
+ ],
+ [
+ "▁sorrow",
+ -13.087210655212402
+ ],
+ [
+ "▁Movers",
+ -13.08731460571289
+ ],
+ [
+ "renforcer",
+ -13.08735466003418
+ ],
+ [
+ "▁Wach",
+ -13.087381362915039
+ ],
+ [
+ "0,00",
+ -13.087390899658203
+ ],
+ [
+ "▁glove",
+ -13.08753490447998
+ ],
+ [
+ "▁stâng",
+ -13.087669372558594
+ ],
+ [
+ "rgendwann",
+ -13.087687492370605
+ ],
+ [
+ "▁Philippine",
+ -13.08769416809082
+ ],
+ [
+ "▁anunțat",
+ -13.087716102600098
+ ],
+ [
+ "▁Coleman",
+ -13.087723731994629
+ ],
+ [
+ "affir",
+ -13.087918281555176
+ ],
+ [
+ "uleiul",
+ -13.08808422088623
+ ],
+ [
+ "▁Coconut",
+ -13.088197708129883
+ ],
+ [
+ "▁Supplement",
+ -13.088210105895996
+ ],
+ [
+ "haudiere",
+ -13.088293075561523
+ ],
+ [
+ "▁kettle",
+ -13.088313102722168
+ ],
+ [
+ "▁3,5",
+ -13.088370323181152
+ ],
+ [
+ "refurbished",
+ -13.088425636291504
+ ],
+ [
+ "esthétique",
+ -13.088665962219238
+ ],
+ [
+ "performing",
+ -13.088667869567871
+ ],
+ [
+ "▁Engag",
+ -13.088762283325195
+ ],
+ [
+ "Group",
+ -13.088801383972168
+ ],
+ [
+ "▁viande",
+ -13.088887214660645
+ ],
+ [
+ "▁oricum",
+ -13.088888168334961
+ ],
+ [
+ "Spitalul",
+ -13.089093208312988
+ ],
+ [
+ "▁cesse",
+ -13.089110374450684
+ ],
+ [
+ "▁contradiction",
+ -13.089130401611328
+ ],
+ [
+ "▁Chrysler",
+ -13.089154243469238
+ ],
+ [
+ "▁poultry",
+ -13.089154243469238
+ ],
+ [
+ "▁thirteen",
+ -13.089154243469238
+ ],
+ [
+ "▁sightseeing",
+ -13.089155197143555
+ ],
+ [
+ "▁Miguel",
+ -13.089158058166504
+ ],
+ [
+ "▁terminology",
+ -13.089334487915039
+ ],
+ [
+ "▁Genetic",
+ -13.089553833007812
+ ],
+ [
+ "commercial",
+ -13.08963394165039
+ ],
+ [
+ "gehoben",
+ -13.08965015411377
+ ],
+ [
+ "RIGHT",
+ -13.08995532989502
+ ],
+ [
+ "▁proprietate",
+ -13.089990615844727
+ ],
+ [
+ "▁Cannes",
+ -13.090012550354004
+ ],
+ [
+ "▁klicken",
+ -13.090023040771484
+ ],
+ [
+ "▁Belgique",
+ -13.0901460647583
+ ],
+ [
+ "tapped",
+ -13.09034538269043
+ ],
+ [
+ "kinetic",
+ -13.090569496154785
+ ],
+ [
+ "▁feuilles",
+ -13.090673446655273
+ ],
+ [
+ "whitening",
+ -13.090760231018066
+ ],
+ [
+ "Any",
+ -13.090946197509766
+ ],
+ [
+ "Manager",
+ -13.091099739074707
+ ],
+ [
+ "▁constatat",
+ -13.091106414794922
+ ],
+ [
+ "▁Myanmar",
+ -13.091140747070312
+ ],
+ [
+ "▁Examination",
+ -13.091142654418945
+ ],
+ [
+ "▁règle",
+ -13.091208457946777
+ ],
+ [
+ "▁umgesetzt",
+ -13.09128475189209
+ ],
+ [
+ "211",
+ -13.091336250305176
+ ],
+ [
+ "▁Herald",
+ -13.091449737548828
+ ],
+ [
+ "Alex",
+ -13.091680526733398
+ ],
+ [
+ "▁drauf",
+ -13.091707229614258
+ ],
+ [
+ "logger",
+ -13.091714859008789
+ ],
+ [
+ "▁pictur",
+ -13.09186840057373
+ ],
+ [
+ "▁Divi",
+ -13.09196949005127
+ ],
+ [
+ "▁furnizat",
+ -13.092089653015137
+ ],
+ [
+ "▁verzichten",
+ -13.092132568359375
+ ],
+ [
+ "▁Sergi",
+ -13.092199325561523
+ ],
+ [
+ "contaminated",
+ -13.09223747253418
+ ],
+ [
+ "▁Buddy",
+ -13.092243194580078
+ ],
+ [
+ "▁chilled",
+ -13.092268943786621
+ ],
+ [
+ "▁vorlieg",
+ -13.092317581176758
+ ],
+ [
+ "▁Claudia",
+ -13.092632293701172
+ ],
+ [
+ "▁miserable",
+ -13.092653274536133
+ ],
+ [
+ "▁sketches",
+ -13.092683792114258
+ ],
+ [
+ "schicken",
+ -13.092814445495605
+ ],
+ [
+ "since",
+ -13.0928373336792
+ ],
+ [
+ "2.9",
+ -13.092840194702148
+ ],
+ [
+ "▁sitzen",
+ -13.092928886413574
+ ],
+ [
+ "ceapa",
+ -13.093396186828613
+ ],
+ [
+ "respectarea",
+ -13.093438148498535
+ ],
+ [
+ "▁handheld",
+ -13.093448638916016
+ ],
+ [
+ "popular",
+ -13.093527793884277
+ ],
+ [
+ "calming",
+ -13.093603134155273
+ ],
+ [
+ "Govern",
+ -13.093632698059082
+ ],
+ [
+ "▁omega",
+ -13.093645095825195
+ ],
+ [
+ "▁Planner",
+ -13.093791007995605
+ ],
+ [
+ "enriched",
+ -13.093850135803223
+ ],
+ [
+ "154",
+ -13.093976974487305
+ ],
+ [
+ "▁autorisé",
+ -13.093989372253418
+ ],
+ [
+ "▁cadouri",
+ -13.09407901763916
+ ],
+ [
+ "▁vulnerabilities",
+ -13.094143867492676
+ ],
+ [
+ "▁Arbeitnehmer",
+ -13.094158172607422
+ ],
+ [
+ "éditeur",
+ -13.094234466552734
+ ],
+ [
+ "▁Anleitung",
+ -13.094317436218262
+ ],
+ [
+ "rubbing",
+ -13.094343185424805
+ ],
+ [
+ "▁autovehicul",
+ -13.094621658325195
+ ],
+ [
+ "▁öffnen",
+ -13.094621658325195
+ ],
+ [
+ "▁Napoleon",
+ -13.094622611999512
+ ],
+ [
+ "▁cliché",
+ -13.094637870788574
+ ],
+ [
+ "▁Schaf",
+ -13.09469985961914
+ ],
+ [
+ "regulating",
+ -13.094894409179688
+ ],
+ [
+ "▁Kühl",
+ -13.09490966796875
+ ],
+ [
+ "▁blush",
+ -13.094913482666016
+ ],
+ [
+ "▁discard",
+ -13.094992637634277
+ ],
+ [
+ "▁confine",
+ -13.095027923583984
+ ],
+ [
+ "▁Rodriguez",
+ -13.09511947631836
+ ],
+ [
+ "▁ADHD",
+ -13.095165252685547
+ ],
+ [
+ "▁Madame",
+ -13.09516716003418
+ ],
+ [
+ "▁résolution",
+ -13.095319747924805
+ ],
+ [
+ "▁flair",
+ -13.095369338989258
+ ],
+ [
+ "▁claw",
+ -13.095422744750977
+ ],
+ [
+ "▁1929",
+ -13.095643043518066
+ ],
+ [
+ "ETH",
+ -13.095672607421875
+ ],
+ [
+ "nähe",
+ -13.095804214477539
+ ],
+ [
+ "▁soothe",
+ -13.0958251953125
+ ],
+ [
+ "4.9",
+ -13.095833778381348
+ ],
+ [
+ "montée",
+ -13.095925331115723
+ ],
+ [
+ "confirming",
+ -13.095989227294922
+ ],
+ [
+ "continent",
+ -13.09613037109375
+ ],
+ [
+ "reiz",
+ -13.09643840789795
+ ],
+ [
+ "john",
+ -13.096577644348145
+ ],
+ [
+ "IONAL",
+ -13.096588134765625
+ ],
+ [
+ "▁exported",
+ -13.0966215133667
+ ],
+ [
+ "▁Prison",
+ -13.096651077270508
+ ],
+ [
+ "possessed",
+ -13.096952438354492
+ ],
+ [
+ "▁placebo",
+ -13.096991539001465
+ ],
+ [
+ "▁biodiversity",
+ -13.097116470336914
+ ],
+ [
+ "▁combustion",
+ -13.097116470336914
+ ],
+ [
+ "▁Plumbing",
+ -13.09711742401123
+ ],
+ [
+ "ixie",
+ -13.097124099731445
+ ],
+ [
+ "▁repetition",
+ -13.09715461730957
+ ],
+ [
+ "▁soumis",
+ -13.097372055053711
+ ],
+ [
+ "▁reduc",
+ -13.097671508789062
+ ],
+ [
+ "▁constrain",
+ -13.097759246826172
+ ],
+ [
+ "Anti",
+ -13.097760200500488
+ ],
+ [
+ "consolidated",
+ -13.097817420959473
+ ],
+ [
+ "214",
+ -13.098095893859863
+ ],
+ [
+ "▁breaches",
+ -13.098108291625977
+ ],
+ [
+ "infringement",
+ -13.098115921020508
+ ],
+ [
+ "▁drizzle",
+ -13.098115921020508
+ ],
+ [
+ "▁erhöhen",
+ -13.098116874694824
+ ],
+ [
+ "▁Somerset",
+ -13.098118782043457
+ ],
+ [
+ "▁blonde",
+ -13.098132133483887
+ ],
+ [
+ "▁Funny",
+ -13.09813404083252
+ ],
+ [
+ "tuşi",
+ -13.098149299621582
+ ],
+ [
+ "▁reinvent",
+ -13.098162651062012
+ ],
+ [
+ "▁sérieux",
+ -13.098247528076172
+ ],
+ [
+ "▁croire",
+ -13.098308563232422
+ ],
+ [
+ "general",
+ -13.098315238952637
+ ],
+ [
+ "▁Distance",
+ -13.098319053649902
+ ],
+ [
+ "▁VoIP",
+ -13.098348617553711
+ ],
+ [
+ "▁adăugat",
+ -13.098406791687012
+ ],
+ [
+ "matik",
+ -13.098546028137207
+ ],
+ [
+ "▁avatar",
+ -13.098647117614746
+ ],
+ [
+ "▁superstar",
+ -13.098804473876953
+ ],
+ [
+ "8.0",
+ -13.098814010620117
+ ],
+ [
+ "lusieurs",
+ -13.098982810974121
+ ],
+ [
+ "▁Judeţean",
+ -13.099117279052734
+ ],
+ [
+ "offenen",
+ -13.099128723144531
+ ],
+ [
+ "RAF",
+ -13.099133491516113
+ ],
+ [
+ "▁restroom",
+ -13.099207878112793
+ ],
+ [
+ "enfance",
+ -13.099348068237305
+ ],
+ [
+ "▁garnish",
+ -13.099499702453613
+ ],
+ [
+ "▁vermittelt",
+ -13.099631309509277
+ ],
+ [
+ "Histoire",
+ -13.099634170532227
+ ],
+ [
+ "cyan",
+ -13.100628852844238
+ ],
+ [
+ "Talk",
+ -13.100666046142578
+ ],
+ [
+ "▁Varianten",
+ -13.10069465637207
+ ],
+ [
+ "▁Lille",
+ -13.10085678100586
+ ],
+ [
+ "▁offenbar",
+ -13.10098934173584
+ ],
+ [
+ "▁rénovation",
+ -13.10112190246582
+ ],
+ [
+ "▁comentarii",
+ -13.101249694824219
+ ],
+ [
+ "▁Bedford",
+ -13.10130500793457
+ ],
+ [
+ "▁cercetări",
+ -13.101325988769531
+ ],
+ [
+ "▁précision",
+ -13.101337432861328
+ ],
+ [
+ "MRC",
+ -13.101358413696289
+ ],
+ [
+ "alterations",
+ -13.101476669311523
+ ],
+ [
+ "▁discours",
+ -13.101531028747559
+ ],
+ [
+ "äger",
+ -13.101577758789062
+ ],
+ [
+ "▁antreprenor",
+ -13.101622581481934
+ ],
+ [
+ "▁Oriental",
+ -13.101849555969238
+ ],
+ [
+ "conducerea",
+ -13.101868629455566
+ ],
+ [
+ "CBC",
+ -13.101932525634766
+ ],
+ [
+ "▁mince",
+ -13.101985931396484
+ ],
+ [
+ "▁presidency",
+ -13.10212516784668
+ ],
+ [
+ "▁lipstick",
+ -13.102167129516602
+ ],
+ [
+ "▁SERVICES",
+ -13.102237701416016
+ ],
+ [
+ "productive",
+ -13.10237979888916
+ ],
+ [
+ "Assad",
+ -13.102400779724121
+ ],
+ [
+ "▁efectiv",
+ -13.102540969848633
+ ],
+ [
+ "▁gestern",
+ -13.102596282958984
+ ],
+ [
+ "▁RGB",
+ -13.102606773376465
+ ],
+ [
+ "▁Transilvania",
+ -13.102627754211426
+ ],
+ [
+ "▁Raleigh",
+ -13.102670669555664
+ ],
+ [
+ "DOM",
+ -13.102702140808105
+ ],
+ [
+ "▁iesit",
+ -13.102806091308594
+ ],
+ [
+ "▁anuntat",
+ -13.102810859680176
+ ],
+ [
+ "▁automatiquement",
+ -13.102901458740234
+ ],
+ [
+ "▁proliferation",
+ -13.103130340576172
+ ],
+ [
+ "▁Maroc",
+ -13.103156089782715
+ ],
+ [
+ "▁prezenţ",
+ -13.10323429107666
+ ],
+ [
+ "▁Filipino",
+ -13.103296279907227
+ ],
+ [
+ "▁Traian",
+ -13.103351593017578
+ ],
+ [
+ "▁swimmer",
+ -13.10356616973877
+ ],
+ [
+ "▁Slovenia",
+ -13.103632926940918
+ ],
+ [
+ "phobia",
+ -13.103724479675293
+ ],
+ [
+ "curricular",
+ -13.103734016418457
+ ],
+ [
+ "jurnal",
+ -13.103825569152832
+ ],
+ [
+ "▁vorne",
+ -13.103870391845703
+ ],
+ [
+ "▁asuma",
+ -13.103875160217285
+ ],
+ [
+ "defended",
+ -13.104104995727539
+ ],
+ [
+ "▁imminent",
+ -13.104140281677246
+ ],
+ [
+ "favored",
+ -13.10417366027832
+ ],
+ [
+ "▁innovator",
+ -13.104179382324219
+ ],
+ [
+ "▁Salzburg",
+ -13.104289054870605
+ ],
+ [
+ "5.4",
+ -13.104452133178711
+ ],
+ [
+ "Safe",
+ -13.104597091674805
+ ],
+ [
+ "▁inteleg",
+ -13.104744911193848
+ ],
+ [
+ "▁charisma",
+ -13.104781150817871
+ ],
+ [
+ "nature",
+ -13.104784965515137
+ ],
+ [
+ "4.8",
+ -13.104942321777344
+ ],
+ [
+ "argues",
+ -13.105104446411133
+ ],
+ [
+ "▁dimensiune",
+ -13.105142593383789
+ ],
+ [
+ "▁subdivision",
+ -13.105142593383789
+ ],
+ [
+ "▁embarrassing",
+ -13.105144500732422
+ ],
+ [
+ "▁confuse",
+ -13.105207443237305
+ ],
+ [
+ "DIC",
+ -13.105460166931152
+ ],
+ [
+ "rubrique",
+ -13.10549545288086
+ ],
+ [
+ "dépendance",
+ -13.105598449707031
+ ],
+ [
+ "INCLUD",
+ -13.10565185546875
+ ],
+ [
+ "▁Griffin",
+ -13.10574722290039
+ ],
+ [
+ "157",
+ -13.105751037597656
+ ],
+ [
+ "▁revamp",
+ -13.105839729309082
+ ],
+ [
+ "▁umgehen",
+ -13.10595989227295
+ ],
+ [
+ "▁mențin",
+ -13.106231689453125
+ ],
+ [
+ "▁1937",
+ -13.106695175170898
+ ],
+ [
+ "eklagte",
+ -13.106766700744629
+ ],
+ [
+ "▁clientèle",
+ -13.106801986694336
+ ],
+ [
+ "▁campsite",
+ -13.10708999633789
+ ],
+ [
+ "▁florist",
+ -13.107144355773926
+ ],
+ [
+ "▁Ferguson",
+ -13.107159614562988
+ ],
+ [
+ "▁demolition",
+ -13.107160568237305
+ ],
+ [
+ "▁McCain",
+ -13.107254981994629
+ ],
+ [
+ "▁reckon",
+ -13.10733413696289
+ ],
+ [
+ "striped",
+ -13.107414245605469
+ ],
+ [
+ "▁sonore",
+ -13.107481002807617
+ ],
+ [
+ "migrated",
+ -13.107548713684082
+ ],
+ [
+ "▁fluorescent",
+ -13.107664108276367
+ ],
+ [
+ "▁Colegi",
+ -13.107762336730957
+ ],
+ [
+ "ianu",
+ -13.107860565185547
+ ],
+ [
+ "cruising",
+ -13.107882499694824
+ ],
+ [
+ "LINK",
+ -13.107965469360352
+ ],
+ [
+ "▁Cutting",
+ -13.108001708984375
+ ],
+ [
+ "ABILITY",
+ -13.108168601989746
+ ],
+ [
+ "▁Categories",
+ -13.108168601989746
+ ],
+ [
+ "▁erhoben",
+ -13.108168601989746
+ ],
+ [
+ "▁Cocktail",
+ -13.108169555664062
+ ],
+ [
+ "▁Generator",
+ -13.108177185058594
+ ],
+ [
+ "▁gesucht",
+ -13.108186721801758
+ ],
+ [
+ "▁telescope",
+ -13.10818862915039
+ ],
+ [
+ "KET",
+ -13.108192443847656
+ ],
+ [
+ "▁hilfreich",
+ -13.108192443847656
+ ],
+ [
+ "▁beneficiary",
+ -13.108585357666016
+ ],
+ [
+ "▁Winston",
+ -13.108636856079102
+ ],
+ [
+ "Auswirkungen",
+ -13.108675956726074
+ ],
+ [
+ "portrayed",
+ -13.108705520629883
+ ],
+ [
+ "▁Aspekte",
+ -13.108743667602539
+ ],
+ [
+ "ffected",
+ -13.108901023864746
+ ],
+ [
+ "eutic",
+ -13.108905792236328
+ ],
+ [
+ "International",
+ -13.109021186828613
+ ],
+ [
+ "attente",
+ -13.109078407287598
+ ],
+ [
+ "mentioning",
+ -13.109119415283203
+ ],
+ [
+ "launch",
+ -13.109129905700684
+ ],
+ [
+ "▁EURO",
+ -13.109152793884277
+ ],
+ [
+ "▁Fraser",
+ -13.109344482421875
+ ],
+ [
+ "▁Johannes",
+ -13.109408378601074
+ ],
+ [
+ "▁felicit",
+ -13.109477043151855
+ ],
+ [
+ "▁plâng",
+ -13.109522819519043
+ ],
+ [
+ "izant",
+ -13.10971736907959
+ ],
+ [
+ "▁reţe",
+ -13.109846115112305
+ ],
+ [
+ "Mech",
+ -13.109954833984375
+ ],
+ [
+ "▁algebra",
+ -13.110193252563477
+ ],
+ [
+ "▁surgeries",
+ -13.110257148742676
+ ],
+ [
+ "▁semifinal",
+ -13.110262870788574
+ ],
+ [
+ "▁intimidating",
+ -13.110288619995117
+ ],
+ [
+ "▁exkl",
+ -13.110604286193848
+ ],
+ [
+ "asigurarea",
+ -13.110918998718262
+ ],
+ [
+ "Tek",
+ -13.111136436462402
+ ],
+ [
+ "▁Einladung",
+ -13.111205101013184
+ ],
+ [
+ "▁similaire",
+ -13.111205101013184
+ ],
+ [
+ "▁bebelus",
+ -13.111221313476562
+ ],
+ [
+ "▁déclin",
+ -13.111400604248047
+ ],
+ [
+ "▁Console",
+ -13.111495018005371
+ ],
+ [
+ "RET",
+ -13.111573219299316
+ ],
+ [
+ "appli",
+ -13.111586570739746
+ ],
+ [
+ "45%",
+ -13.111663818359375
+ ],
+ [
+ "Evenimentul",
+ -13.111811637878418
+ ],
+ [
+ "sincerely",
+ -13.111812591552734
+ ],
+ [
+ "sammlung",
+ -13.112098693847656
+ ],
+ [
+ "Amérique",
+ -13.112220764160156
+ ],
+ [
+ "▁1919",
+ -13.112326622009277
+ ],
+ [
+ "regulation",
+ -13.112367630004883
+ ],
+ [
+ "gebäude",
+ -13.112726211547852
+ ],
+ [
+ "▁Perspektive",
+ -13.112726211547852
+ ],
+ [
+ "Espagne",
+ -13.112744331359863
+ ],
+ [
+ "▁Underground",
+ -13.11283016204834
+ ],
+ [
+ "secret",
+ -13.112833976745605
+ ],
+ [
+ "▁Aussicht",
+ -13.112874031066895
+ ],
+ [
+ "Photo",
+ -13.112977027893066
+ ],
+ [
+ "▁Brust",
+ -13.113144874572754
+ ],
+ [
+ "▁Sustainability",
+ -13.11323356628418
+ ],
+ [
+ "▁clădiri",
+ -13.11323356628418
+ ],
+ [
+ "▁librarian",
+ -13.11323356628418
+ ],
+ [
+ "▁HBO",
+ -13.113235473632812
+ ],
+ [
+ "▁Parallel",
+ -13.113240242004395
+ ],
+ [
+ "▁shimmer",
+ -13.113283157348633
+ ],
+ [
+ "▁schlicht",
+ -13.113292694091797
+ ],
+ [
+ "▁anticipat",
+ -13.113311767578125
+ ],
+ [
+ "▁foolish",
+ -13.11335563659668
+ ],
+ [
+ "▁Ability",
+ -13.11347484588623
+ ],
+ [
+ "▁ceremoni",
+ -13.11358642578125
+ ],
+ [
+ "▁Ablauf",
+ -13.11359977722168
+ ],
+ [
+ "icrobial",
+ -13.113606452941895
+ ],
+ [
+ "▁actiuni",
+ -13.11362361907959
+ ],
+ [
+ "▁Wilhelm",
+ -13.113761901855469
+ ],
+ [
+ "▁nennen",
+ -13.113775253295898
+ ],
+ [
+ "▁botez",
+ -13.113832473754883
+ ],
+ [
+ "Alpes",
+ -13.113912582397461
+ ],
+ [
+ "▁libér",
+ -13.11392593383789
+ ],
+ [
+ "▁sneakers",
+ -13.114052772521973
+ ],
+ [
+ "geschafft",
+ -13.114252090454102
+ ],
+ [
+ "▁downstairs",
+ -13.114261627197266
+ ],
+ [
+ "▁wrench",
+ -13.114294052124023
+ ],
+ [
+ "▁erheblich",
+ -13.11442756652832
+ ],
+ [
+ "▁alimentar",
+ -13.114710807800293
+ ],
+ [
+ "▁suger",
+ -13.11474323272705
+ ],
+ [
+ "analysis",
+ -13.114883422851562
+ ],
+ [
+ "öhn",
+ -13.114891052246094
+ ],
+ [
+ "▁Nantes",
+ -13.114895820617676
+ ],
+ [
+ "▁Arbor",
+ -13.114899635314941
+ ],
+ [
+ "ooze",
+ -13.115150451660156
+ ],
+ [
+ "▁facade",
+ -13.115229606628418
+ ],
+ [
+ "▁MySQL",
+ -13.115266799926758
+ ],
+ [
+ "▁Salvador",
+ -13.115266799926758
+ ],
+ [
+ "▁Schlafzimmer",
+ -13.115279197692871
+ ],
+ [
+ "▁autentic",
+ -13.115320205688477
+ ],
+ [
+ "▁prezint",
+ -13.115348815917969
+ ],
+ [
+ "▁campground",
+ -13.115397453308105
+ ],
+ [
+ "Query",
+ -13.11540412902832
+ ],
+ [
+ "bekannt",
+ -13.115598678588867
+ ],
+ [
+ "arcinia",
+ -13.115632057189941
+ ],
+ [
+ "▁stunt",
+ -13.115825653076172
+ ],
+ [
+ "▁informare",
+ -13.115830421447754
+ ],
+ [
+ "▁interzis",
+ -13.11584186553955
+ ],
+ [
+ "▁Burke",
+ -13.115995407104492
+ ],
+ [
+ "certified",
+ -13.11601734161377
+ ],
+ [
+ "▁clove",
+ -13.11605167388916
+ ],
+ [
+ "java",
+ -13.116271018981934
+ ],
+ [
+ "▁Vielfalt",
+ -13.116284370422363
+ ],
+ [
+ "gebung",
+ -13.116329193115234
+ ],
+ [
+ "▁9/11",
+ -13.116497993469238
+ ],
+ [
+ "▁disruptive",
+ -13.11650562286377
+ ],
+ [
+ "visual",
+ -13.116693496704102
+ ],
+ [
+ "▁anunţat",
+ -13.11679458618164
+ ],
+ [
+ "▁Plätze",
+ -13.116799354553223
+ ],
+ [
+ "▁reduceri",
+ -13.116920471191406
+ ],
+ [
+ "autorisation",
+ -13.116950035095215
+ ],
+ [
+ "▁ligament",
+ -13.11705207824707
+ ],
+ [
+ "▁învăța",
+ -13.117081642150879
+ ],
+ [
+ "läufig",
+ -13.117303848266602
+ ],
+ [
+ "▁Copenhagen",
+ -13.117303848266602
+ ],
+ [
+ "▁commodities",
+ -13.117303848266602
+ ],
+ [
+ "▁eindeutig",
+ -13.117313385009766
+ ],
+ [
+ "▁catheter",
+ -13.117321014404297
+ ],
+ [
+ "erklärung",
+ -13.117720603942871
+ ],
+ [
+ "▁intelectual",
+ -13.117814064025879
+ ],
+ [
+ "▁municipality",
+ -13.117891311645508
+ ],
+ [
+ "▁1936",
+ -13.11798095703125
+ ],
+ [
+ "rruption",
+ -13.118217468261719
+ ],
+ [
+ "▁Lafayette",
+ -13.118324279785156
+ ],
+ [
+ "▁berühmte",
+ -13.118324279785156
+ ],
+ [
+ "▁idylli",
+ -13.118325233459473
+ ],
+ [
+ "▁caldura",
+ -13.118447303771973
+ ],
+ [
+ "▁tablette",
+ -13.118535995483398
+ ],
+ [
+ "▁liquidity",
+ -13.118728637695312
+ ],
+ [
+ "NGOs",
+ -13.118885040283203
+ ],
+ [
+ "▁supliment",
+ -13.11889934539795
+ ],
+ [
+ "contact",
+ -13.119075775146484
+ ],
+ [
+ "lustig",
+ -13.119219779968262
+ ],
+ [
+ "▁watercolor",
+ -13.119319915771484
+ ],
+ [
+ "▁Tiffany",
+ -13.119344711303711
+ ],
+ [
+ "▁Glauben",
+ -13.119365692138672
+ ],
+ [
+ "Immobilie",
+ -13.119406700134277
+ ],
+ [
+ "▁stripped",
+ -13.119549751281738
+ ],
+ [
+ "▁Beatles",
+ -13.119601249694824
+ ],
+ [
+ "ани",
+ -13.119770050048828
+ ],
+ [
+ "▁lifespan",
+ -13.119986534118652
+ ],
+ [
+ "▁profondeur",
+ -13.120251655578613
+ ],
+ [
+ "▁durere",
+ -13.120329856872559
+ ],
+ [
+ "▁Lithuania",
+ -13.120367050170898
+ ],
+ [
+ "▁resurrection",
+ -13.120367050170898
+ ],
+ [
+ "▁suitcase",
+ -13.120535850524902
+ ],
+ [
+ "▁Plumber",
+ -13.120545387268066
+ ],
+ [
+ "criticized",
+ -13.120595932006836
+ ],
+ [
+ "feared",
+ -13.120756149291992
+ ],
+ [
+ "▁Aunt",
+ -13.120929718017578
+ ],
+ [
+ "otwithstanding",
+ -13.121068000793457
+ ],
+ [
+ "verständlich",
+ -13.12115478515625
+ ],
+ [
+ "fiber",
+ -13.121248245239258
+ ],
+ [
+ "headquartered",
+ -13.121390342712402
+ ],
+ [
+ "▁Perspective",
+ -13.121391296386719
+ ],
+ [
+ "▁semantic",
+ -13.121413230895996
+ ],
+ [
+ "VIEW",
+ -13.121431350708008
+ ],
+ [
+ "▁Ersatzteile",
+ -13.121567726135254
+ ],
+ [
+ "▁disgust",
+ -13.121685981750488
+ ],
+ [
+ "rrington",
+ -13.121834754943848
+ ],
+ [
+ "ässe",
+ -13.121922492980957
+ ],
+ [
+ "▁anerkannt",
+ -13.121956825256348
+ ],
+ [
+ "meaning",
+ -13.12203598022461
+ ],
+ [
+ "178",
+ -13.122039794921875
+ ],
+ [
+ "▁grupuri",
+ -13.1221284866333
+ ],
+ [
+ "ciones",
+ -13.122267723083496
+ ],
+ [
+ "▁Mobility",
+ -13.122414588928223
+ ],
+ [
+ "▁unstable",
+ -13.122422218322754
+ ],
+ [
+ "▁FULL",
+ -13.122456550598145
+ ],
+ [
+ "austausch",
+ -13.122491836547852
+ ],
+ [
+ "▁culminat",
+ -13.122549057006836
+ ],
+ [
+ "▁Roast",
+ -13.122742652893066
+ ],
+ [
+ "existant",
+ -13.122940063476562
+ ],
+ [
+ "167",
+ -13.123008728027344
+ ],
+ [
+ "tinerii",
+ -13.123040199279785
+ ],
+ [
+ "September",
+ -13.123115539550781
+ ],
+ [
+ "▁haircut",
+ -13.123274803161621
+ ],
+ [
+ "▁Tutorial",
+ -13.123440742492676
+ ],
+ [
+ "▁enquiries",
+ -13.123440742492676
+ ],
+ [
+ "▁livelihood",
+ -13.123440742492676
+ ],
+ [
+ "▁proficiency",
+ -13.123440742492676
+ ],
+ [
+ "▁pavement",
+ -13.123443603515625
+ ],
+ [
+ "▁Reservation",
+ -13.123445510864258
+ ],
+ [
+ "aimerai",
+ -13.123491287231445
+ ],
+ [
+ "▁laboratoire",
+ -13.123492240905762
+ ],
+ [
+ "leihen",
+ -13.123501777648926
+ ],
+ [
+ "ministerium",
+ -13.123518943786621
+ ],
+ [
+ "▁Concentr",
+ -13.12366008758545
+ ],
+ [
+ "▁swipe",
+ -13.12368106842041
+ ],
+ [
+ "extrêmement",
+ -13.123687744140625
+ ],
+ [
+ "cultivated",
+ -13.123708724975586
+ ],
+ [
+ "▁Converse",
+ -13.123845100402832
+ ],
+ [
+ "▁paycheck",
+ -13.123863220214844
+ ],
+ [
+ "olltest",
+ -13.123995780944824
+ ],
+ [
+ "▁Bauch",
+ -13.124022483825684
+ ],
+ [
+ "▁autobuz",
+ -13.124067306518555
+ ],
+ [
+ "attack",
+ -13.124094009399414
+ ],
+ [
+ "While",
+ -13.124311447143555
+ ],
+ [
+ "Retrouvez",
+ -13.124320983886719
+ ],
+ [
+ "▁Dolphin",
+ -13.124466896057129
+ ],
+ [
+ "▁Shelby",
+ -13.124480247497559
+ ],
+ [
+ "▁Diagnostic",
+ -13.124486923217773
+ ],
+ [
+ "▁reconcil",
+ -13.124558448791504
+ ],
+ [
+ "▁Iaşi",
+ -13.124733924865723
+ ],
+ [
+ "▁iubesc",
+ -13.124979972839355
+ ],
+ [
+ "▁Bestseller",
+ -13.124985694885254
+ ],
+ [
+ "▁antrenor",
+ -13.125035285949707
+ ],
+ [
+ "▁Imaging",
+ -13.125089645385742
+ ],
+ [
+ "▁priorité",
+ -13.125295639038086
+ ],
+ [
+ "▁brewery",
+ -13.125494003295898
+ ],
+ [
+ "▁residual",
+ -13.125494003295898
+ ],
+ [
+ "▁intermittent",
+ -13.125494956970215
+ ],
+ [
+ "Kollekt",
+ -13.125585556030273
+ ],
+ [
+ "▁Walsh",
+ -13.12558650970459
+ ],
+ [
+ "▁marvelous",
+ -13.125653266906738
+ ],
+ [
+ "canceled",
+ -13.125686645507812
+ ],
+ [
+ "174",
+ -13.125761985778809
+ ],
+ [
+ "normes",
+ -13.125837326049805
+ ],
+ [
+ "▁Tempo",
+ -13.125996589660645
+ ],
+ [
+ "▁Târgu",
+ -13.126008987426758
+ ],
+ [
+ "877",
+ -13.126165390014648
+ ],
+ [
+ "5-8",
+ -13.126190185546875
+ ],
+ [
+ "960",
+ -13.126486778259277
+ ],
+ [
+ "▁Scandinavia",
+ -13.1265230178833
+ ],
+ [
+ "▁prolific",
+ -13.126526832580566
+ ],
+ [
+ "lasi",
+ -13.126916885375977
+ ],
+ [
+ "glück",
+ -13.127097129821777
+ ],
+ [
+ "▁immersion",
+ -13.127204895019531
+ ],
+ [
+ "RSA",
+ -13.127323150634766
+ ],
+ [
+ "▁Polk",
+ -13.127340316772461
+ ],
+ [
+ "▁transmitter",
+ -13.12747859954834
+ ],
+ [
+ "▁Kleidung",
+ -13.12755298614502
+ ],
+ [
+ "▁Cosmo",
+ -13.127676963806152
+ ],
+ [
+ "▁1935",
+ -13.127788543701172
+ ],
+ [
+ "höhere",
+ -13.127906799316406
+ ],
+ [
+ "▁Tatsache",
+ -13.128074645996094
+ ],
+ [
+ "▁Outlet",
+ -13.1282377243042
+ ],
+ [
+ "▁canalisation",
+ -13.12824821472168
+ ],
+ [
+ "Mbps",
+ -13.128433227539062
+ ],
+ [
+ "▁skeptical",
+ -13.128582954406738
+ ],
+ [
+ "mplification",
+ -13.128617286682129
+ ],
+ [
+ "▁Advice",
+ -13.128618240356445
+ ],
+ [
+ "▁détaillé",
+ -13.128676414489746
+ ],
+ [
+ "660",
+ -13.128701210021973
+ ],
+ [
+ "▁eyebrow",
+ -13.128722190856934
+ ],
+ [
+ "▁HIGH",
+ -13.128898620605469
+ ],
+ [
+ "hnlich",
+ -13.129073143005371
+ ],
+ [
+ "▁depăș",
+ -13.12910270690918
+ ],
+ [
+ "▁procurori",
+ -13.129140853881836
+ ],
+ [
+ "▁refrain",
+ -13.129212379455566
+ ],
+ [
+ "▁geschaffen",
+ -13.12952995300293
+ ],
+ [
+ "justement",
+ -13.129663467407227
+ ],
+ [
+ "exposing",
+ -13.129700660705566
+ ],
+ [
+ "243",
+ -13.1298828125
+ ],
+ [
+ "sectorul",
+ -13.130104064941406
+ ],
+ [
+ "▁courrier",
+ -13.130180358886719
+ ],
+ [
+ "▁carcas",
+ -13.130199432373047
+ ],
+ [
+ "sitter",
+ -13.13022518157959
+ ],
+ [
+ "▁Schreiben",
+ -13.130335807800293
+ ],
+ [
+ "▁malfunction",
+ -13.130358695983887
+ ],
+ [
+ "poartă",
+ -13.130522727966309
+ ],
+ [
+ "raisons",
+ -13.130565643310547
+ ],
+ [
+ "▁HOT",
+ -13.130650520324707
+ ],
+ [
+ "▁refreshed",
+ -13.130730628967285
+ ],
+ [
+ "mânt",
+ -13.130744934082031
+ ],
+ [
+ "▁coefficient",
+ -13.13097858428955
+ ],
+ [
+ "▁instituţii",
+ -13.131194114685059
+ ],
+ [
+ "▁sanguin",
+ -13.131202697753906
+ ],
+ [
+ "▁ceci",
+ -13.131213188171387
+ ],
+ [
+ "▁garçon",
+ -13.131232261657715
+ ],
+ [
+ "deluxe",
+ -13.131237030029297
+ ],
+ [
+ "▁rectif",
+ -13.131311416625977
+ ],
+ [
+ "920",
+ -13.131364822387695
+ ],
+ [
+ "Exista",
+ -13.131428718566895
+ ],
+ [
+ "▁magnif",
+ -13.131568908691406
+ ],
+ [
+ "efficiencies",
+ -13.131681442260742
+ ],
+ [
+ "▁Mitsubishi",
+ -13.131681442260742
+ ],
+ [
+ "▁consortium",
+ -13.131681442260742
+ ],
+ [
+ "▁baggage",
+ -13.131683349609375
+ ],
+ [
+ "▁guild",
+ -13.131736755371094
+ ],
+ [
+ "▁sixty",
+ -13.13193130493164
+ ],
+ [
+ "▁Retreat",
+ -13.13245677947998
+ ],
+ [
+ "batting",
+ -13.132473945617676
+ ],
+ [
+ "470",
+ -13.132708549499512
+ ],
+ [
+ "▁Britanie",
+ -13.132718086242676
+ ],
+ [
+ "displaced",
+ -13.132734298706055
+ ],
+ [
+ "▁spați",
+ -13.132794380187988
+ ],
+ [
+ "▁exceptionnelle",
+ -13.13281536102295
+ ],
+ [
+ "▁authorize",
+ -13.132906913757324
+ ],
+ [
+ "▁prescribe",
+ -13.133187294006348
+ ],
+ [
+ "▁dépannage",
+ -13.133234024047852
+ ],
+ [
+ "▁sexuelle",
+ -13.133234024047852
+ ],
+ [
+ "valid",
+ -13.133275032043457
+ ],
+ [
+ "▁hymn",
+ -13.133752822875977
+ ],
+ [
+ "▁histories",
+ -13.133757591247559
+ ],
+ [
+ "▁oriunde",
+ -13.133764266967773
+ ],
+ [
+ "Pop",
+ -13.133785247802734
+ ],
+ [
+ "▁dispoziţi",
+ -13.133800506591797
+ ],
+ [
+ "ADI",
+ -13.133819580078125
+ ],
+ [
+ "Google",
+ -13.133830070495605
+ ],
+ [
+ "▁Autism",
+ -13.133918762207031
+ ],
+ [
+ "▁aggr",
+ -13.134354591369629
+ ],
+ [
+ "bleed",
+ -13.134618759155273
+ ],
+ [
+ "▁displacement",
+ -13.13478946685791
+ ],
+ [
+ "▁hobbies",
+ -13.13478946685791
+ ],
+ [
+ "▁anatomy",
+ -13.134799003601074
+ ],
+ [
+ "▁Klinik",
+ -13.134821891784668
+ ],
+ [
+ "▁CCTV",
+ -13.1348237991333
+ ],
+ [
+ "readable",
+ -13.134886741638184
+ ],
+ [
+ "ulph",
+ -13.134982109069824
+ ],
+ [
+ "metabol",
+ -13.135035514831543
+ ],
+ [
+ "▁rugăm",
+ -13.135037422180176
+ ],
+ [
+ "▁Scotia",
+ -13.135087013244629
+ ],
+ [
+ "▁Einheit",
+ -13.135211944580078
+ ],
+ [
+ "▁troupe",
+ -13.13581371307373
+ ],
+ [
+ "▁Practitioner",
+ -13.135828018188477
+ ],
+ [
+ "▁oarec",
+ -13.135909080505371
+ ],
+ [
+ "Appel",
+ -13.135998725891113
+ ],
+ [
+ "situația",
+ -13.136096000671387
+ ],
+ [
+ "▁Yemen",
+ -13.136353492736816
+ ],
+ [
+ "piping",
+ -13.136515617370605
+ ],
+ [
+ "blood",
+ -13.136772155761719
+ ],
+ [
+ "engraved",
+ -13.136866569519043
+ ],
+ [
+ "▁Cristina",
+ -13.136866569519043
+ ],
+ [
+ "▁inaccurate",
+ -13.136866569519043
+ ],
+ [
+ "savory",
+ -13.136878967285156
+ ],
+ [
+ "atism",
+ -13.136919021606445
+ ],
+ [
+ "▁dependency",
+ -13.137007713317871
+ ],
+ [
+ "▁assertion",
+ -13.137015342712402
+ ],
+ [
+ "▁intersect",
+ -13.137201309204102
+ ],
+ [
+ "DATA",
+ -13.137224197387695
+ ],
+ [
+ "▁britanic",
+ -13.1373872756958
+ ],
+ [
+ "▁sanitaire",
+ -13.137393951416016
+ ],
+ [
+ "▁PLUS",
+ -13.137436866760254
+ ],
+ [
+ "▁platter",
+ -13.137730598449707
+ ],
+ [
+ "▁reconsider",
+ -13.137802124023438
+ ],
+ [
+ "▁Swim",
+ -13.13786792755127
+ ],
+ [
+ "▁Scene",
+ -13.137896537780762
+ ],
+ [
+ "▁Reynolds",
+ -13.137907028198242
+ ],
+ [
+ "▁gesund",
+ -13.137922286987305
+ ],
+ [
+ "international",
+ -13.137959480285645
+ ],
+ [
+ "government",
+ -13.13804817199707
+ ],
+ [
+ "▁gemstone",
+ -13.138052940368652
+ ],
+ [
+ "▁reproductive",
+ -13.1381196975708
+ ],
+ [
+ "▁expressive",
+ -13.13820743560791
+ ],
+ [
+ "▁tranche",
+ -13.13842487335205
+ ],
+ [
+ "▁Niagara",
+ -13.138427734375
+ ],
+ [
+ "▁Studierende",
+ -13.138434410095215
+ ],
+ [
+ "▁crave",
+ -13.138607025146484
+ ],
+ [
+ "pathetic",
+ -13.138739585876465
+ ],
+ [
+ "▁1916",
+ -13.138858795166016
+ ],
+ [
+ "▁Thousand",
+ -13.138873100280762
+ ],
+ [
+ "uffed",
+ -13.138893127441406
+ ],
+ [
+ "▁Lancaster",
+ -13.138960838317871
+ ],
+ [
+ "▁revenge",
+ -13.138972282409668
+ ],
+ [
+ "▁melody",
+ -13.1389741897583
+ ],
+ [
+ "Suitable",
+ -13.138991355895996
+ ],
+ [
+ "▁beacon",
+ -13.139082908630371
+ ],
+ [
+ "▁MAY",
+ -13.139205932617188
+ ],
+ [
+ "livré",
+ -13.139216423034668
+ ],
+ [
+ "Virus",
+ -13.139391899108887
+ ],
+ [
+ "▁collaborator",
+ -13.139413833618164
+ ],
+ [
+ "produktion",
+ -13.139480590820312
+ ],
+ [
+ "▁iluminat",
+ -13.139593124389648
+ ],
+ [
+ "facets",
+ -13.13975715637207
+ ],
+ [
+ "▁expus",
+ -13.139784812927246
+ ],
+ [
+ "▁baptism",
+ -13.13999080657959
+ ],
+ [
+ "▁urgency",
+ -13.140016555786133
+ ],
+ [
+ "artery",
+ -13.14030647277832
+ ],
+ [
+ "▁eingeladen",
+ -13.14043140411377
+ ],
+ [
+ "▁entfernen",
+ -13.14051342010498
+ ],
+ [
+ "soaking",
+ -13.140555381774902
+ ],
+ [
+ "▁irré",
+ -13.140557289123535
+ ],
+ [
+ "▁purity",
+ -13.140700340270996
+ ],
+ [
+ "▁adăug",
+ -13.140731811523438
+ ],
+ [
+ "historischen",
+ -13.140777587890625
+ ],
+ [
+ "crezi",
+ -13.140793800354004
+ ],
+ [
+ "▁tarziu",
+ -13.141035079956055
+ ],
+ [
+ "▁Mozart",
+ -13.141040802001953
+ ],
+ [
+ "▁trimming",
+ -13.141056060791016
+ ],
+ [
+ "▁violat",
+ -13.141056060791016
+ ],
+ [
+ "▁Vermögen",
+ -13.14108943939209
+ ],
+ [
+ "▁Theorie",
+ -13.141114234924316
+ ],
+ [
+ "scheibe",
+ -13.14114761352539
+ ],
+ [
+ "Partidul",
+ -13.141324996948242
+ ],
+ [
+ "▁childcare",
+ -13.14133071899414
+ ],
+ [
+ "ajele",
+ -13.141345977783203
+ ],
+ [
+ "▁Punjab",
+ -13.141390800476074
+ ],
+ [
+ "6.3",
+ -13.14156436920166
+ ],
+ [
+ "▁recount",
+ -13.141571044921875
+ ],
+ [
+ "▁repel",
+ -13.141799926757812
+ ],
+ [
+ "vantage",
+ -13.1419095993042
+ ],
+ [
+ "6.4",
+ -13.141953468322754
+ ],
+ [
+ "▁comedian",
+ -13.142087936401367
+ ],
+ [
+ "▁snappe",
+ -13.142256736755371
+ ],
+ [
+ "PLE",
+ -13.142271041870117
+ ],
+ [
+ "▁rapper",
+ -13.142439842224121
+ ],
+ [
+ "▁Belfast",
+ -13.142657279968262
+ ],
+ [
+ "▁predictive",
+ -13.14271068572998
+ ],
+ [
+ "dépôt",
+ -13.1427583694458
+ ],
+ [
+ "flavored",
+ -13.142769813537598
+ ],
+ [
+ "chließlich",
+ -13.14293098449707
+ ],
+ [
+ "▁stump",
+ -13.142955780029297
+ ],
+ [
+ "▁lakh",
+ -13.142963409423828
+ ],
+ [
+ "3:30",
+ -13.143021583557129
+ ],
+ [
+ "▁cetățeni",
+ -13.1431245803833
+ ],
+ [
+ "▁Milliarden",
+ -13.143125534057617
+ ],
+ [
+ "Assurance",
+ -13.143128395080566
+ ],
+ [
+ "▁Marketplace",
+ -13.143329620361328
+ ],
+ [
+ "equipped",
+ -13.143423080444336
+ ],
+ [
+ "▁russe",
+ -13.143462181091309
+ ],
+ [
+ "Exactly",
+ -13.143651008605957
+ ],
+ [
+ "▁Venez",
+ -13.144125938415527
+ ],
+ [
+ "▁Pavilion",
+ -13.144171714782715
+ ],
+ [
+ "▁incontournable",
+ -13.144171714782715
+ ],
+ [
+ "▁slaughter",
+ -13.14417839050293
+ ],
+ [
+ "asteptam",
+ -13.144190788269043
+ ],
+ [
+ "▁Fighter",
+ -13.144196510314941
+ ],
+ [
+ "▁Landkreis",
+ -13.144278526306152
+ ],
+ [
+ "▁lumini",
+ -13.144312858581543
+ ],
+ [
+ "▁connaît",
+ -13.144615173339844
+ ],
+ [
+ "▁Breite",
+ -13.144674301147461
+ ],
+ [
+ "▁Disability",
+ -13.144774436950684
+ ],
+ [
+ "▁Alfa",
+ -13.144786834716797
+ ],
+ [
+ "▁poise",
+ -13.144895553588867
+ ],
+ [
+ "▁Alpen",
+ -13.144898414611816
+ ],
+ [
+ "betont",
+ -13.145031929016113
+ ],
+ [
+ "159",
+ -13.145161628723145
+ ],
+ [
+ "▁geprägt",
+ -13.145219802856445
+ ],
+ [
+ "▁intrigued",
+ -13.145219802856445
+ ],
+ [
+ "▁sympathy",
+ -13.145220756530762
+ ],
+ [
+ "societal",
+ -13.145225524902344
+ ],
+ [
+ "▁sédui",
+ -13.145243644714355
+ ],
+ [
+ "▁differentiation",
+ -13.145384788513184
+ ],
+ [
+ "▁aprobare",
+ -13.145744323730469
+ ],
+ [
+ "schirm",
+ -13.14585018157959
+ ],
+ [
+ "sagt",
+ -13.145956039428711
+ ],
+ [
+ "7.3",
+ -13.146101951599121
+ ],
+ [
+ "Bib",
+ -13.146263122558594
+ ],
+ [
+ "europäischen",
+ -13.146268844604492
+ ],
+ [
+ "▁Innovative",
+ -13.146268844604492
+ ],
+ [
+ "▁autonome",
+ -13.146330833435059
+ ],
+ [
+ "▁Objective",
+ -13.146400451660156
+ ],
+ [
+ "▁refusal",
+ -13.146551132202148
+ ],
+ [
+ "▁exposé",
+ -13.146719932556152
+ ],
+ [
+ "▁cetăţeni",
+ -13.146793365478516
+ ],
+ [
+ "▁stimmt",
+ -13.146798133850098
+ ],
+ [
+ "acordul",
+ -13.147162437438965
+ ],
+ [
+ "▁hormonal",
+ -13.147254943847656
+ ],
+ [
+ "intermédiaire",
+ -13.147319793701172
+ ],
+ [
+ "▁doubl",
+ -13.147374153137207
+ ],
+ [
+ "▁flute",
+ -13.147509574890137
+ ],
+ [
+ "▁Balkon",
+ -13.147523880004883
+ ],
+ [
+ "▁Florian",
+ -13.147607803344727
+ ],
+ [
+ "737",
+ -13.147614479064941
+ ],
+ [
+ "▁dritte",
+ -13.147639274597168
+ ],
+ [
+ "spitze",
+ -13.147685050964355
+ ],
+ [
+ "donnent",
+ -13.14778995513916
+ ],
+ [
+ "▁Zuhause",
+ -13.147850036621094
+ ],
+ [
+ "▁VIII",
+ -13.147852897644043
+ ],
+ [
+ "familien",
+ -13.148151397705078
+ ],
+ [
+ "▁sécurisé",
+ -13.148313522338867
+ ],
+ [
+ "▁glamour",
+ -13.148370742797852
+ ],
+ [
+ "▁societati",
+ -13.148370742797852
+ ],
+ [
+ "typique",
+ -13.1483793258667
+ ],
+ [
+ "▁addicted",
+ -13.148421287536621
+ ],
+ [
+ "▁Providence",
+ -13.148500442504883
+ ],
+ [
+ "▁Extended",
+ -13.148506164550781
+ ],
+ [
+ "▁Barbie",
+ -13.148513793945312
+ ],
+ [
+ "zustand",
+ -13.148516654968262
+ ],
+ [
+ "▁Sauna",
+ -13.148638725280762
+ ],
+ [
+ "▁propane",
+ -13.148663520812988
+ ],
+ [
+ "europa",
+ -13.148894309997559
+ ],
+ [
+ "glued",
+ -13.148940086364746
+ ],
+ [
+ "▁Mystery",
+ -13.148941993713379
+ ],
+ [
+ "▁travaillé",
+ -13.149106979370117
+ ],
+ [
+ "riol",
+ -13.149251937866211
+ ],
+ [
+ "fleisch",
+ -13.149288177490234
+ ],
+ [
+ "▁Eintritt",
+ -13.149327278137207
+ ],
+ [
+ "▁Syndrome",
+ -13.149422645568848
+ ],
+ [
+ "▁petroleum",
+ -13.149426460266113
+ ],
+ [
+ "▁genial",
+ -13.149433135986328
+ ],
+ [
+ "sponsored",
+ -13.149436950683594
+ ],
+ [
+ "▁Cindy",
+ -13.149436950683594
+ ],
+ [
+ "▁courier",
+ -13.149600982666016
+ ],
+ [
+ "▁Scrap",
+ -13.149640083312988
+ ],
+ [
+ "▁conţin",
+ -13.149724006652832
+ ],
+ [
+ "(2007)",
+ -13.149764060974121
+ ],
+ [
+ "▁gewährleisten",
+ -13.149949073791504
+ ],
+ [
+ "▁proprietor",
+ -13.15011215209961
+ ],
+ [
+ "▁cheque",
+ -13.15046215057373
+ ],
+ [
+ "maternity",
+ -13.150477409362793
+ ],
+ [
+ "▁Gustav",
+ -13.15048599243164
+ ],
+ [
+ "▁arterial",
+ -13.150497436523438
+ ],
+ [
+ "▁whiskey",
+ -13.150510787963867
+ ],
+ [
+ "▁concealed",
+ -13.150525093078613
+ ],
+ [
+ "thèque",
+ -13.150553703308105
+ ],
+ [
+ "felony",
+ -13.150579452514648
+ ],
+ [
+ "▁tweeted",
+ -13.150613784790039
+ ],
+ [
+ "OTA",
+ -13.150619506835938
+ ],
+ [
+ "nsel",
+ -13.150664329528809
+ ],
+ [
+ "▁coarse",
+ -13.150664329528809
+ ],
+ [
+ "▁identificat",
+ -13.150707244873047
+ ],
+ [
+ "▁variability",
+ -13.150716781616211
+ ],
+ [
+ "civ",
+ -13.150843620300293
+ ],
+ [
+ "▁drastic",
+ -13.150956153869629
+ ],
+ [
+ "▁hatred",
+ -13.151090621948242
+ ],
+ [
+ "▁Bürgermeister",
+ -13.151237487792969
+ ],
+ [
+ "▁utilizatorilor",
+ -13.15124225616455
+ ],
+ [
+ "OULD",
+ -13.15137004852295
+ ],
+ [
+ "rmaßen",
+ -13.151383399963379
+ ],
+ [
+ "▁windshield",
+ -13.151530265808105
+ ],
+ [
+ "▁Particular",
+ -13.151531219482422
+ ],
+ [
+ "▁Tunnel",
+ -13.151638984680176
+ ],
+ [
+ "▁litri",
+ -13.15164852142334
+ ],
+ [
+ "extrême",
+ -13.15180492401123
+ ],
+ [
+ "▁Schalt",
+ -13.151944160461426
+ ],
+ [
+ "paket",
+ -13.152159690856934
+ ],
+ [
+ "berlin",
+ -13.152169227600098
+ ],
+ [
+ "▁slujb",
+ -13.152193069458008
+ ],
+ [
+ "facilitated",
+ -13.152206420898438
+ ],
+ [
+ "Congressional",
+ -13.152510643005371
+ ],
+ [
+ "▁honeymoon",
+ -13.152585983276367
+ ],
+ [
+ "▁Provision",
+ -13.152697563171387
+ ],
+ [
+ "▁Outfit",
+ -13.152779579162598
+ ],
+ [
+ "udder",
+ -13.152814865112305
+ ],
+ [
+ "▁chandelier",
+ -13.153002738952637
+ ],
+ [
+ "donating",
+ -13.153132438659668
+ ],
+ [
+ "historic",
+ -13.15333080291748
+ ],
+ [
+ "organized",
+ -13.153508186340332
+ ],
+ [
+ "(8)",
+ -13.15356731414795
+ ],
+ [
+ "▁touristique",
+ -13.153610229492188
+ ],
+ [
+ "▁Roosevelt",
+ -13.153643608093262
+ ],
+ [
+ "▁Verständnis",
+ -13.153643608093262
+ ],
+ [
+ "▁prilej",
+ -13.153655052185059
+ ],
+ [
+ "Vanity",
+ -13.153806686401367
+ ],
+ [
+ "chilly",
+ -13.153964042663574
+ ],
+ [
+ "loyer",
+ -13.154031753540039
+ ],
+ [
+ "▁Zhang",
+ -13.154053688049316
+ ],
+ [
+ "▁Nouveau",
+ -13.154193878173828
+ ],
+ [
+ "Soft",
+ -13.154326438903809
+ ],
+ [
+ "▁motherboard",
+ -13.15441608428955
+ ],
+ [
+ "▁Erklärung",
+ -13.154701232910156
+ ],
+ [
+ "▁Tasmania",
+ -13.154702186584473
+ ],
+ [
+ "▁verändern",
+ -13.154703140258789
+ ],
+ [
+ "▁seldom",
+ -13.154711723327637
+ ],
+ [
+ "▁Karriere",
+ -13.154714584350586
+ ],
+ [
+ "▁Mixed",
+ -13.154902458190918
+ ],
+ [
+ "umfang",
+ -13.154970169067383
+ ],
+ [
+ "▁Strategies",
+ -13.155035972595215
+ ],
+ [
+ "CHAR",
+ -13.155051231384277
+ ],
+ [
+ "olitary",
+ -13.155075073242188
+ ],
+ [
+ "▁Persoan",
+ -13.1550874710083
+ ],
+ [
+ "bewegung",
+ -13.155242919921875
+ ],
+ [
+ "▁Ernest",
+ -13.155367851257324
+ ],
+ [
+ "withdrawn",
+ -13.155855178833008
+ ],
+ [
+ "▁stationary",
+ -13.155881881713867
+ ],
+ [
+ "▁bland",
+ -13.155939102172852
+ ],
+ [
+ "▁Replace",
+ -13.156059265136719
+ ],
+ [
+ "▁Londres",
+ -13.156290054321289
+ ],
+ [
+ "▁plural",
+ -13.156290054321289
+ ],
+ [
+ "▁concentrat",
+ -13.156515121459961
+ ],
+ [
+ "Maschine",
+ -13.156675338745117
+ ],
+ [
+ "▁Advocate",
+ -13.156820297241211
+ ],
+ [
+ "▁vermitteln",
+ -13.156824111938477
+ ],
+ [
+ "▁dispenser",
+ -13.156827926635742
+ ],
+ [
+ "▁tedious",
+ -13.15695858001709
+ ],
+ [
+ "▁Straight",
+ -13.15705394744873
+ ],
+ [
+ "▁Corona",
+ -13.157061576843262
+ ],
+ [
+ "▁monumental",
+ -13.157073020935059
+ ],
+ [
+ "▁migrate",
+ -13.15720272064209
+ ],
+ [
+ "▁verlieren",
+ -13.157366752624512
+ ],
+ [
+ "▁Lub",
+ -13.157482147216797
+ ],
+ [
+ "▁reinforcement",
+ -13.157827377319336
+ ],
+ [
+ "▁cherish",
+ -13.157843589782715
+ ],
+ [
+ "Veterinary",
+ -13.157881736755371
+ ],
+ [
+ "geschwindigkeit",
+ -13.157881736755371
+ ],
+ [
+ "▁féminin",
+ -13.157881736755371
+ ],
+ [
+ "▁Facilities",
+ -13.157964706420898
+ ],
+ [
+ "▁urmari",
+ -13.158050537109375
+ ],
+ [
+ "▁Vertical",
+ -13.158098220825195
+ ],
+ [
+ "echoe",
+ -13.158188819885254
+ ],
+ [
+ "toured",
+ -13.158548355102539
+ ],
+ [
+ "Served",
+ -13.158772468566895
+ ],
+ [
+ "más",
+ -13.158853530883789
+ ],
+ [
+ "license",
+ -13.158893585205078
+ ],
+ [
+ "misunderstanding",
+ -13.158944129943848
+ ],
+ [
+ "▁glamorous",
+ -13.158944129943848
+ ],
+ [
+ "BJP",
+ -13.158973693847656
+ ],
+ [
+ "▁découvert",
+ -13.159173965454102
+ ],
+ [
+ "schönsten",
+ -13.159517288208008
+ ],
+ [
+ "▁(2018)",
+ -13.159577369689941
+ ],
+ [
+ "▁orasului",
+ -13.159581184387207
+ ],
+ [
+ "328",
+ -13.159674644470215
+ ],
+ [
+ "thighs",
+ -13.159801483154297
+ ],
+ [
+ "éclairage",
+ -13.160008430480957
+ ],
+ [
+ "Oamenii",
+ -13.160009384155273
+ ],
+ [
+ "▁Transmission",
+ -13.16014575958252
+ ],
+ [
+ "▁transpir",
+ -13.16015911102295
+ ],
+ [
+ "▁președinte",
+ -13.160321235656738
+ ],
+ [
+ "finalists",
+ -13.160327911376953
+ ],
+ [
+ "genügend",
+ -13.160524368286133
+ ],
+ [
+ "▁Aufmerksamkeit",
+ -13.160539627075195
+ ],
+ [
+ "▁unglaublich",
+ -13.160539627075195
+ ],
+ [
+ "▁descarc",
+ -13.160604476928711
+ ],
+ [
+ "▁Couch",
+ -13.160683631896973
+ ],
+ [
+ "eaucoup",
+ -13.160788536071777
+ ],
+ [
+ "▁adidas",
+ -13.161075592041016
+ ],
+ [
+ "▁1-800-",
+ -13.161077499389648
+ ],
+ [
+ "▁Communities",
+ -13.161102294921875
+ ],
+ [
+ "▁Einkommen",
+ -13.161102294921875
+ ],
+ [
+ "▁Reagan",
+ -13.16114330291748
+ ],
+ [
+ "▁Stoke",
+ -13.161260604858398
+ ],
+ [
+ "▁Snapchat",
+ -13.161269187927246
+ ],
+ [
+ "éclat",
+ -13.161272048950195
+ ],
+ [
+ "▁auseinander",
+ -13.161367416381836
+ ],
+ [
+ "▁richesse",
+ -13.16137409210205
+ ],
+ [
+ "▁toggle",
+ -13.161396026611328
+ ],
+ [
+ "▁Zutaten",
+ -13.161606788635254
+ ],
+ [
+ "▁député",
+ -13.16161060333252
+ ],
+ [
+ "▁battlefield",
+ -13.161611557006836
+ ],
+ [
+ "▁spirituel",
+ -13.161611557006836
+ ],
+ [
+ "▁Shuttle",
+ -13.161632537841797
+ ],
+ [
+ "▁Aktien",
+ -13.161665916442871
+ ],
+ [
+ "hormon",
+ -13.161819458007812
+ ],
+ [
+ "connection",
+ -13.16187858581543
+ ],
+ [
+ "▁vizitatori",
+ -13.16191577911377
+ ],
+ [
+ "érité",
+ -13.161971092224121
+ ],
+ [
+ "truck",
+ -13.1619873046875
+ ],
+ [
+ "▁yourselves",
+ -13.162139892578125
+ ],
+ [
+ "▁Logistics",
+ -13.162140846252441
+ ],
+ [
+ "coveted",
+ -13.16215705871582
+ ],
+ [
+ "▁şedinţ",
+ -13.162671089172363
+ ],
+ [
+ "▁messenger",
+ -13.162703514099121
+ ],
+ [
+ "▁țar",
+ -13.162918090820312
+ ],
+ [
+ "▁Grau",
+ -13.163025856018066
+ ],
+ [
+ "chirurgie",
+ -13.163138389587402
+ ],
+ [
+ "▁Ressourcen",
+ -13.16320514678955
+ ],
+ [
+ "▁Jésus",
+ -13.163207054138184
+ ],
+ [
+ "▁acțiune",
+ -13.163208961486816
+ ],
+ [
+ "▁Bundesliga",
+ -13.163249015808105
+ ],
+ [
+ "Lizenz",
+ -13.163379669189453
+ ],
+ [
+ "ELLE",
+ -13.163908958435059
+ ],
+ [
+ "vraie",
+ -13.1639986038208
+ ],
+ [
+ "ruined",
+ -13.164018630981445
+ ],
+ [
+ "▁Marble",
+ -13.164109230041504
+ ],
+ [
+ "▁Zambia",
+ -13.164308547973633
+ ],
+ [
+ "▁Finnish",
+ -13.164366722106934
+ ],
+ [
+ "▁trackback",
+ -13.164488792419434
+ ],
+ [
+ "héros",
+ -13.16451644897461
+ ],
+ [
+ "▁réclam",
+ -13.164534568786621
+ ],
+ [
+ "locurile",
+ -13.164706230163574
+ ],
+ [
+ "tägliche",
+ -13.164753913879395
+ ],
+ [
+ "IFF",
+ -13.164824485778809
+ ],
+ [
+ "▁contextual",
+ -13.164938926696777
+ ],
+ [
+ "▁Elvis",
+ -13.165084838867188
+ ],
+ [
+ "▁Batch",
+ -13.165183067321777
+ ],
+ [
+ "▁appris",
+ -13.16519546508789
+ ],
+ [
+ "intensive",
+ -13.165404319763184
+ ],
+ [
+ "▁întâmplat",
+ -13.16565990447998
+ ],
+ [
+ "▁prelucr",
+ -13.16576099395752
+ ],
+ [
+ "flore",
+ -13.165873527526855
+ ],
+ [
+ "▁Alkohol",
+ -13.165877342224121
+ ],
+ [
+ "Konzern",
+ -13.165895462036133
+ ],
+ [
+ "Delete",
+ -13.166082382202148
+ ],
+ [
+ "öck",
+ -13.16612720489502
+ ],
+ [
+ "▁clientii",
+ -13.16614818572998
+ ],
+ [
+ "▁innovate",
+ -13.166224479675293
+ ],
+ [
+ "▁ASAP",
+ -13.166345596313477
+ ],
+ [
+ "crumbs",
+ -13.166425704956055
+ ],
+ [
+ "reusable",
+ -13.166489601135254
+ ],
+ [
+ "▁Beaver",
+ -13.166507720947266
+ ],
+ [
+ "▁rosii",
+ -13.166643142700195
+ ],
+ [
+ "Arr",
+ -13.166704177856445
+ ],
+ [
+ "▁Zubehör",
+ -13.166948318481445
+ ],
+ [
+ "▁stolz",
+ -13.166952133178711
+ ],
+ [
+ "▁$75",
+ -13.16695499420166
+ ],
+ [
+ "▁Frühling",
+ -13.166967391967773
+ ],
+ [
+ "▁disagreement",
+ -13.166988372802734
+ ],
+ [
+ "▁formulate",
+ -13.167381286621094
+ ],
+ [
+ "braking",
+ -13.167522430419922
+ ],
+ [
+ "▁submarine",
+ -13.167535781860352
+ ],
+ [
+ "▁identificare",
+ -13.167652130126953
+ ],
+ [
+ "lansarea",
+ -13.167659759521484
+ ],
+ [
+ "covered",
+ -13.167753219604492
+ ],
+ [
+ "benso",
+ -13.167859077453613
+ ],
+ [
+ "▁situatie",
+ -13.167989730834961
+ ],
+ [
+ "hilf",
+ -13.1681547164917
+ ],
+ [
+ "▁Southampton",
+ -13.168557167053223
+ ],
+ [
+ "▁intéressé",
+ -13.168557167053223
+ ],
+ [
+ "▁congressional",
+ -13.168572425842285
+ ],
+ [
+ "65%",
+ -13.168595314025879
+ ],
+ [
+ "▁Allison",
+ -13.168627738952637
+ ],
+ [
+ "Mainland",
+ -13.168726921081543
+ ],
+ [
+ "▁touchscreen",
+ -13.16882038116455
+ ],
+ [
+ "leitet",
+ -13.168922424316406
+ ],
+ [
+ "mnului",
+ -13.16958999633789
+ ],
+ [
+ "▁engagiert",
+ -13.169631004333496
+ ],
+ [
+ "joacă",
+ -13.16964340209961
+ ],
+ [
+ "▁$5,000",
+ -13.169652938842773
+ ],
+ [
+ "upscale",
+ -13.1697359085083
+ ],
+ [
+ "▁vérité",
+ -13.16983413696289
+ ],
+ [
+ "flüssig",
+ -13.170167922973633
+ ],
+ [
+ "Richtlinie",
+ -13.170169830322266
+ ],
+ [
+ "▁positif",
+ -13.170169830322266
+ ],
+ [
+ "▁diferenta",
+ -13.170175552368164
+ ],
+ [
+ "▁întâi",
+ -13.170707702636719
+ ],
+ [
+ "ethylene",
+ -13.170791625976562
+ ],
+ [
+ "kreuz",
+ -13.170913696289062
+ ],
+ [
+ "Surely",
+ -13.170990943908691
+ ],
+ [
+ "puneti",
+ -13.171002388000488
+ ],
+ [
+ "europe",
+ -13.171142578125
+ ],
+ [
+ "▁comunist",
+ -13.171271324157715
+ ],
+ [
+ "unterricht",
+ -13.171302795410156
+ ],
+ [
+ "▁Füll",
+ -13.171304702758789
+ ],
+ [
+ "▁Aberdeen",
+ -13.171792030334473
+ ],
+ [
+ "▁DSLR",
+ -13.171792030334473
+ ],
+ [
+ "▁functioneaza",
+ -13.171799659729004
+ ],
+ [
+ "▁benches",
+ -13.171807289123535
+ ],
+ [
+ "▁Alpine",
+ -13.171866416931152
+ ],
+ [
+ "phthal",
+ -13.172003746032715
+ ],
+ [
+ "▁counselling",
+ -13.17219066619873
+ ],
+ [
+ "▁erzielen",
+ -13.172323226928711
+ ],
+ [
+ "▁părinţi",
+ -13.172329902648926
+ ],
+ [
+ "▁besitzen",
+ -13.17236614227295
+ ],
+ [
+ "heavenly",
+ -13.172389030456543
+ ],
+ [
+ "▁masque",
+ -13.17281723022461
+ ],
+ [
+ "▁Legislature",
+ -13.172859191894531
+ ],
+ [
+ "▁Recycling",
+ -13.172861099243164
+ ],
+ [
+ "▁Derma",
+ -13.172883987426758
+ ],
+ [
+ "reunite",
+ -13.172926902770996
+ ],
+ [
+ "recettes",
+ -13.17310619354248
+ ],
+ [
+ "converge",
+ -13.173262596130371
+ ],
+ [
+ "▁compoziti",
+ -13.17327880859375
+ ],
+ [
+ "▁Nürnberg",
+ -13.173398971557617
+ ],
+ [
+ "760",
+ -13.173545837402344
+ ],
+ [
+ "▁entière",
+ -13.173674583435059
+ ],
+ [
+ "▁parchment",
+ -13.173944473266602
+ ],
+ [
+ "▁Aufwand",
+ -13.173945426940918
+ ],
+ [
+ "▁antivirus",
+ -13.174087524414062
+ ],
+ [
+ "▁remettr",
+ -13.17409610748291
+ ],
+ [
+ "▁NEVER",
+ -13.174243927001953
+ ],
+ [
+ "▁restrictive",
+ -13.174266815185547
+ ],
+ [
+ "▁beurre",
+ -13.174283027648926
+ ],
+ [
+ "▁frigider",
+ -13.174478530883789
+ ],
+ [
+ "acquisition",
+ -13.174642562866211
+ ],
+ [
+ "▁Correct",
+ -13.174866676330566
+ ],
+ [
+ "▁immortal",
+ -13.175017356872559
+ ],
+ [
+ "▁occupancy",
+ -13.175017356872559
+ ],
+ [
+ "▁Tucson",
+ -13.175019264221191
+ ],
+ [
+ "▁Dhabi",
+ -13.175025939941406
+ ],
+ [
+ "obligation",
+ -13.175033569335938
+ ],
+ [
+ "▁warfare",
+ -13.175037384033203
+ ],
+ [
+ "▁syntax",
+ -13.175045013427734
+ ],
+ [
+ "APS",
+ -13.175106048583984
+ ],
+ [
+ "мен",
+ -13.175209999084473
+ ],
+ [
+ "▁diferenț",
+ -13.175251960754395
+ ],
+ [
+ "wordpress",
+ -13.17549991607666
+ ],
+ [
+ "▁Wohnzimmer",
+ -13.175593376159668
+ ],
+ [
+ "oppo",
+ -13.175736427307129
+ ],
+ [
+ "▁miscare",
+ -13.175762176513672
+ ],
+ [
+ "companiilor",
+ -13.17581558227539
+ ],
+ [
+ "▁bezahlt",
+ -13.17584228515625
+ ],
+ [
+ "Sterne",
+ -13.175864219665527
+ ],
+ [
+ "inability",
+ -13.175898551940918
+ ],
+ [
+ "▁Hoffnung",
+ -13.176156044006348
+ ],
+ [
+ "▁românească",
+ -13.176176071166992
+ ],
+ [
+ "document",
+ -13.176177024841309
+ ],
+ [
+ "borrowers",
+ -13.17625904083252
+ ],
+ [
+ "▁rasa",
+ -13.176301956176758
+ ],
+ [
+ "▁bénéfice",
+ -13.176445960998535
+ ],
+ [
+ "▁Panda",
+ -13.17645263671875
+ ],
+ [
+ "▁cărţi",
+ -13.176730155944824
+ ],
+ [
+ "▁Vorgehen",
+ -13.17690658569336
+ ],
+ [
+ "▁afecteaz",
+ -13.176956176757812
+ ],
+ [
+ "▁diagnos",
+ -13.177050590515137
+ ],
+ [
+ "▁Dentistry",
+ -13.177180290222168
+ ],
+ [
+ "▁staggering",
+ -13.177180290222168
+ ],
+ [
+ "präsident",
+ -13.177181243896484
+ ],
+ [
+ "▁vocational",
+ -13.177239418029785
+ ],
+ [
+ "Combined",
+ -13.177287101745605
+ ],
+ [
+ "stère",
+ -13.177306175231934
+ ],
+ [
+ "▁frunze",
+ -13.177478790283203
+ ],
+ [
+ "OLI",
+ -13.177525520324707
+ ],
+ [
+ "▁răc",
+ -13.177752494812012
+ ],
+ [
+ "▁changé",
+ -13.177754402160645
+ ],
+ [
+ "▁reprezentanți",
+ -13.177757263183594
+ ],
+ [
+ "▁ausgeschlossen",
+ -13.177777290344238
+ ],
+ [
+ "Windows",
+ -13.177891731262207
+ ],
+ [
+ "sometimes",
+ -13.177898406982422
+ ],
+ [
+ "▁dargestellt",
+ -13.178120613098145
+ ],
+ [
+ "provoking",
+ -13.178263664245605
+ ],
+ [
+ "terribly",
+ -13.178264617919922
+ ],
+ [
+ "▁speculate",
+ -13.178274154663086
+ ],
+ [
+ "▁complément",
+ -13.178305625915527
+ ],
+ [
+ "▁(2006)",
+ -13.178306579589844
+ ],
+ [
+ "zulegen",
+ -13.178668022155762
+ ],
+ [
+ "▁définitive",
+ -13.178876876831055
+ ],
+ [
+ "considerare",
+ -13.17911148071289
+ ],
+ [
+ "▁Subaru",
+ -13.179354667663574
+ ],
+ [
+ "WAN",
+ -13.179390907287598
+ ],
+ [
+ "guessed",
+ -13.179417610168457
+ ],
+ [
+ "spannung",
+ -13.179479598999023
+ ],
+ [
+ "▁supernatural",
+ -13.179515838623047
+ ],
+ [
+ "▁Interstate",
+ -13.17957878112793
+ ],
+ [
+ "▁redundant",
+ -13.179891586303711
+ ],
+ [
+ "▁HUG",
+ -13.179893493652344
+ ],
+ [
+ "▁restauration",
+ -13.180006980895996
+ ],
+ [
+ "repute",
+ -13.180011749267578
+ ],
+ [
+ "coagul",
+ -13.180028915405273
+ ],
+ [
+ "tehnologia",
+ -13.18043327331543
+ ],
+ [
+ "warded",
+ -13.180444717407227
+ ],
+ [
+ "▁lobster",
+ -13.180469512939453
+ ],
+ [
+ "▁Hafen",
+ -13.180542945861816
+ ],
+ [
+ "▁Guess",
+ -13.18056583404541
+ ],
+ [
+ "seraient",
+ -13.181038856506348
+ ],
+ [
+ "▁trench",
+ -13.181156158447266
+ ],
+ [
+ "▁piept",
+ -13.181283950805664
+ ],
+ [
+ "categorized",
+ -13.181396484375
+ ],
+ [
+ "softer",
+ -13.1815185546875
+ ],
+ [
+ "▁feasibility",
+ -13.181519508361816
+ ],
+ [
+ "▁restructuring",
+ -13.181519508361816
+ ],
+ [
+ "▁GOOD",
+ -13.181537628173828
+ ],
+ [
+ "▁inspiré",
+ -13.181610107421875
+ ],
+ [
+ "▁spéci",
+ -13.18163013458252
+ ],
+ [
+ "▁Mattress",
+ -13.181686401367188
+ ],
+ [
+ "▁biologique",
+ -13.181702613830566
+ ],
+ [
+ "▁Crema",
+ -13.182043075561523
+ ],
+ [
+ "▁korrekt",
+ -13.182063102722168
+ ],
+ [
+ "▁imperfect",
+ -13.182205200195312
+ ],
+ [
+ "▁advantageous",
+ -13.182329177856445
+ ],
+ [
+ "9.00",
+ -13.182390213012695
+ ],
+ [
+ "PAL",
+ -13.182557106018066
+ ],
+ [
+ "▁Illustration",
+ -13.182607650756836
+ ],
+ [
+ "▁Katherine",
+ -13.182607650756836
+ ],
+ [
+ "▁cervical",
+ -13.182607650756836
+ ],
+ [
+ "▁hectic",
+ -13.182611465454102
+ ],
+ [
+ "▁Belastung",
+ -13.182615280151367
+ ],
+ [
+ "▁Laguna",
+ -13.182628631591797
+ ],
+ [
+ "▁Burton",
+ -13.182761192321777
+ ],
+ [
+ "nettoyage",
+ -13.182875633239746
+ ],
+ [
+ "Toward",
+ -13.183072090148926
+ ],
+ [
+ "continuare",
+ -13.183072090148926
+ ],
+ [
+ "▁acumulat",
+ -13.183106422424316
+ ],
+ [
+ "▁déposé",
+ -13.183216094970703
+ ],
+ [
+ "▁prestige",
+ -13.183269500732422
+ ],
+ [
+ "▁LNG",
+ -13.183525085449219
+ ],
+ [
+ "▁Dacia",
+ -13.183662414550781
+ ],
+ [
+ "▁concede",
+ -13.183691024780273
+ ],
+ [
+ "▁reconciliation",
+ -13.183822631835938
+ ],
+ [
+ "Sistemul",
+ -13.183877944946289
+ ],
+ [
+ "Speed",
+ -13.183937072753906
+ ],
+ [
+ "▁Implant",
+ -13.183977127075195
+ ],
+ [
+ "▁möchtest",
+ -13.184020042419434
+ ],
+ [
+ "▁Norton",
+ -13.184064865112305
+ ],
+ [
+ "▁cosmic",
+ -13.184181213378906
+ ],
+ [
+ "enregistrement",
+ -13.184247016906738
+ ],
+ [
+ "țării",
+ -13.18433952331543
+ ],
+ [
+ "Veröffentlichung",
+ -13.184786796569824
+ ],
+ [
+ "erlebnis",
+ -13.184786796569824
+ ],
+ [
+ "▁Carpenter",
+ -13.184786796569824
+ ],
+ [
+ "▁INFORMATION",
+ -13.184786796569824
+ ],
+ [
+ "invites",
+ -13.18481731414795
+ ],
+ [
+ "▁gewan",
+ -13.1849365234375
+ ],
+ [
+ "▁réservé",
+ -13.184986114501953
+ ],
+ [
+ "▁aquatic",
+ -13.184988021850586
+ ],
+ [
+ "▁Seoul",
+ -13.18507194519043
+ ],
+ [
+ "▁älter",
+ -13.185185432434082
+ ],
+ [
+ "▁classmates",
+ -13.185223579406738
+ ],
+ [
+ "gelangen",
+ -13.185253143310547
+ ],
+ [
+ "▁Camill",
+ -13.185285568237305
+ ],
+ [
+ "simo",
+ -13.185291290283203
+ ],
+ [
+ "▁dormitor",
+ -13.185333251953125
+ ],
+ [
+ "wahren",
+ -13.185354232788086
+ ],
+ [
+ "▁incremental",
+ -13.185357093811035
+ ],
+ [
+ "▁caci",
+ -13.185494422912598
+ ],
+ [
+ "mittlere",
+ -13.185752868652344
+ ],
+ [
+ "▁condominium",
+ -13.185877799987793
+ ],
+ [
+ "▁rainforest",
+ -13.185877799987793
+ ],
+ [
+ "▁championnat",
+ -13.185891151428223
+ ],
+ [
+ "▁interrupted",
+ -13.185921669006348
+ ],
+ [
+ "▁tactile",
+ -13.185930252075195
+ ],
+ [
+ "▁unconditional",
+ -13.185945510864258
+ ],
+ [
+ "▁reactive",
+ -13.186041831970215
+ ],
+ [
+ "▁Stretch",
+ -13.1861572265625
+ ],
+ [
+ "▁serene",
+ -13.18624210357666
+ ],
+ [
+ "570",
+ -13.186318397521973
+ ],
+ [
+ "igte",
+ -13.186376571655273
+ ],
+ [
+ "Louis",
+ -13.186410903930664
+ ],
+ [
+ "▁Mittelpunkt",
+ -13.186493873596191
+ ],
+ [
+ "EEP",
+ -13.18651294708252
+ ],
+ [
+ "▁vault",
+ -13.186552047729492
+ ],
+ [
+ "absolu",
+ -13.186893463134766
+ ],
+ [
+ "▁solidarity",
+ -13.186971664428711
+ ],
+ [
+ "CLICK",
+ -13.18708324432373
+ ],
+ [
+ "▁hustle",
+ -13.187090873718262
+ ],
+ [
+ "▁microscope",
+ -13.187105178833008
+ ],
+ [
+ "▁Recommended",
+ -13.187111854553223
+ ],
+ [
+ "âche",
+ -13.18716812133789
+ ],
+ [
+ "▁flashlight",
+ -13.187286376953125
+ ],
+ [
+ "modificarea",
+ -13.18754768371582
+ ],
+ [
+ "izaţi",
+ -13.18773078918457
+ ],
+ [
+ "planned",
+ -13.187899589538574
+ ],
+ [
+ "Download",
+ -13.187906265258789
+ ],
+ [
+ "▁gourmand",
+ -13.188064575195312
+ ],
+ [
+ "▁subsidiaries",
+ -13.188064575195312
+ ],
+ [
+ "orthodox",
+ -13.188135147094727
+ ],
+ [
+ "▁Auburn",
+ -13.188323020935059
+ ],
+ [
+ "▁exprimat",
+ -13.188336372375488
+ ],
+ [
+ "procédé",
+ -13.18861198425293
+ ],
+ [
+ "▁ressenti",
+ -13.188648223876953
+ ],
+ [
+ "▁stint",
+ -13.188678741455078
+ ],
+ [
+ "Essentially",
+ -13.189072608947754
+ ],
+ [
+ "▁Savior",
+ -13.189164161682129
+ ],
+ [
+ "▁Flood",
+ -13.189168930053711
+ ],
+ [
+ "▁neurological",
+ -13.189249038696289
+ ],
+ [
+ "▁strig",
+ -13.189340591430664
+ ],
+ [
+ "scended",
+ -13.189421653747559
+ ],
+ [
+ "▁Shiva",
+ -13.189483642578125
+ ],
+ [
+ "▁Sketch",
+ -13.189544677734375
+ ],
+ [
+ "▁monarch",
+ -13.18956184387207
+ ],
+ [
+ "▁Preview",
+ -13.189632415771484
+ ],
+ [
+ "▁bewegt",
+ -13.189811706542969
+ ],
+ [
+ "mapped",
+ -13.189818382263184
+ ],
+ [
+ "énorme",
+ -13.189962387084961
+ ],
+ [
+ "▁définition",
+ -13.189963340759277
+ ],
+ [
+ "▁nécessité",
+ -13.189984321594238
+ ],
+ [
+ "▁antren",
+ -13.190027236938477
+ ],
+ [
+ "▁Infant",
+ -13.190072059631348
+ ],
+ [
+ "▁incumbent",
+ -13.190255165100098
+ ],
+ [
+ "▁pavilion",
+ -13.190255165100098
+ ],
+ [
+ "▁Taliban",
+ -13.19025707244873
+ ],
+ [
+ "Easily",
+ -13.19025993347168
+ ],
+ [
+ "▁verteilt",
+ -13.19030475616455
+ ],
+ [
+ "▁Biblical",
+ -13.190320014953613
+ ],
+ [
+ "Christian",
+ -13.190333366394043
+ ],
+ [
+ "județul",
+ -13.190436363220215
+ ],
+ [
+ "Learning",
+ -13.19046688079834
+ ],
+ [
+ "▁Expand",
+ -13.19054126739502
+ ],
+ [
+ "▁Attach",
+ -13.19056224822998
+ ],
+ [
+ "consideră",
+ -13.190573692321777
+ ],
+ [
+ "einsatz",
+ -13.190574645996094
+ ],
+ [
+ "Numai",
+ -13.190585136413574
+ ],
+ [
+ "▁Eintrag",
+ -13.190597534179688
+ ],
+ [
+ "▁üblich",
+ -13.190607070922852
+ ],
+ [
+ "▁cumpără",
+ -13.19062614440918
+ ],
+ [
+ "escaped",
+ -13.190693855285645
+ ],
+ [
+ "▁Ortodox",
+ -13.190804481506348
+ ],
+ [
+ "▁obţinut",
+ -13.190805435180664
+ ],
+ [
+ "ecluded",
+ -13.191036224365234
+ ],
+ [
+ "▁brownie",
+ -13.191089630126953
+ ],
+ [
+ "▁regulament",
+ -13.191253662109375
+ ],
+ [
+ "▁Chaos",
+ -13.191302299499512
+ ],
+ [
+ "▁masiv",
+ -13.19132137298584
+ ],
+ [
+ "▁Gerald",
+ -13.191376686096191
+ ],
+ [
+ "▁Sigur",
+ -13.191380500793457
+ ],
+ [
+ "▁wavelength",
+ -13.191380500793457
+ ],
+ [
+ "▁retiring",
+ -13.191396713256836
+ ],
+ [
+ "▁exactement",
+ -13.191819190979004
+ ],
+ [
+ "ntino",
+ -13.191823959350586
+ ],
+ [
+ "▁Krebs",
+ -13.19194221496582
+ ],
+ [
+ "▁monatlich",
+ -13.191956520080566
+ ],
+ [
+ "▁aranj",
+ -13.192011833190918
+ ],
+ [
+ "▁priveşt",
+ -13.192099571228027
+ ],
+ [
+ "▁mecanic",
+ -13.192109107971191
+ ],
+ [
+ "money",
+ -13.192233085632324
+ ],
+ [
+ "parliamentary",
+ -13.1922607421875
+ ],
+ [
+ "▁probation",
+ -13.192427635192871
+ ],
+ [
+ "embroidered",
+ -13.192451477050781
+ ],
+ [
+ "▁amenajat",
+ -13.192451477050781
+ ],
+ [
+ "▁remnant",
+ -13.192451477050781
+ ],
+ [
+ "▁senzati",
+ -13.192472457885742
+ ],
+ [
+ "▁Declaration",
+ -13.192483901977539
+ ],
+ [
+ "farbe",
+ -13.192506790161133
+ ],
+ [
+ "▁skinny",
+ -13.19260311126709
+ ],
+ [
+ "Energi",
+ -13.192648887634277
+ ],
+ [
+ "verhältnisse",
+ -13.19288158416748
+ ],
+ [
+ "Recruit",
+ -13.192972183227539
+ ],
+ [
+ "frying",
+ -13.193161010742188
+ ],
+ [
+ "925",
+ -13.193294525146484
+ ],
+ [
+ "nstruire",
+ -13.193302154541016
+ ],
+ [
+ "toasted",
+ -13.193424224853516
+ ],
+ [
+ "▁nicotine",
+ -13.193551063537598
+ ],
+ [
+ "recessed",
+ -13.193570137023926
+ ],
+ [
+ "▁dialect",
+ -13.193572044372559
+ ],
+ [
+ "▁confisc",
+ -13.193575859069824
+ ],
+ [
+ "▁bubbl",
+ -13.193643569946289
+ ],
+ [
+ "▁Precision",
+ -13.193682670593262
+ ],
+ [
+ "▁sollicit",
+ -13.193842887878418
+ ],
+ [
+ "▁Moral",
+ -13.193977355957031
+ ],
+ [
+ "▁renseignements",
+ -13.194112777709961
+ ],
+ [
+ "UMP",
+ -13.194116592407227
+ ],
+ [
+ "ijn",
+ -13.194183349609375
+ ],
+ [
+ "▁fermeture",
+ -13.194320678710938
+ ],
+ [
+ "▁blueprint",
+ -13.19462776184082
+ ],
+ [
+ "▁groceries",
+ -13.194652557373047
+ ],
+ [
+ "möbel",
+ -13.194655418395996
+ ],
+ [
+ "▁Plenty",
+ -13.194657325744629
+ ],
+ [
+ "▁forfeit",
+ -13.194719314575195
+ ],
+ [
+ "méthodes",
+ -13.194915771484375
+ ],
+ [
+ "paving",
+ -13.19493293762207
+ ],
+ [
+ "outheastern",
+ -13.194979667663574
+ ],
+ [
+ "▁Overview",
+ -13.19503116607666
+ ],
+ [
+ "▁observers",
+ -13.195171356201172
+ ],
+ [
+ "▁Timișoara",
+ -13.19520378112793
+ ],
+ [
+ "noticing",
+ -13.195332527160645
+ ],
+ [
+ "▁Owl",
+ -13.195381164550781
+ ],
+ [
+ "▁1925",
+ -13.195517539978027
+ ],
+ [
+ "▁prüfen",
+ -13.195755004882812
+ ],
+ [
+ "▁Bewohner",
+ -13.195756912231445
+ ],
+ [
+ "▁Latvia",
+ -13.195770263671875
+ ],
+ [
+ "▁Tuscan",
+ -13.19577407836914
+ ],
+ [
+ "▁apprenticeship",
+ -13.195789337158203
+ ],
+ [
+ "▁courteous",
+ -13.1958646774292
+ ],
+ [
+ "adult",
+ -13.196023941040039
+ ],
+ [
+ "Licensed",
+ -13.196029663085938
+ ],
+ [
+ "abused",
+ -13.196762084960938
+ ],
+ [
+ "confidence",
+ -13.19678020477295
+ ],
+ [
+ "▁revolt",
+ -13.196782112121582
+ ],
+ [
+ "conference",
+ -13.196861267089844
+ ],
+ [
+ "genoss",
+ -13.196914672851562
+ ],
+ [
+ "▁răni",
+ -13.196944236755371
+ ],
+ [
+ "▁Intervention",
+ -13.196949005126953
+ ],
+ [
+ "▁primesc",
+ -13.196969985961914
+ ],
+ [
+ "trays",
+ -13.197041511535645
+ ],
+ [
+ "nozzle",
+ -13.197216033935547
+ ],
+ [
+ "▁splitting",
+ -13.197443962097168
+ ],
+ [
+ "▁könne",
+ -13.197507858276367
+ ],
+ [
+ "▁peisaj",
+ -13.197943687438965
+ ],
+ [
+ "▁academia",
+ -13.197962760925293
+ ],
+ [
+ "▁chakra",
+ -13.197979927062988
+ ],
+ [
+ "▁Abdul",
+ -13.1981201171875
+ ],
+ [
+ "▁Beschreibung",
+ -13.198225021362305
+ ],
+ [
+ "Regeln",
+ -13.19831371307373
+ ],
+ [
+ "eezy",
+ -13.198314666748047
+ ],
+ [
+ "▁problématique",
+ -13.198515892028809
+ ],
+ [
+ "▁Ausführung",
+ -13.198524475097656
+ ],
+ [
+ "▁reconnect",
+ -13.19868278503418
+ ],
+ [
+ "▁telefonic",
+ -13.198966026306152
+ ],
+ [
+ "▁Ethereum",
+ -13.199069023132324
+ ],
+ [
+ "▁Winnipeg",
+ -13.199069023132324
+ ],
+ [
+ "▁misconception",
+ -13.199069023132324
+ ],
+ [
+ "▁Verpackung",
+ -13.199070930480957
+ ],
+ [
+ "▁erzeugt",
+ -13.199097633361816
+ ],
+ [
+ "▁Identity",
+ -13.199104309082031
+ ],
+ [
+ "▁dunkle",
+ -13.199109077453613
+ ],
+ [
+ "sustaining",
+ -13.19916820526123
+ ],
+ [
+ "▁pereche",
+ -13.199178695678711
+ ],
+ [
+ "▁neîn",
+ -13.199239730834961
+ ],
+ [
+ "directorul",
+ -13.199291229248047
+ ],
+ [
+ "▁élabor",
+ -13.199584007263184
+ ],
+ [
+ "▁Hollow",
+ -13.19960880279541
+ ],
+ [
+ "▁getestet",
+ -13.199751853942871
+ ],
+ [
+ "▁Promote",
+ -13.199797630310059
+ ],
+ [
+ "agriculture",
+ -13.199920654296875
+ ],
+ [
+ "▁deosebir",
+ -13.199934005737305
+ ],
+ [
+ "▁neam",
+ -13.199999809265137
+ ],
+ [
+ "aufbau",
+ -13.200042724609375
+ ],
+ [
+ "▁susținut",
+ -13.200079917907715
+ ],
+ [
+ "fueled",
+ -13.200119018554688
+ ],
+ [
+ "▁impresionant",
+ -13.200177192687988
+ ],
+ [
+ "innate",
+ -13.20026969909668
+ ],
+ [
+ "grenzt",
+ -13.200340270996094
+ ],
+ [
+ "rescued",
+ -13.200514793395996
+ ],
+ [
+ "bestand",
+ -13.200559616088867
+ ],
+ [
+ "▁adjunct",
+ -13.200729370117188
+ ],
+ [
+ "▁Mischung",
+ -13.200754165649414
+ ],
+ [
+ "▁Lease",
+ -13.201258659362793
+ ],
+ [
+ "espagnol",
+ -13.201284408569336
+ ],
+ [
+ "▁Kickstarter",
+ -13.201284408569336
+ ],
+ [
+ "▁buzunar",
+ -13.201284408569336
+ ],
+ [
+ "▁buddies",
+ -13.20129108428955
+ ],
+ [
+ "käufe",
+ -13.201485633850098
+ ],
+ [
+ "cevoir",
+ -13.201582908630371
+ ],
+ [
+ "▁creşte",
+ -13.201675415039062
+ ],
+ [
+ "▁Cluster",
+ -13.201825141906738
+ ],
+ [
+ "▁obișnui",
+ -13.201838493347168
+ ],
+ [
+ "▁cassette",
+ -13.201889038085938
+ ],
+ [
+ "▁optisch",
+ -13.201947212219238
+ ],
+ [
+ "manned",
+ -13.20200252532959
+ ],
+ [
+ "schneid",
+ -13.202362060546875
+ ],
+ [
+ "Württemberg",
+ -13.202393531799316
+ ],
+ [
+ "shredded",
+ -13.202393531799316
+ ],
+ [
+ "▁botanical",
+ -13.20239543914795
+ ],
+ [
+ "characterization",
+ -13.202445983886719
+ ],
+ [
+ "▁Durchführung",
+ -13.202452659606934
+ ],
+ [
+ "▁tireless",
+ -13.20250129699707
+ ],
+ [
+ "lässlich",
+ -13.20254135131836
+ ],
+ [
+ "▁Merchant",
+ -13.202570915222168
+ ],
+ [
+ "joutez",
+ -13.20259952545166
+ ],
+ [
+ "▁amélior",
+ -13.202676773071289
+ ],
+ [
+ "fixed",
+ -13.202741622924805
+ ],
+ [
+ "kho",
+ -13.202760696411133
+ ],
+ [
+ "▁televizor",
+ -13.202948570251465
+ ],
+ [
+ "▁Davies",
+ -13.202964782714844
+ ],
+ [
+ "enceinte",
+ -13.203118324279785
+ ],
+ [
+ "▁Panorama",
+ -13.20350456237793
+ ],
+ [
+ "▁maternal",
+ -13.203507423400879
+ ],
+ [
+ "diversified",
+ -13.203513145446777
+ ],
+ [
+ "▁Jü",
+ -13.203570365905762
+ ],
+ [
+ "▁naz",
+ -13.203730583190918
+ ],
+ [
+ "▁plonge",
+ -13.2039213180542
+ ],
+ [
+ "geschickt",
+ -13.203944206237793
+ ],
+ [
+ "MIS",
+ -13.204215049743652
+ ],
+ [
+ "ragged",
+ -13.204553604125977
+ ],
+ [
+ "▁diarrhea",
+ -13.20461654663086
+ ],
+ [
+ "▁tsunami",
+ -13.20461654663086
+ ],
+ [
+ "▁Nikola",
+ -13.204625129699707
+ ],
+ [
+ "▁festivities",
+ -13.20464038848877
+ ],
+ [
+ "potting",
+ -13.20479965209961
+ ],
+ [
+ "▁telefonisch",
+ -13.204874038696289
+ ],
+ [
+ "TAR",
+ -13.204971313476562
+ ],
+ [
+ "▁schimbări",
+ -13.205023765563965
+ ],
+ [
+ "▁occidental",
+ -13.205172538757324
+ ],
+ [
+ "schloss",
+ -13.205179214477539
+ ],
+ [
+ "Print",
+ -13.205284118652344
+ ],
+ [
+ "▁autoritățil",
+ -13.205361366271973
+ ],
+ [
+ "idos",
+ -13.20556640625
+ ],
+ [
+ "mediocr",
+ -13.20559310913086
+ ],
+ [
+ "▁Decla",
+ -13.205686569213867
+ ],
+ [
+ "▁Elliott",
+ -13.205729484558105
+ ],
+ [
+ "▁pinpoint",
+ -13.205734252929688
+ ],
+ [
+ "▁disciple",
+ -13.20579719543457
+ ],
+ [
+ "▁Cairo",
+ -13.2058744430542
+ ],
+ [
+ "▁15-20",
+ -13.2059326171875
+ ],
+ [
+ "▁limbaj",
+ -13.20611572265625
+ ],
+ [
+ "▁retenu",
+ -13.206154823303223
+ ],
+ [
+ "▁Blüte",
+ -13.20628833770752
+ ],
+ [
+ "▁MINI",
+ -13.206467628479004
+ ],
+ [
+ "▁lumină",
+ -13.206567764282227
+ ],
+ [
+ "▁flawed",
+ -13.206846237182617
+ ],
+ [
+ "▁Belarus",
+ -13.207067489624023
+ ],
+ [
+ "Totul",
+ -13.207207679748535
+ ],
+ [
+ "hôte",
+ -13.207273483276367
+ ],
+ [
+ "▁verbringen",
+ -13.207315444946289
+ ],
+ [
+ "▁simultaneous",
+ -13.207344055175781
+ ],
+ [
+ "▁competiți",
+ -13.207402229309082
+ ],
+ [
+ "▁lancement",
+ -13.207413673400879
+ ],
+ [
+ "▁proprietati",
+ -13.207432746887207
+ ],
+ [
+ "▁angajator",
+ -13.207465171813965
+ ],
+ [
+ "▁ignorant",
+ -13.207674026489258
+ ],
+ [
+ "▁indicative",
+ -13.207700729370117
+ ],
+ [
+ "▁Bearbeitung",
+ -13.207961082458496
+ ],
+ [
+ "▁Ungaria",
+ -13.207961082458496
+ ],
+ [
+ "▁Sfint",
+ -13.208015441894531
+ ],
+ [
+ "▁Trojan",
+ -13.20804214477539
+ ],
+ [
+ "▁1911",
+ -13.208100318908691
+ ],
+ [
+ "▁reliabl",
+ -13.2081937789917
+ ],
+ [
+ "6-0",
+ -13.20827865600586
+ ],
+ [
+ "obst",
+ -13.208523750305176
+ ],
+ [
+ "▁relève",
+ -13.208579063415527
+ ],
+ [
+ "▁standpoint",
+ -13.208874702453613
+ ],
+ [
+ "ridden",
+ -13.208918571472168
+ ],
+ [
+ "▁Pdf",
+ -13.209005355834961
+ ],
+ [
+ "tatewide",
+ -13.209051132202148
+ ],
+ [
+ "Water",
+ -13.209062576293945
+ ],
+ [
+ "▁Pricing",
+ -13.209089279174805
+ ],
+ [
+ "▁protecţi",
+ -13.209168434143066
+ ],
+ [
+ "November",
+ -13.209615707397461
+ ],
+ [
+ "▁televiziune",
+ -13.20964241027832
+ ],
+ [
+ "Sodium",
+ -13.209881782531738
+ ],
+ [
+ "douceur",
+ -13.209942817687988
+ ],
+ [
+ "▁Flasche",
+ -13.210183143615723
+ ],
+ [
+ "3.9",
+ -13.210193634033203
+ ],
+ [
+ "▁electromagnetic",
+ -13.210195541381836
+ ],
+ [
+ "▁mitochondria",
+ -13.210195541381836
+ ],
+ [
+ "Suddenly",
+ -13.210199356079102
+ ],
+ [
+ "▁Drupal",
+ -13.210201263427734
+ ],
+ [
+ "▁supraveghere",
+ -13.210211753845215
+ ],
+ [
+ "▁cornea",
+ -13.210288047790527
+ ],
+ [
+ "räumt",
+ -13.210309982299805
+ ],
+ [
+ "▁healed",
+ -13.210410118103027
+ ],
+ [
+ "Roc",
+ -13.210649490356445
+ ],
+ [
+ "▁temporar",
+ -13.210707664489746
+ ],
+ [
+ "▁amaze",
+ -13.210770606994629
+ ],
+ [
+ "▁confrunta",
+ -13.210833549499512
+ ],
+ [
+ "Afterward",
+ -13.210836410522461
+ ],
+ [
+ "▁festgelegt",
+ -13.21084213256836
+ ],
+ [
+ "▁Kuchen",
+ -13.210844993591309
+ ],
+ [
+ "▁perpetual",
+ -13.210858345031738
+ ],
+ [
+ "systematically",
+ -13.211000442504883
+ ],
+ [
+ "▁coloan",
+ -13.211006164550781
+ ],
+ [
+ "▁extensi",
+ -13.211058616638184
+ ],
+ [
+ "▁Județean",
+ -13.211315155029297
+ ],
+ [
+ "▁amelior",
+ -13.211315155029297
+ ],
+ [
+ "▁illustrator",
+ -13.211315155029297
+ ],
+ [
+ "▁titanium",
+ -13.211344718933105
+ ],
+ [
+ "SMEs",
+ -13.211384773254395
+ ],
+ [
+ "taxable",
+ -13.211578369140625
+ ],
+ [
+ "▁Borough",
+ -13.211607933044434
+ ],
+ [
+ "verlust",
+ -13.211772918701172
+ ],
+ [
+ "ductive",
+ -13.21233081817627
+ ],
+ [
+ "▁Küste",
+ -13.212335586547852
+ ],
+ [
+ "▁végétal",
+ -13.212410926818848
+ ],
+ [
+ "▁breastfeeding",
+ -13.212435722351074
+ ],
+ [
+ "▁captivating",
+ -13.212435722351074
+ ],
+ [
+ "▁Chevy",
+ -13.212443351745605
+ ],
+ [
+ "▁aerospace",
+ -13.212469100952148
+ ],
+ [
+ "pozitia",
+ -13.213095664978027
+ ],
+ [
+ "Tutor",
+ -13.213199615478516
+ ],
+ [
+ "▁spum",
+ -13.213312149047852
+ ],
+ [
+ "curând",
+ -13.213419914245605
+ ],
+ [
+ "iscus",
+ -13.213458061218262
+ ],
+ [
+ "October",
+ -13.213495254516602
+ ],
+ [
+ "▁Reparatur",
+ -13.213557243347168
+ ],
+ [
+ "▁Servicii",
+ -13.213574409484863
+ ],
+ [
+ "▁Gonz",
+ -13.21357536315918
+ ],
+ [
+ "▁cybersecurity",
+ -13.21357536315918
+ ],
+ [
+ "▁UCLA",
+ -13.213678359985352
+ ],
+ [
+ "rissa",
+ -13.213835716247559
+ ],
+ [
+ "▁Kemp",
+ -13.213850021362305
+ ],
+ [
+ "▁piston",
+ -13.214046478271484
+ ],
+ [
+ "▁révèle",
+ -13.214118957519531
+ ],
+ [
+ "▁posséd",
+ -13.21412181854248
+ ],
+ [
+ "▁versehen",
+ -13.214129447937012
+ ],
+ [
+ "▁scrutin",
+ -13.214226722717285
+ ],
+ [
+ "donnant",
+ -13.21436882019043
+ ],
+ [
+ "▁Geschwindigkeit",
+ -13.214680671691895
+ ],
+ [
+ "▁Panasonic",
+ -13.214680671691895
+ ],
+ [
+ "audio",
+ -13.214700698852539
+ ],
+ [
+ "▁Packaging",
+ -13.214771270751953
+ ],
+ [
+ "phra",
+ -13.2147798538208
+ ],
+ [
+ "▁Letzte",
+ -13.214954376220703
+ ],
+ [
+ "insicht",
+ -13.215141296386719
+ ],
+ [
+ "▁sammeln",
+ -13.215243339538574
+ ],
+ [
+ "▁extins",
+ -13.215259552001953
+ ],
+ [
+ "▁collège",
+ -13.215266227722168
+ ],
+ [
+ "ancies",
+ -13.215343475341797
+ ],
+ [
+ "▁întâlnit",
+ -13.215350151062012
+ ],
+ [
+ "▁Servi",
+ -13.215392112731934
+ ],
+ [
+ "stattet",
+ -13.215493202209473
+ ],
+ [
+ "▁abstraction",
+ -13.215566635131836
+ ],
+ [
+ "▁candidature",
+ -13.215592384338379
+ ],
+ [
+ "ONU",
+ -13.215676307678223
+ ],
+ [
+ "▁raffle",
+ -13.215826988220215
+ ],
+ [
+ "▁Soldier",
+ -13.215834617614746
+ ],
+ [
+ "▁stipulate",
+ -13.215883255004883
+ ],
+ [
+ "▁vizual",
+ -13.215950012207031
+ ],
+ [
+ "lucht",
+ -13.216007232666016
+ ],
+ [
+ "▁circus",
+ -13.216068267822266
+ ],
+ [
+ "▁decree",
+ -13.216259002685547
+ ],
+ [
+ "immeuble",
+ -13.216367721557617
+ ],
+ [
+ "Store",
+ -13.216426849365234
+ ],
+ [
+ "randul",
+ -13.216622352600098
+ ],
+ [
+ "▁narration",
+ -13.216933250427246
+ ],
+ [
+ "implication",
+ -13.216958045959473
+ ],
+ [
+ "▁discontinued",
+ -13.216971397399902
+ ],
+ [
+ "▁Pilates",
+ -13.216989517211914
+ ],
+ [
+ "▁biais",
+ -13.21701431274414
+ ],
+ [
+ "panel",
+ -13.217325210571289
+ ],
+ [
+ "▁mower",
+ -13.217458724975586
+ ],
+ [
+ "▁Castro",
+ -13.21753978729248
+ ],
+ [
+ "pregătire",
+ -13.217641830444336
+ ],
+ [
+ "▁denomination",
+ -13.218062400817871
+ ],
+ [
+ "▁throttle",
+ -13.21806526184082
+ ],
+ [
+ "▁finition",
+ -13.218086242675781
+ ],
+ [
+ "▁clarification",
+ -13.218286514282227
+ ],
+ [
+ "laut",
+ -13.218366622924805
+ ],
+ [
+ "▁wastewater",
+ -13.2184419631958
+ ],
+ [
+ "▁Sanchez",
+ -13.218770980834961
+ ],
+ [
+ "▁Umfeld",
+ -13.2189359664917
+ ],
+ [
+ "▁consili",
+ -13.218997955322266
+ ],
+ [
+ "extrait",
+ -13.219013214111328
+ ],
+ [
+ "ionism",
+ -13.2190523147583
+ ],
+ [
+ "▁Cannabis",
+ -13.219186782836914
+ ],
+ [
+ "▁misconduct",
+ -13.219186782836914
+ ],
+ [
+ "▁shepherd",
+ -13.219186782836914
+ ],
+ [
+ "▁feminist",
+ -13.21919059753418
+ ],
+ [
+ "▁criterii",
+ -13.219212532043457
+ ],
+ [
+ "America",
+ -13.219219207763672
+ ],
+ [
+ "▁Telephone",
+ -13.219270706176758
+ ],
+ [
+ "▁Fritz",
+ -13.219438552856445
+ ],
+ [
+ "▁cheltui",
+ -13.219794273376465
+ ],
+ [
+ "▁Übung",
+ -13.219857215881348
+ ],
+ [
+ "făcută",
+ -13.22006893157959
+ ],
+ [
+ "▁străzi",
+ -13.220170021057129
+ ],
+ [
+ "influencing",
+ -13.220315933227539
+ ],
+ [
+ "▁Democracy",
+ -13.220321655273438
+ ],
+ [
+ "atorium",
+ -13.220376014709473
+ ],
+ [
+ "▁Stufe",
+ -13.220465660095215
+ ],
+ [
+ "▁Cornell",
+ -13.220660209655762
+ ],
+ [
+ "zugehen",
+ -13.22074031829834
+ ],
+ [
+ "▁coton",
+ -13.220804214477539
+ ],
+ [
+ "▁beinhaltet",
+ -13.220881462097168
+ ],
+ [
+ "▁kritisch",
+ -13.220884323120117
+ ],
+ [
+ "▁Kalender",
+ -13.22105884552002
+ ],
+ [
+ "▁Teig",
+ -13.221253395080566
+ ],
+ [
+ "cooked",
+ -13.221264839172363
+ ],
+ [
+ "▁diversité",
+ -13.221390724182129
+ ],
+ [
+ "recognizable",
+ -13.221446990966797
+ ],
+ [
+ "▁Dictionary",
+ -13.221446990966797
+ ],
+ [
+ "attribution",
+ -13.22145938873291
+ ],
+ [
+ "▁Teresa",
+ -13.221471786499023
+ ],
+ [
+ "▁Ahmad",
+ -13.221487998962402
+ ],
+ [
+ "HAM",
+ -13.221627235412598
+ ],
+ [
+ "▁floss",
+ -13.221668243408203
+ ],
+ [
+ "génie",
+ -13.2218599319458
+ ],
+ [
+ "▁Espa",
+ -13.221989631652832
+ ],
+ [
+ "hersteller",
+ -13.221993446350098
+ ],
+ [
+ "Musée",
+ -13.222001075744629
+ ],
+ [
+ "▁Crawford",
+ -13.222579002380371
+ ],
+ [
+ "▁Phantom",
+ -13.222579002380371
+ ],
+ [
+ "▁Jenkins",
+ -13.222640037536621
+ ],
+ [
+ "genauer",
+ -13.222774505615234
+ ],
+ [
+ "▁acţiuni",
+ -13.222885131835938
+ ],
+ [
+ "▁meciuri",
+ -13.22322940826416
+ ],
+ [
+ "▁verstärkt",
+ -13.22326374053955
+ ],
+ [
+ "▁troop",
+ -13.22341251373291
+ ],
+ [
+ "räder",
+ -13.223483085632324
+ ],
+ [
+ "Putting",
+ -13.223536491394043
+ ],
+ [
+ "NASDAQ",
+ -13.223712921142578
+ ],
+ [
+ "▁Buddhism",
+ -13.223712921142578
+ ],
+ [
+ "▁Religious",
+ -13.223712921142578
+ ],
+ [
+ "▁accommodating",
+ -13.223712921142578
+ ],
+ [
+ "▁lendemain",
+ -13.223712921142578
+ ],
+ [
+ "▁plywood",
+ -13.223714828491211
+ ],
+ [
+ "▁inflatable",
+ -13.223724365234375
+ ],
+ [
+ "▁sèche",
+ -13.223731994628906
+ ],
+ [
+ "▁fragil",
+ -13.223845481872559
+ ],
+ [
+ "▁Filip",
+ -13.224115371704102
+ ],
+ [
+ "▁Terrace",
+ -13.224274635314941
+ ],
+ [
+ "Biblio",
+ -13.22432804107666
+ ],
+ [
+ "resides",
+ -13.22448444366455
+ ],
+ [
+ "▁varf",
+ -13.22451114654541
+ ],
+ [
+ "Bildern",
+ -13.224528312683105
+ ],
+ [
+ "loß",
+ -13.224685668945312
+ ],
+ [
+ "555",
+ -13.224702835083008
+ ],
+ [
+ "▁astounding",
+ -13.224847793579102
+ ],
+ [
+ "▁brillant",
+ -13.224857330322266
+ ],
+ [
+ "▁Railroad",
+ -13.224871635437012
+ ],
+ [
+ "minimizing",
+ -13.224907875061035
+ ],
+ [
+ "▁Benedict",
+ -13.225019454956055
+ ],
+ [
+ "▁$400",
+ -13.225068092346191
+ ],
+ [
+ "▁schematic",
+ -13.225217819213867
+ ],
+ [
+ "Canada",
+ -13.225371360778809
+ ],
+ [
+ "▁psihic",
+ -13.225415229797363
+ ],
+ [
+ "▁avertiz",
+ -13.225497245788574
+ ],
+ [
+ "▁Breed",
+ -13.225550651550293
+ ],
+ [
+ "▁gradina",
+ -13.225606918334961
+ ],
+ [
+ "▁Liege",
+ -13.225822448730469
+ ],
+ [
+ "▁Retirement",
+ -13.225983619689941
+ ],
+ [
+ "▁pergola",
+ -13.226005554199219
+ ],
+ [
+ "▁Kuwait",
+ -13.2260103225708
+ ],
+ [
+ "▁logistic",
+ -13.22629451751709
+ ],
+ [
+ "▁captive",
+ -13.22651481628418
+ ],
+ [
+ "prepared",
+ -13.226568222045898
+ ],
+ [
+ "▁prononc",
+ -13.226568222045898
+ ],
+ [
+ "Celui",
+ -13.226676940917969
+ ],
+ [
+ "deutschland",
+ -13.227120399475098
+ ],
+ [
+ "▁devreme",
+ -13.227124214172363
+ ],
+ [
+ "▁părți",
+ -13.227270126342773
+ ],
+ [
+ "▁1934",
+ -13.227517127990723
+ ],
+ [
+ "▁ersetzt",
+ -13.227560997009277
+ ],
+ [
+ "▁frightening",
+ -13.227689743041992
+ ],
+ [
+ "▁fiecărui",
+ -13.227819442749023
+ ],
+ [
+ "correct",
+ -13.22799015045166
+ ],
+ [
+ "6.6",
+ -13.228057861328125
+ ],
+ [
+ "▁Manitoba",
+ -13.228259086608887
+ ],
+ [
+ "Chartered",
+ -13.228416442871094
+ ],
+ [
+ "▁părăs",
+ -13.228543281555176
+ ],
+ [
+ "Powered",
+ -13.228697776794434
+ ],
+ [
+ "impede",
+ -13.22876262664795
+ ],
+ [
+ "agonist",
+ -13.22878646850586
+ ],
+ [
+ "▁stratégique",
+ -13.228829383850098
+ ],
+ [
+ "▁vigilant",
+ -13.228830337524414
+ ],
+ [
+ "faceted",
+ -13.228930473327637
+ ],
+ [
+ "available",
+ -13.229308128356934
+ ],
+ [
+ "▁Promise",
+ -13.229388236999512
+ ],
+ [
+ "▁humorous",
+ -13.229446411132812
+ ],
+ [
+ "treibt",
+ -13.229449272155762
+ ],
+ [
+ "▁Patrol",
+ -13.229514122009277
+ ],
+ [
+ "huh",
+ -13.229523658752441
+ ],
+ [
+ "ztlich",
+ -13.229804039001465
+ ],
+ [
+ "▁rejet",
+ -13.2299165725708
+ ],
+ [
+ "odeur",
+ -13.229935646057129
+ ],
+ [
+ "usziehbar",
+ -13.22996997833252
+ ],
+ [
+ "▁gespannt",
+ -13.229972839355469
+ ],
+ [
+ "church",
+ -13.230018615722656
+ ],
+ [
+ "▁Popescu",
+ -13.230109214782715
+ ],
+ [
+ "▁einmalig",
+ -13.230518341064453
+ ],
+ [
+ "diluted",
+ -13.230551719665527
+ ],
+ [
+ "lighted",
+ -13.231070518493652
+ ],
+ [
+ "▁stattfinden",
+ -13.23111343383789
+ ],
+ [
+ "▁Reaktion",
+ -13.231183052062988
+ ],
+ [
+ "▁délivr",
+ -13.23134994506836
+ ],
+ [
+ "▁Helfer",
+ -13.231407165527344
+ ],
+ [
+ "Fiind",
+ -13.23142147064209
+ ],
+ [
+ "rmând",
+ -13.231507301330566
+ ],
+ [
+ "▁Beweis",
+ -13.231671333312988
+ ],
+ [
+ "▁Violet",
+ -13.231733322143555
+ ],
+ [
+ "kamera",
+ -13.231764793395996
+ ],
+ [
+ "▁Romney",
+ -13.231779098510742
+ ],
+ [
+ "▁Bradford",
+ -13.231800079345703
+ ],
+ [
+ "stellbar",
+ -13.231852531433105
+ ],
+ [
+ "▁roadmap",
+ -13.231921195983887
+ ],
+ [
+ "▁subconscious",
+ -13.23204231262207
+ ],
+ [
+ "contrasting",
+ -13.232138633728027
+ ],
+ [
+ "mécanisme",
+ -13.232254981994629
+ ],
+ [
+ "kämpft",
+ -13.232255935668945
+ ],
+ [
+ "▁Preston",
+ -13.232719421386719
+ ],
+ [
+ "▁Anliegen",
+ -13.232802391052246
+ ],
+ [
+ "▁necessities",
+ -13.232827186584473
+ ],
+ [
+ "▁detrimental",
+ -13.232828140258789
+ ],
+ [
+ "▁sprawl",
+ -13.232830047607422
+ ],
+ [
+ "▁Erfüllung",
+ -13.23287582397461
+ ],
+ [
+ "▁massacre",
+ -13.2329683303833
+ ],
+ [
+ "▁pietre",
+ -13.232987403869629
+ ],
+ [
+ "▁situații",
+ -13.233027458190918
+ ],
+ [
+ "vêtement",
+ -13.233080863952637
+ ],
+ [
+ "Listed",
+ -13.233144760131836
+ ],
+ [
+ "▁extravagant",
+ -13.233399391174316
+ ],
+ [
+ "▁axle",
+ -13.233525276184082
+ ],
+ [
+ "OTT",
+ -13.233663558959961
+ ],
+ [
+ "wildly",
+ -13.233744621276855
+ ],
+ [
+ "70,000",
+ -13.233797073364258
+ ],
+ [
+ "▁chauffeur",
+ -13.23384952545166
+ ],
+ [
+ "▁Brasov",
+ -13.233972549438477
+ ],
+ [
+ "▁Fähigkeiten",
+ -13.233972549438477
+ ],
+ [
+ "▁staatlich",
+ -13.234025001525879
+ ],
+ [
+ "outlines",
+ -13.234034538269043
+ ],
+ [
+ "▁aufmerksam",
+ -13.234545707702637
+ ],
+ [
+ "▁Relation",
+ -13.234749794006348
+ ],
+ [
+ "▁Stephan",
+ -13.234947204589844
+ ],
+ [
+ "yland",
+ -13.23494815826416
+ ],
+ [
+ "proclaimed",
+ -13.235086441040039
+ ],
+ [
+ "Wallet",
+ -13.235100746154785
+ ],
+ [
+ "verarbeitung",
+ -13.235118865966797
+ ],
+ [
+ "▁überraschen",
+ -13.235118865966797
+ ],
+ [
+ "▁Injury",
+ -13.235125541687012
+ ],
+ [
+ "▁horsepower",
+ -13.235237121582031
+ ],
+ [
+ "▁Tropical",
+ -13.23523998260498
+ ],
+ [
+ "▁wives",
+ -13.235459327697754
+ ],
+ [
+ "adherence",
+ -13.235677719116211
+ ],
+ [
+ "schätzung",
+ -13.235692977905273
+ ],
+ [
+ "▁coherent",
+ -13.235708236694336
+ ],
+ [
+ "parlament",
+ -13.23574161529541
+ ],
+ [
+ "▁stup",
+ -13.235852241516113
+ ],
+ [
+ "▁resonance",
+ -13.23626708984375
+ ],
+ [
+ "▁inheritance",
+ -13.236355781555176
+ ],
+ [
+ "commenced",
+ -13.23645305633545
+ ],
+ [
+ "▁supervise",
+ -13.236475944519043
+ ],
+ [
+ "▁facilitator",
+ -13.236488342285156
+ ],
+ [
+ "fares",
+ -13.236678123474121
+ ],
+ [
+ "▁Tibet",
+ -13.23672866821289
+ ],
+ [
+ "communication",
+ -13.236787796020508
+ ],
+ [
+ "yog",
+ -13.236806869506836
+ ],
+ [
+ "▁WLAN",
+ -13.236842155456543
+ ],
+ [
+ "▁Chili",
+ -13.23685073852539
+ ],
+ [
+ "▁Harold",
+ -13.2369966506958
+ ],
+ [
+ "▁Guerre",
+ -13.237005233764648
+ ],
+ [
+ "▁Femme",
+ -13.237146377563477
+ ],
+ [
+ "▁Lisbon",
+ -13.237231254577637
+ ],
+ [
+ "▁mulțumi",
+ -13.237415313720703
+ ],
+ [
+ "▁vorbereitet",
+ -13.237415313720703
+ ],
+ [
+ "▁aperture",
+ -13.237422943115234
+ ],
+ [
+ "▁Universities",
+ -13.237442016601562
+ ],
+ [
+ "▁reckless",
+ -13.237471580505371
+ ],
+ [
+ "▁Botschaft",
+ -13.237533569335938
+ ],
+ [
+ "▁Squad",
+ -13.238022804260254
+ ],
+ [
+ "▁buoy",
+ -13.238061904907227
+ ],
+ [
+ "participarea",
+ -13.238236427307129
+ ],
+ [
+ "stiinta",
+ -13.238389015197754
+ ],
+ [
+ "▁repeal",
+ -13.238415718078613
+ ],
+ [
+ "drilled",
+ -13.238489151000977
+ ],
+ [
+ "▁Conversation",
+ -13.238567352294922
+ ],
+ [
+ "▁subsid",
+ -13.238615036010742
+ ],
+ [
+ "anstalt",
+ -13.238741874694824
+ ],
+ [
+ "faktor",
+ -13.23874282836914
+ ],
+ [
+ "▁swamp",
+ -13.238790512084961
+ ],
+ [
+ "pflichtig",
+ -13.238921165466309
+ ],
+ [
+ "▁camion",
+ -13.238970756530762
+ ],
+ [
+ "▁gouvern",
+ -13.239032745361328
+ ],
+ [
+ "▁archaeological",
+ -13.239141464233398
+ ],
+ [
+ "▁glitch",
+ -13.239198684692383
+ ],
+ [
+ "average",
+ -13.239294052124023
+ ],
+ [
+ "▁coffre",
+ -13.239481925964355
+ ],
+ [
+ "▁Insert",
+ -13.239513397216797
+ ],
+ [
+ "▁colonne",
+ -13.2395601272583
+ ],
+ [
+ "▁Assess",
+ -13.23962116241455
+ ],
+ [
+ "▁batches",
+ -13.239716529846191
+ ],
+ [
+ "▁ammunition",
+ -13.239717483520508
+ ],
+ [
+ "▁scissors",
+ -13.239717483520508
+ ],
+ [
+ "▁Locksmith",
+ -13.239740371704102
+ ],
+ [
+ "▁Bollywood",
+ -13.239991188049316
+ ],
+ [
+ "expédi",
+ -13.240288734436035
+ ],
+ [
+ "▁descendants",
+ -13.24039363861084
+ ],
+ [
+ "▁unwilling",
+ -13.240506172180176
+ ],
+ [
+ "▁Noise",
+ -13.240649223327637
+ ],
+ [
+ "▁Directive",
+ -13.240660667419434
+ ],
+ [
+ "ATOR",
+ -13.240765571594238
+ ],
+ [
+ "▁Rajasthan",
+ -13.240870475769043
+ ],
+ [
+ "▁chaotic",
+ -13.240888595581055
+ ],
+ [
+ "▁NEED",
+ -13.24093246459961
+ ],
+ [
+ "▁părere",
+ -13.24095344543457
+ ],
+ [
+ "▁begonnen",
+ -13.241448402404785
+ ],
+ [
+ "▁Reef",
+ -13.241504669189453
+ ],
+ [
+ "▁vorgesehen",
+ -13.24161434173584
+ ],
+ [
+ "▁allocate",
+ -13.241826057434082
+ ],
+ [
+ "▁exceptionnel",
+ -13.241936683654785
+ ],
+ [
+ "▁gefertigt",
+ -13.24203872680664
+ ],
+ [
+ "fading",
+ -13.242072105407715
+ ],
+ [
+ "▁interpersonal",
+ -13.242178916931152
+ ],
+ [
+ "▁occupie",
+ -13.242204666137695
+ ],
+ [
+ "▁Teatr",
+ -13.242579460144043
+ ],
+ [
+ "▁kilomètres",
+ -13.242603302001953
+ ],
+ [
+ "▁verbinden",
+ -13.242608070373535
+ ],
+ [
+ "▁Frucht",
+ -13.242643356323242
+ ],
+ [
+ "augmented",
+ -13.242720603942871
+ ],
+ [
+ "▁twentieth",
+ -13.243181228637695
+ ],
+ [
+ "▁aggression",
+ -13.243183135986328
+ ],
+ [
+ "▁Miracle",
+ -13.243184089660645
+ ],
+ [
+ "▁peninsula",
+ -13.243184089660645
+ ],
+ [
+ "▁Fernando",
+ -13.243185043334961
+ ],
+ [
+ "▁autorităţil",
+ -13.243203163146973
+ ],
+ [
+ "▁Iisus",
+ -13.243217468261719
+ ],
+ [
+ "▁puck",
+ -13.243423461914062
+ ],
+ [
+ "titel",
+ -13.243454933166504
+ ],
+ [
+ "▁remake",
+ -13.243562698364258
+ ],
+ [
+ "freiheit",
+ -13.243563652038574
+ ],
+ [
+ "▁Belize",
+ -13.243590354919434
+ ],
+ [
+ "▁secundar",
+ -13.243779182434082
+ ],
+ [
+ "▁perpetrat",
+ -13.243786811828613
+ ],
+ [
+ "jedenfalls",
+ -13.243797302246094
+ ],
+ [
+ "linked",
+ -13.243820190429688
+ ],
+ [
+ "▁dégag",
+ -13.243918418884277
+ ],
+ [
+ "LAY",
+ -13.243926048278809
+ ],
+ [
+ "behandlung",
+ -13.244172096252441
+ ],
+ [
+ "▁1928",
+ -13.244193077087402
+ ],
+ [
+ "▁Nickel",
+ -13.244205474853516
+ ],
+ [
+ "rophy",
+ -13.244256973266602
+ ],
+ [
+ "▁autonomy",
+ -13.244338989257812
+ ],
+ [
+ "▁Treffen",
+ -13.244402885437012
+ ],
+ [
+ "▁groundbreaking",
+ -13.24445915222168
+ ],
+ [
+ "politisch",
+ -13.244484901428223
+ ],
+ [
+ "▁Vector",
+ -13.244553565979004
+ ],
+ [
+ "oricine",
+ -13.244684219360352
+ ],
+ [
+ "utilisées",
+ -13.244684219360352
+ ],
+ [
+ "plete",
+ -13.244771003723145
+ ],
+ [
+ "droht",
+ -13.244918823242188
+ ],
+ [
+ "▁alternativ",
+ -13.245104789733887
+ ],
+ [
+ "▁Bernie",
+ -13.245213508605957
+ ],
+ [
+ "▁embellish",
+ -13.245260238647461
+ ],
+ [
+ "▁Curriculum",
+ -13.24549674987793
+ ],
+ [
+ "herrscht",
+ -13.245525360107422
+ ],
+ [
+ "escalier",
+ -13.246126174926758
+ ],
+ [
+ "hian",
+ -13.246333122253418
+ ],
+ [
+ "ertaining",
+ -13.246387481689453
+ ],
+ [
+ "hitter",
+ -13.246430397033691
+ ],
+ [
+ "▁kompetente",
+ -13.24665641784668
+ ],
+ [
+ "▁trekking",
+ -13.246760368347168
+ ],
+ [
+ "EACH",
+ -13.246841430664062
+ ],
+ [
+ "▁Bedien",
+ -13.2470703125
+ ],
+ [
+ "starred",
+ -13.247169494628906
+ ],
+ [
+ "▁săptămâna",
+ -13.247236251831055
+ ],
+ [
+ "▁Gratuit",
+ -13.247239112854004
+ ],
+ [
+ "▁Jahrzehnte",
+ -13.247241020202637
+ ],
+ [
+ "ingénieur",
+ -13.24731731414795
+ ],
+ [
+ "▁Huang",
+ -13.24736213684082
+ ],
+ [
+ "Music",
+ -13.247401237487793
+ ],
+ [
+ "misiei",
+ -13.247544288635254
+ ],
+ [
+ "▁masuri",
+ -13.247733116149902
+ ],
+ [
+ "▁Achievement",
+ -13.247817039489746
+ ],
+ [
+ "▁Dorothy",
+ -13.247817039489746
+ ],
+ [
+ "blätter",
+ -13.247817993164062
+ ],
+ [
+ "éloign",
+ -13.247817993164062
+ ],
+ [
+ "▁Anglia",
+ -13.247990608215332
+ ],
+ [
+ "brach",
+ -13.248013496398926
+ ],
+ [
+ "▁Optimization",
+ -13.248085021972656
+ ],
+ [
+ "6.7",
+ -13.248170852661133
+ ],
+ [
+ "winkel",
+ -13.248210906982422
+ ],
+ [
+ "contenan",
+ -13.248347282409668
+ ],
+ [
+ "Astăzi",
+ -13.248398780822754
+ ],
+ [
+ "wiped",
+ -13.248441696166992
+ ],
+ [
+ "granting",
+ -13.248665809631348
+ ],
+ [
+ "▁plăti",
+ -13.248859405517578
+ ],
+ [
+ "▁Compensation",
+ -13.248979568481445
+ ],
+ [
+ "▁Verkäufer",
+ -13.248979568481445
+ ],
+ [
+ "▁angajați",
+ -13.248980522155762
+ ],
+ [
+ "▁diminished",
+ -13.24902057647705
+ ],
+ [
+ "employment",
+ -13.249250411987305
+ ],
+ [
+ "yahoo",
+ -13.249435424804688
+ ],
+ [
+ "▁détrui",
+ -13.249698638916016
+ ],
+ [
+ "▁suffisant",
+ -13.24982738494873
+ ],
+ [
+ "▁Moldovei",
+ -13.250144004821777
+ ],
+ [
+ "▁Pokemon",
+ -13.250144004821777
+ ],
+ [
+ "▁Malcolm",
+ -13.250144958496094
+ ],
+ [
+ "▁mysteries",
+ -13.250147819519043
+ ],
+ [
+ "▁Diversity",
+ -13.250149726867676
+ ],
+ [
+ "▁clinique",
+ -13.250327110290527
+ ],
+ [
+ "landais",
+ -13.250344276428223
+ ],
+ [
+ "▁campanii",
+ -13.250399589538574
+ ],
+ [
+ "▁témoignage",
+ -13.250439643859863
+ ],
+ [
+ "▁paralel",
+ -13.250467300415039
+ ],
+ [
+ "▁travailleurs",
+ -13.250576972961426
+ ],
+ [
+ "▁salvage",
+ -13.250580787658691
+ ],
+ [
+ "▁crayon",
+ -13.250732421875
+ ],
+ [
+ "immédiat",
+ -13.25085163116455
+ ],
+ [
+ "hopped",
+ -13.250958442687988
+ ],
+ [
+ "▁senzor",
+ -13.25102710723877
+ ],
+ [
+ "▁imbunatati",
+ -13.251073837280273
+ ],
+ [
+ "▁capitalize",
+ -13.2511568069458
+ ],
+ [
+ "▁Elephant",
+ -13.25130844116211
+ ],
+ [
+ "▁insomnia",
+ -13.25131607055664
+ ],
+ [
+ "▁Ansicht",
+ -13.251325607299805
+ ],
+ [
+ "▁lupte",
+ -13.251556396484375
+ ],
+ [
+ "▁genomic",
+ -13.251557350158691
+ ],
+ [
+ "▁Grape",
+ -13.251769065856934
+ ],
+ [
+ "MONT",
+ -13.25197982788086
+ ],
+ [
+ "métiers",
+ -13.252004623413086
+ ],
+ [
+ "▁Pierce",
+ -13.252123832702637
+ ],
+ [
+ "consulted",
+ -13.252388954162598
+ ],
+ [
+ "▁Responsible",
+ -13.252474784851074
+ ],
+ [
+ "symmetry",
+ -13.252476692199707
+ ],
+ [
+ "▁sulfur",
+ -13.252487182617188
+ ],
+ [
+ "▁înapoi",
+ -13.252510070800781
+ ],
+ [
+ "▁Junction",
+ -13.252549171447754
+ ],
+ [
+ "▁trilogy",
+ -13.252622604370117
+ ],
+ [
+ "▁unkompliziert",
+ -13.253059387207031
+ ],
+ [
+ "▁zugänglich",
+ -13.253059387207031
+ ],
+ [
+ "▁préfèr",
+ -13.253153800964355
+ ],
+ [
+ "oarelor",
+ -13.253361701965332
+ ],
+ [
+ "langage",
+ -13.253460884094238
+ ],
+ [
+ "admired",
+ -13.253589630126953
+ ],
+ [
+ "platform",
+ -13.253595352172852
+ ],
+ [
+ "▁pluralit",
+ -13.253616333007812
+ ],
+ [
+ "▁betrachtet",
+ -13.253643035888672
+ ],
+ [
+ "▁reproduc",
+ -13.253790855407715
+ ],
+ [
+ "exemple",
+ -13.25385570526123
+ ],
+ [
+ "▁conspir",
+ -13.254347801208496
+ ],
+ [
+ "▁pelvi",
+ -13.25437068939209
+ ],
+ [
+ "leased",
+ -13.254551887512207
+ ],
+ [
+ "▁souffle",
+ -13.254570960998535
+ ],
+ [
+ "▁approprié",
+ -13.254705429077148
+ ],
+ [
+ "absorbing",
+ -13.254817962646484
+ ],
+ [
+ "dividing",
+ -13.254855155944824
+ ],
+ [
+ "herently",
+ -13.255147933959961
+ ],
+ [
+ "▁blister",
+ -13.255179405212402
+ ],
+ [
+ "löst",
+ -13.255182266235352
+ ],
+ [
+ "Apotheke",
+ -13.255398750305176
+ ],
+ [
+ "▁Asociaţi",
+ -13.255424499511719
+ ],
+ [
+ "education",
+ -13.255904197692871
+ ],
+ [
+ "▁retract",
+ -13.255982398986816
+ ],
+ [
+ "▁appraise",
+ -13.255990982055664
+ ],
+ [
+ "▁Debbie",
+ -13.256075859069824
+ ],
+ [
+ "▁arhitect",
+ -13.256193161010742
+ ],
+ [
+ "▁Mohamed",
+ -13.256568908691406
+ ],
+ [
+ "▁îndrept",
+ -13.256568908691406
+ ],
+ [
+ "▁exhaustive",
+ -13.256753921508789
+ ],
+ [
+ "▁Notebook",
+ -13.257004737854004
+ ],
+ [
+ "crashing",
+ -13.257068634033203
+ ],
+ [
+ "▁Betreiber",
+ -13.257155418395996
+ ],
+ [
+ "▁présidentielle",
+ -13.257159233093262
+ ],
+ [
+ "▁Träger",
+ -13.257172584533691
+ ],
+ [
+ "▁noteworthy",
+ -13.257259368896484
+ ],
+ [
+ "▁séparé",
+ -13.257729530334473
+ ],
+ [
+ "▁doppelt",
+ -13.257795333862305
+ ],
+ [
+ "tină",
+ -13.258066177368164
+ ],
+ [
+ "Quelques",
+ -13.258085250854492
+ ],
+ [
+ "culoarea",
+ -13.258100509643555
+ ],
+ [
+ "▁ethic",
+ -13.258166313171387
+ ],
+ [
+ "▁cohesive",
+ -13.258329391479492
+ ],
+ [
+ "▁congratulations",
+ -13.258334159851074
+ ],
+ [
+ "▁sovereignty",
+ -13.25833797454834
+ ],
+ [
+ "▁Aplica",
+ -13.258413314819336
+ ],
+ [
+ "▁Covenant",
+ -13.25851058959961
+ ],
+ [
+ "▁multicultural",
+ -13.258591651916504
+ ],
+ [
+ "assemblée",
+ -13.258955001831055
+ ],
+ [
+ "▁petals",
+ -13.258974075317383
+ ],
+ [
+ "erode",
+ -13.259026527404785
+ ],
+ [
+ "▁porumb",
+ -13.259035110473633
+ ],
+ [
+ "▁Barrier",
+ -13.259050369262695
+ ],
+ [
+ "▁WWE",
+ -13.259085655212402
+ ],
+ [
+ "Etwa",
+ -13.259175300598145
+ ],
+ [
+ "▁recunosc",
+ -13.259271621704102
+ ],
+ [
+ "▁turtle",
+ -13.259415626525879
+ ],
+ [
+ "▁vârf",
+ -13.259444236755371
+ ],
+ [
+ "▁Ranking",
+ -13.259448051452637
+ ],
+ [
+ "▁sympathetic",
+ -13.259514808654785
+ ],
+ [
+ "exploded",
+ -13.2595796585083
+ ],
+ [
+ "▁influenț",
+ -13.259591102600098
+ ],
+ [
+ "▁Fireplace",
+ -13.25972843170166
+ ],
+ [
+ "▁Nachwuchs",
+ -13.260090827941895
+ ],
+ [
+ "▁empfohlen",
+ -13.260090827941895
+ ],
+ [
+ "Voir",
+ -13.260661125183105
+ ],
+ [
+ "▁Vimeo",
+ -13.26069164276123
+ ],
+ [
+ "▁weaving",
+ -13.260967254638672
+ ],
+ [
+ "beneficiar",
+ -13.261198043823242
+ ],
+ [
+ "▁balade",
+ -13.261216163635254
+ ],
+ [
+ "▁Mercy",
+ -13.261566162109375
+ ],
+ [
+ "3.000",
+ -13.26181697845459
+ ],
+ [
+ "Immediately",
+ -13.261857032775879
+ ],
+ [
+ "▁frosting",
+ -13.261868476867676
+ ],
+ [
+ "▁Fiscal",
+ -13.261882781982422
+ ],
+ [
+ "downloadable",
+ -13.26188850402832
+ ],
+ [
+ "▁Hwy",
+ -13.261902809143066
+ ],
+ [
+ "évoluer",
+ -13.261951446533203
+ ],
+ [
+ "▁vieille",
+ -13.2620210647583
+ ],
+ [
+ "heißen",
+ -13.262436866760254
+ ],
+ [
+ "▁étrangère",
+ -13.262446403503418
+ ],
+ [
+ "▁incapable",
+ -13.262490272521973
+ ],
+ [
+ "volunteered",
+ -13.262520790100098
+ ],
+ [
+ "fortunately",
+ -13.262564659118652
+ ],
+ [
+ "company",
+ -13.262738227844238
+ ],
+ [
+ "denkt",
+ -13.2627592086792
+ ],
+ [
+ "▁citesc",
+ -13.262818336486816
+ ],
+ [
+ "▁intrebare",
+ -13.262896537780762
+ ],
+ [
+ "pleasantly",
+ -13.262990951538086
+ ],
+ [
+ "▁Minecraft",
+ -13.263079643249512
+ ],
+ [
+ "▁Schmuck",
+ -13.26308536529541
+ ],
+ [
+ "▁maghiar",
+ -13.263099670410156
+ ],
+ [
+ "conductive",
+ -13.263339042663574
+ ],
+ [
+ "décrit",
+ -13.263534545898438
+ ],
+ [
+ "provide",
+ -13.26353931427002
+ ],
+ [
+ "▁depăş",
+ -13.263628959655762
+ ],
+ [
+ "ituated",
+ -13.263657569885254
+ ],
+ [
+ "▁trumpet",
+ -13.264216423034668
+ ],
+ [
+ "▁nastere",
+ -13.2642240524292
+ ],
+ [
+ "▁Région",
+ -13.264245986938477
+ ],
+ [
+ "Occupational",
+ -13.264411926269531
+ ],
+ [
+ "▁Grecia",
+ -13.264415740966797
+ ],
+ [
+ "▁Conclusion",
+ -13.26449203491211
+ ],
+ [
+ "▁collaborateurs",
+ -13.264927864074707
+ ],
+ [
+ "▁Alibaba",
+ -13.265398025512695
+ ],
+ [
+ "▁amplasat",
+ -13.265398979187012
+ ],
+ [
+ "▁Plastik",
+ -13.265992164611816
+ ],
+ [
+ "▁stash",
+ -13.266023635864258
+ ],
+ [
+ "▁Bonnie",
+ -13.266045570373535
+ ],
+ [
+ "▁ehrlich",
+ -13.266156196594238
+ ],
+ [
+ "▁contention",
+ -13.266193389892578
+ ],
+ [
+ "▁Oslo",
+ -13.266263008117676
+ ],
+ [
+ "englische",
+ -13.266319274902344
+ ],
+ [
+ "measurable",
+ -13.266439437866211
+ ],
+ [
+ "loppy",
+ -13.266470909118652
+ ],
+ [
+ "▁Refrigerat",
+ -13.266579627990723
+ ],
+ [
+ "▁remboursement",
+ -13.266580581665039
+ ],
+ [
+ "▁societăţi",
+ -13.266580581665039
+ ],
+ [
+ "translates",
+ -13.266607284545898
+ ],
+ [
+ "ichtigkeit",
+ -13.266685485839844
+ ],
+ [
+ "agentur",
+ -13.266741752624512
+ ],
+ [
+ "▁compute",
+ -13.266800880432129
+ ],
+ [
+ "berater",
+ -13.266921043395996
+ ],
+ [
+ "▁Georgetown",
+ -13.266945838928223
+ ],
+ [
+ "wolves",
+ -13.266951560974121
+ ],
+ [
+ "ceased",
+ -13.266959190368652
+ ],
+ [
+ "▁Binary",
+ -13.267030715942383
+ ],
+ [
+ "▁kontrolliert",
+ -13.267172813415527
+ ],
+ [
+ "informer",
+ -13.267416000366211
+ ],
+ [
+ "lehrer",
+ -13.267578125
+ ],
+ [
+ "lieferung",
+ -13.267709732055664
+ ],
+ [
+ "▁definit",
+ -13.267742156982422
+ ],
+ [
+ "chèque",
+ -13.267765045166016
+ ],
+ [
+ "▁clergy",
+ -13.267765045166016
+ ],
+ [
+ "▁ministries",
+ -13.267767906188965
+ ],
+ [
+ "▁plague",
+ -13.267779350280762
+ ],
+ [
+ "▁Jedi",
+ -13.267805099487305
+ ],
+ [
+ "▁Blackjack",
+ -13.268025398254395
+ ],
+ [
+ "▁subsection",
+ -13.26807689666748
+ ],
+ [
+ "▁Sachsen",
+ -13.268121719360352
+ ],
+ [
+ "valorile",
+ -13.268146514892578
+ ],
+ [
+ "molded",
+ -13.26816463470459
+ ],
+ [
+ "▁betroffen",
+ -13.268183708190918
+ ],
+ [
+ "▁adecvat",
+ -13.268229484558105
+ ],
+ [
+ "▁collègue",
+ -13.26835823059082
+ ],
+ [
+ "▁chinez",
+ -13.268392562866211
+ ],
+ [
+ "emelle",
+ -13.268695831298828
+ ],
+ [
+ "▁körperliche",
+ -13.268902778625488
+ ],
+ [
+ "▁titan",
+ -13.26891040802002
+ ],
+ [
+ "▁sophistication",
+ -13.268951416015625
+ ],
+ [
+ "▁provoke",
+ -13.268957138061523
+ ],
+ [
+ "▁pensii",
+ -13.269042015075684
+ ],
+ [
+ "▁Tucker",
+ -13.269377708435059
+ ],
+ [
+ "▁motoare",
+ -13.26943302154541
+ ],
+ [
+ "supported",
+ -13.269536972045898
+ ],
+ [
+ "▁Sicil",
+ -13.269697189331055
+ ],
+ [
+ "▁Ausgangs",
+ -13.26987361907959
+ ],
+ [
+ "▁verletzt",
+ -13.269908905029297
+ ],
+ [
+ "Ligue",
+ -13.269996643066406
+ ],
+ [
+ "▁organizatori",
+ -13.270026206970215
+ ],
+ [
+ "▁apprentice",
+ -13.270099639892578
+ ],
+ [
+ "▁Potato",
+ -13.270183563232422
+ ],
+ [
+ "▁Duft",
+ -13.27039623260498
+ ],
+ [
+ "▁medicament",
+ -13.270566940307617
+ ],
+ [
+ "Hôtel",
+ -13.270740509033203
+ ],
+ [
+ "▁Triangle",
+ -13.270842552185059
+ ],
+ [
+ "buted",
+ -13.271100044250488
+ ],
+ [
+ "▁Bentley",
+ -13.271336555480957
+ ],
+ [
+ "următoarele",
+ -13.271389961242676
+ ],
+ [
+ "animate",
+ -13.271404266357422
+ ],
+ [
+ "megapixel",
+ -13.271404266357422
+ ],
+ [
+ "einfachen",
+ -13.271514892578125
+ ],
+ [
+ "▁performanț",
+ -13.271544456481934
+ ],
+ [
+ "lurry",
+ -13.27184009552002
+ ],
+ [
+ "suffisamment",
+ -13.27192211151123
+ ],
+ [
+ "▁Weihnachten",
+ -13.27192211151123
+ ],
+ [
+ "▁Detective",
+ -13.27194595336914
+ ],
+ [
+ "▁lovit",
+ -13.272049903869629
+ ],
+ [
+ "▁blouse",
+ -13.27213191986084
+ ],
+ [
+ "▁hartie",
+ -13.272163391113281
+ ],
+ [
+ "vro",
+ -13.27225112915039
+ ],
+ [
+ "▁disastrous",
+ -13.272517204284668
+ ],
+ [
+ "vermutlich",
+ -13.2725191116333
+ ],
+ [
+ "▁Stafford",
+ -13.272527694702148
+ ],
+ [
+ "ehlt",
+ -13.272628784179688
+ ],
+ [
+ "▁vielseitig",
+ -13.272643089294434
+ ],
+ [
+ "Manifest",
+ -13.273274421691895
+ ],
+ [
+ "homage",
+ -13.27354907989502
+ ],
+ [
+ "menée",
+ -13.273566246032715
+ ],
+ [
+ "▁erläuter",
+ -13.27370834350586
+ ],
+ [
+ "▁volontaire",
+ -13.273709297180176
+ ],
+ [
+ "wrought",
+ -13.27371597290039
+ ],
+ [
+ "▁Naples",
+ -13.273719787597656
+ ],
+ [
+ "recommending",
+ -13.273759841918945
+ ],
+ [
+ "▁thermique",
+ -13.273774147033691
+ ],
+ [
+ "▁subtitle",
+ -13.273787498474121
+ ],
+ [
+ "▁Slam",
+ -13.273809432983398
+ ],
+ [
+ "▁necesitate",
+ -13.273809432983398
+ ],
+ [
+ "trimmed",
+ -13.274099349975586
+ ],
+ [
+ "urmatoarele",
+ -13.274178504943848
+ ],
+ [
+ "▁Sorin",
+ -13.274245262145996
+ ],
+ [
+ "▁compromis",
+ -13.274300575256348
+ ],
+ [
+ "overcoming",
+ -13.274477005004883
+ ],
+ [
+ "▁Samantha",
+ -13.274901390075684
+ ],
+ [
+ "dazzling",
+ -13.27490234375
+ ],
+ [
+ "▁Pearson",
+ -13.274903297424316
+ ],
+ [
+ "▁glazing",
+ -13.274911880493164
+ ],
+ [
+ "Revelation",
+ -13.274921417236328
+ ],
+ [
+ "destinée",
+ -13.275156021118164
+ ],
+ [
+ "öffnet",
+ -13.27515983581543
+ ],
+ [
+ "CERT",
+ -13.275327682495117
+ ],
+ [
+ "▁Sneak",
+ -13.275503158569336
+ ],
+ [
+ "proiectele",
+ -13.275605201721191
+ ],
+ [
+ "▁longitudinal",
+ -13.27609634399414
+ ],
+ [
+ "▁cocaine",
+ -13.276098251342773
+ ],
+ [
+ "▁universitar",
+ -13.276108741760254
+ ],
+ [
+ "▁refreshments",
+ -13.276166915893555
+ ],
+ [
+ "▁instanţ",
+ -13.276243209838867
+ ],
+ [
+ "▁kostenfrei",
+ -13.276397705078125
+ ],
+ [
+ "▁comédie",
+ -13.276451110839844
+ ],
+ [
+ "▁Locat",
+ -13.276725769042969
+ ],
+ [
+ "▁Albania",
+ -13.276732444763184
+ ],
+ [
+ "▁mécanique",
+ -13.276776313781738
+ ],
+ [
+ "messung",
+ -13.27683162689209
+ ],
+ [
+ "issus",
+ -13.277260780334473
+ ],
+ [
+ "pinned",
+ -13.277328491210938
+ ],
+ [
+ "▁sanft",
+ -13.277335166931152
+ ],
+ [
+ "▁geprüft",
+ -13.277435302734375
+ ],
+ [
+ "▁procè",
+ -13.277442932128906
+ ],
+ [
+ "▁Üb",
+ -13.277765274047852
+ ],
+ [
+ "5-0",
+ -13.277802467346191
+ ],
+ [
+ "▁Catering",
+ -13.277957916259766
+ ],
+ [
+ "▁prosperous",
+ -13.27801513671875
+ ],
+ [
+ "▁replication",
+ -13.278098106384277
+ ],
+ [
+ "▁obese",
+ -13.278441429138184
+ ],
+ [
+ "clerosis",
+ -13.278489112854004
+ ],
+ [
+ "▁Carnegie",
+ -13.278489112854004
+ ],
+ [
+ "▁Incredible",
+ -13.278489112854004
+ ],
+ [
+ "▁Teppich",
+ -13.278489112854004
+ ],
+ [
+ "▁crunchy",
+ -13.278489112854004
+ ],
+ [
+ "▁vomiting",
+ -13.278529167175293
+ ],
+ [
+ "▁sourire",
+ -13.278619766235352
+ ],
+ [
+ "publish",
+ -13.278948783874512
+ ],
+ [
+ "▁exterioar",
+ -13.279094696044922
+ ],
+ [
+ "▁forehead",
+ -13.279107093811035
+ ],
+ [
+ "▁climatique",
+ -13.279313087463379
+ ],
+ [
+ "▁conservator",
+ -13.279458999633789
+ ],
+ [
+ "▁Russland",
+ -13.279687881469727
+ ],
+ [
+ "▁kombiniert",
+ -13.279687881469727
+ ],
+ [
+ "▁Thrones",
+ -13.279688835144043
+ ],
+ [
+ "▁Griffith",
+ -13.27968978881836
+ ],
+ [
+ "▁fragrant",
+ -13.279695510864258
+ ],
+ [
+ "▁RSVP",
+ -13.279698371887207
+ ],
+ [
+ "klima",
+ -13.279751777648926
+ ],
+ [
+ "▁situație",
+ -13.279808044433594
+ ],
+ [
+ "deschiderea",
+ -13.280009269714355
+ ],
+ [
+ "▁moale",
+ -13.280033111572266
+ ],
+ [
+ "▁Trevor",
+ -13.280112266540527
+ ],
+ [
+ "ménager",
+ -13.28011417388916
+ ],
+ [
+ "deploying",
+ -13.280428886413574
+ ],
+ [
+ "▁Loft",
+ -13.280500411987305
+ ],
+ [
+ "▁Willkommen",
+ -13.28059196472168
+ ],
+ [
+ "▁Bezirks",
+ -13.280887603759766
+ ],
+ [
+ "▁Himself",
+ -13.280975341796875
+ ],
+ [
+ "▁quarant",
+ -13.28101634979248
+ ],
+ [
+ "▁1901",
+ -13.281079292297363
+ ],
+ [
+ "▁tripod",
+ -13.28136920928955
+ ],
+ [
+ "▁récolt",
+ -13.281553268432617
+ ],
+ [
+ "natură",
+ -13.281631469726562
+ ],
+ [
+ "School",
+ -13.281649589538574
+ ],
+ [
+ "contested",
+ -13.281773567199707
+ ],
+ [
+ "bwohl",
+ -13.281784057617188
+ ],
+ [
+ "Darren",
+ -13.281830787658691
+ ],
+ [
+ "medicine",
+ -13.281903266906738
+ ],
+ [
+ "▁Impuls",
+ -13.282041549682617
+ ],
+ [
+ "prevailing",
+ -13.282057762145996
+ ],
+ [
+ "▁orthodontic",
+ -13.282089233398438
+ ],
+ [
+ "▁sequential",
+ -13.282089233398438
+ ],
+ [
+ "▁Kolkata",
+ -13.28209114074707
+ ],
+ [
+ "▁séch",
+ -13.282100677490234
+ ],
+ [
+ "▁diaper",
+ -13.28212833404541
+ ],
+ [
+ "▁simplifie",
+ -13.282144546508789
+ ],
+ [
+ "▁reflux",
+ -13.282163619995117
+ ],
+ [
+ "▁Hypo",
+ -13.282242774963379
+ ],
+ [
+ "imprimer",
+ -13.282251358032227
+ ],
+ [
+ "▁Folosi",
+ -13.282401084899902
+ ],
+ [
+ "Info",
+ -13.282570838928223
+ ],
+ [
+ "▁Investiga",
+ -13.282801628112793
+ ],
+ [
+ "stabilirea",
+ -13.282845497131348
+ ],
+ [
+ "élis",
+ -13.283149719238281
+ ],
+ [
+ "ccessed",
+ -13.28320026397705
+ ],
+ [
+ "▁recyclable",
+ -13.283293724060059
+ ],
+ [
+ "▁forbidden",
+ -13.283295631408691
+ ],
+ [
+ "▁Colonel",
+ -13.283297538757324
+ ],
+ [
+ "▁nisip",
+ -13.28330135345459
+ ],
+ [
+ "▁Fundamental",
+ -13.283303260803223
+ ],
+ [
+ "▁nouveauté",
+ -13.283308029174805
+ ],
+ [
+ "khi",
+ -13.283357620239258
+ ],
+ [
+ "▁ecology",
+ -13.28339672088623
+ ],
+ [
+ "▁filament",
+ -13.283540725708008
+ ],
+ [
+ "▁relentless",
+ -13.283559799194336
+ ],
+ [
+ "▁Behavior",
+ -13.283669471740723
+ ],
+ [
+ "titulaire",
+ -13.283900260925293
+ ],
+ [
+ "▁administrativ",
+ -13.28404426574707
+ ],
+ [
+ "▁Vorlage",
+ -13.284209251403809
+ ],
+ [
+ "zeigte",
+ -13.28427791595459
+ ],
+ [
+ "▁Bäume",
+ -13.284497261047363
+ ],
+ [
+ "▁Kartoffel",
+ -13.284497261047363
+ ],
+ [
+ "▁Possible",
+ -13.284500122070312
+ ],
+ [
+ "▁perturb",
+ -13.28466510772705
+ ],
+ [
+ "▁Grigor",
+ -13.284717559814453
+ ],
+ [
+ "▁streng",
+ -13.284759521484375
+ ],
+ [
+ "▁vânzare",
+ -13.285101890563965
+ ],
+ [
+ "concentrating",
+ -13.285698890686035
+ ],
+ [
+ "▁rechtzeitig",
+ -13.2857027053833
+ ],
+ [
+ "▁eternity",
+ -13.28570556640625
+ ],
+ [
+ "▁Puzzle",
+ -13.28575611114502
+ ],
+ [
+ "▁malade",
+ -13.285775184631348
+ ],
+ [
+ "▁Metallic",
+ -13.285776138305664
+ ],
+ [
+ "▁Unterhaltung",
+ -13.285783767700195
+ ],
+ [
+ "▁4:00",
+ -13.285820960998535
+ ],
+ [
+ "▁magique",
+ -13.285908699035645
+ ],
+ [
+ "▁cellphone",
+ -13.285975456237793
+ ],
+ [
+ "▁inhibition",
+ -13.286023139953613
+ ],
+ [
+ "▁remplacement",
+ -13.286025047302246
+ ],
+ [
+ "▁WWII",
+ -13.286089897155762
+ ],
+ [
+ "Eff",
+ -13.286258697509766
+ ],
+ [
+ "kontakt",
+ -13.286832809448242
+ ],
+ [
+ "Update",
+ -13.286869049072266
+ ],
+ [
+ "▁Emerald",
+ -13.286910057067871
+ ],
+ [
+ "▁hammock",
+ -13.286910057067871
+ ],
+ [
+ "POWER",
+ -13.286917686462402
+ ],
+ [
+ "automne",
+ -13.286917686462402
+ ],
+ [
+ "▁(2004)",
+ -13.286961555480957
+ ],
+ [
+ "▁participanți",
+ -13.287012100219727
+ ],
+ [
+ "1998)",
+ -13.287014961242676
+ ],
+ [
+ "▁deletion",
+ -13.287186622619629
+ ],
+ [
+ "▁Proiect",
+ -13.287226676940918
+ ],
+ [
+ "IDENT",
+ -13.287504196166992
+ ],
+ [
+ "▁precis",
+ -13.287623405456543
+ ],
+ [
+ "▁limp",
+ -13.287676811218262
+ ],
+ [
+ "▁Pompe",
+ -13.287686347961426
+ ],
+ [
+ "▁ménage",
+ -13.28780746459961
+ ],
+ [
+ "▁Wahrheit",
+ -13.288119316101074
+ ],
+ [
+ "▁Intelligent",
+ -13.28812026977539
+ ],
+ [
+ "▁instability",
+ -13.2881441116333
+ ],
+ [
+ "insurance",
+ -13.288346290588379
+ ],
+ [
+ "▁Nursery",
+ -13.288352966308594
+ ],
+ [
+ "▁synonym",
+ -13.288427352905273
+ ],
+ [
+ "▁ignite",
+ -13.28848934173584
+ ],
+ [
+ "▁Vernon",
+ -13.28849983215332
+ ],
+ [
+ "purchase",
+ -13.288524627685547
+ ],
+ [
+ "▁disponibilité",
+ -13.288662910461426
+ ],
+ [
+ "▁producţi",
+ -13.28909969329834
+ ],
+ [
+ "▁Pentagon",
+ -13.289329528808594
+ ],
+ [
+ "▁illumination",
+ -13.289329528808594
+ ],
+ [
+ "▁obsolete",
+ -13.289329528808594
+ ],
+ [
+ "▁unacceptable",
+ -13.28933048248291
+ ],
+ [
+ "Gleichzeitig",
+ -13.289938926696777
+ ],
+ [
+ "rutsch",
+ -13.290071487426758
+ ],
+ [
+ "viziuni",
+ -13.290409088134766
+ ],
+ [
+ "▁Nicaragua",
+ -13.29054069519043
+ ],
+ [
+ "▁hesitation",
+ -13.290541648864746
+ ],
+ [
+ "▁nascut",
+ -13.290545463562012
+ ],
+ [
+ "▁Warehouse",
+ -13.29055404663086
+ ],
+ [
+ "geboten",
+ -13.290558815002441
+ ],
+ [
+ "▁Lagos",
+ -13.290844917297363
+ ],
+ [
+ "produced",
+ -13.290874481201172
+ ],
+ [
+ "cativa",
+ -13.291309356689453
+ ],
+ [
+ "▁Tracy",
+ -13.291326522827148
+ ],
+ [
+ "Projekt",
+ -13.291468620300293
+ ],
+ [
+ "▁malaria",
+ -13.291692733764648
+ ],
+ [
+ "▁Baldwin",
+ -13.291755676269531
+ ],
+ [
+ "Take",
+ -13.291791915893555
+ ],
+ [
+ "▁fluctuations",
+ -13.291844367980957
+ ],
+ [
+ "▁titular",
+ -13.29194450378418
+ ],
+ [
+ "bmw",
+ -13.291976928710938
+ ],
+ [
+ "▁brevet",
+ -13.29202651977539
+ ],
+ [
+ "étapes",
+ -13.292173385620117
+ ],
+ [
+ "wikipedia",
+ -13.292373657226562
+ ],
+ [
+ "▁corporal",
+ -13.292424201965332
+ ],
+ [
+ "▁Schönheit",
+ -13.2926664352417
+ ],
+ [
+ "utilizatorii",
+ -13.292695999145508
+ ],
+ [
+ "INFO",
+ -13.292807579040527
+ ],
+ [
+ "▁formularul",
+ -13.292900085449219
+ ],
+ [
+ "femi",
+ -13.292959213256836
+ ],
+ [
+ "Konferenz",
+ -13.29296875
+ ],
+ [
+ "▁carnival",
+ -13.29296875
+ ],
+ [
+ "▁Kräuter",
+ -13.292969703674316
+ ],
+ [
+ "▁gelernt",
+ -13.292981147766113
+ ],
+ [
+ "▁Sherman",
+ -13.293017387390137
+ ],
+ [
+ "▁persistence",
+ -13.293289184570312
+ ],
+ [
+ "▁Behörden",
+ -13.293577194213867
+ ],
+ [
+ "▁Frühjahr",
+ -13.293578147888184
+ ],
+ [
+ "▁Guvern",
+ -13.293649673461914
+ ],
+ [
+ "interpreting",
+ -13.293878555297852
+ ],
+ [
+ "▁nommé",
+ -13.294021606445312
+ ],
+ [
+ "consult",
+ -13.294035911560059
+ ],
+ [
+ "▁obligaţi",
+ -13.294184684753418
+ ],
+ [
+ "▁Newspaper",
+ -13.2942476272583
+ ],
+ [
+ "(2005)",
+ -13.294515609741211
+ ],
+ [
+ "pumped",
+ -13.294614791870117
+ ],
+ [
+ "▁autoritati",
+ -13.294634819030762
+ ],
+ [
+ "▁aplicatii",
+ -13.294644355773926
+ ],
+ [
+ "▁verhindert",
+ -13.294794082641602
+ ],
+ [
+ "▁évident",
+ -13.294794082641602
+ ],
+ [
+ "▁getrennt",
+ -13.294795036315918
+ ],
+ [
+ "▁Encourage",
+ -13.295403480529785
+ ],
+ [
+ "▁lurk",
+ -13.295432090759277
+ ],
+ [
+ "▁condemned",
+ -13.295455932617188
+ ],
+ [
+ "▁4:30",
+ -13.295502662658691
+ ],
+ [
+ "labelled",
+ -13.29576587677002
+ ],
+ [
+ "ordinea",
+ -13.295899391174316
+ ],
+ [
+ "▁pantofi",
+ -13.296012878417969
+ ],
+ [
+ "Default",
+ -13.296042442321777
+ ],
+ [
+ "▁beruh",
+ -13.296120643615723
+ ],
+ [
+ "/01/",
+ -13.296268463134766
+ ],
+ [
+ "league",
+ -13.296503067016602
+ ],
+ [
+ "▁couvert",
+ -13.296524047851562
+ ],
+ [
+ "▁competencies",
+ -13.296622276306152
+ ],
+ [
+ "▁mozzarella",
+ -13.296622276306152
+ ],
+ [
+ "jihad",
+ -13.29662799835205
+ ],
+ [
+ "▁gossip",
+ -13.29662799835205
+ ],
+ [
+ "▁Omaha",
+ -13.296628952026367
+ ],
+ [
+ "▁coincidence",
+ -13.296669960021973
+ ],
+ [
+ "▁Pinot",
+ -13.296710968017578
+ ],
+ [
+ "dotted",
+ -13.296789169311523
+ ],
+ [
+ "schilder",
+ -13.297197341918945
+ ],
+ [
+ "▁Munte",
+ -13.297224998474121
+ ],
+ [
+ "▁Vermieter",
+ -13.297232627868652
+ ],
+ [
+ "▁britannique",
+ -13.297232627868652
+ ],
+ [
+ "▁comentariu",
+ -13.297235488891602
+ ],
+ [
+ "abonnement",
+ -13.29725456237793
+ ],
+ [
+ "▁inventive",
+ -13.29727840423584
+ ],
+ [
+ "complie",
+ -13.297279357910156
+ ],
+ [
+ "composée",
+ -13.29734992980957
+ ],
+ [
+ "▁glatt",
+ -13.297684669494629
+ ],
+ [
+ "adorned",
+ -13.297842979431152
+ ],
+ [
+ "▁Opportunities",
+ -13.297842979431152
+ ],
+ [
+ "▁equilibrium",
+ -13.297842979431152
+ ],
+ [
+ "▁persuasive",
+ -13.297842979431152
+ ],
+ [
+ "▁achiziţi",
+ -13.297843933105469
+ ],
+ [
+ "▁déterminer",
+ -13.297843933105469
+ ],
+ [
+ "▁fleece",
+ -13.297857284545898
+ ],
+ [
+ "▁ivory",
+ -13.29786205291748
+ ],
+ [
+ "▁Genuss",
+ -13.297900199890137
+ ],
+ [
+ "Thousands",
+ -13.297930717468262
+ ],
+ [
+ "▁izolat",
+ -13.297965049743652
+ ],
+ [
+ "▁symbolize",
+ -13.298033714294434
+ ],
+ [
+ "gâteau",
+ -13.298051834106445
+ ],
+ [
+ "▁relații",
+ -13.298062324523926
+ ],
+ [
+ "▁Classroom",
+ -13.298144340515137
+ ],
+ [
+ "settlers",
+ -13.298155784606934
+ ],
+ [
+ "▁vremuri",
+ -13.298195838928223
+ ],
+ [
+ "▁Serial",
+ -13.29838752746582
+ ],
+ [
+ "▁boite",
+ -13.298399925231934
+ ],
+ [
+ "équivalent",
+ -13.298453330993652
+ ],
+ [
+ "▁benutzen",
+ -13.298454284667969
+ ],
+ [
+ "▁Recomand",
+ -13.298462867736816
+ ],
+ [
+ "▁Sinai",
+ -13.298968315124512
+ ],
+ [
+ "▁Advertise",
+ -13.29906940460205
+ ],
+ [
+ "▁Thermal",
+ -13.299206733703613
+ ],
+ [
+ "fiance",
+ -13.299471855163574
+ ],
+ [
+ "▁universitaire",
+ -13.299683570861816
+ ],
+ [
+ "▁rivière",
+ -13.299793243408203
+ ],
+ [
+ "▁reimburse",
+ -13.299907684326172
+ ],
+ [
+ "ţara",
+ -13.299932479858398
+ ],
+ [
+ "tician",
+ -13.30002498626709
+ ],
+ [
+ "intelligence",
+ -13.300041198730469
+ ],
+ [
+ "▁abgestimmt",
+ -13.300288200378418
+ ],
+ [
+ "▁compliqué",
+ -13.300288200378418
+ ],
+ [
+ "▁succulent",
+ -13.300297737121582
+ ],
+ [
+ "opéra",
+ -13.300395011901855
+ ],
+ [
+ "7-9",
+ -13.300456047058105
+ ],
+ [
+ "▁pierderi",
+ -13.300654411315918
+ ],
+ [
+ "extinction",
+ -13.30090045928955
+ ],
+ [
+ "▁Zweifel",
+ -13.30103874206543
+ ],
+ [
+ "ATCH",
+ -13.30112361907959
+ ],
+ [
+ "10,000",
+ -13.301222801208496
+ ],
+ [
+ "▁uninterrupted",
+ -13.301513671875
+ ],
+ [
+ "▁Eigentum",
+ -13.301517486572266
+ ],
+ [
+ "▁Utility",
+ -13.301517486572266
+ ],
+ [
+ "ско",
+ -13.301529884338379
+ ],
+ [
+ "▁tornado",
+ -13.301544189453125
+ ],
+ [
+ "▁Güte",
+ -13.301727294921875
+ ],
+ [
+ "▁pertain",
+ -13.301923751831055
+ ],
+ [
+ "painters",
+ -13.301993370056152
+ ],
+ [
+ "Help",
+ -13.3021240234375
+ ],
+ [
+ "▁străinătate",
+ -13.30212688446045
+ ],
+ [
+ "▁stammen",
+ -13.302170753479004
+ ],
+ [
+ "opposition",
+ -13.302229881286621
+ ],
+ [
+ "▁rhino",
+ -13.302233695983887
+ ],
+ [
+ "intervenir",
+ -13.302427291870117
+ ],
+ [
+ "▁hyperlink",
+ -13.302441596984863
+ ],
+ [
+ "höchst",
+ -13.302518844604492
+ ],
+ [
+ "roach",
+ -13.302627563476562
+ ],
+ [
+ "wSt",
+ -13.302687644958496
+ ],
+ [
+ "▁monastery",
+ -13.302740097045898
+ ],
+ [
+ "▁algae",
+ -13.302754402160645
+ ],
+ [
+ "▁shaving",
+ -13.302757263183594
+ ],
+ [
+ "présentent",
+ -13.302804946899414
+ ],
+ [
+ "Africa",
+ -13.302860260009766
+ ],
+ [
+ "eigener",
+ -13.303047180175781
+ ],
+ [
+ "▁glace",
+ -13.303153991699219
+ ],
+ [
+ "▁discurs",
+ -13.303179740905762
+ ],
+ [
+ "▁autograph",
+ -13.303204536437988
+ ],
+ [
+ "▁Conflict",
+ -13.303359031677246
+ ],
+ [
+ "▁școli",
+ -13.303411483764648
+ ],
+ [
+ "▁excerpt",
+ -13.303617477416992
+ ],
+ [
+ "correlated",
+ -13.303628921508789
+ ],
+ [
+ "empel",
+ -13.303841590881348
+ ],
+ [
+ "cryptocurrencies",
+ -13.30396842956543
+ ],
+ [
+ "▁symposium",
+ -13.30396842956543
+ ],
+ [
+ "▁gewohnt",
+ -13.303994178771973
+ ],
+ [
+ "PTSD",
+ -13.304070472717285
+ ],
+ [
+ "▁harmonic",
+ -13.304166793823242
+ ],
+ [
+ "discarded",
+ -13.304282188415527
+ ],
+ [
+ "▁Flint",
+ -13.304359436035156
+ ],
+ [
+ "Russia",
+ -13.304422378540039
+ ],
+ [
+ "▁ședinț",
+ -13.304583549499512
+ ],
+ [
+ "▁accusations",
+ -13.304727554321289
+ ],
+ [
+ "▁încălc",
+ -13.304827690124512
+ ],
+ [
+ "sendung",
+ -13.305152893066406
+ ],
+ [
+ "▁Chiropractic",
+ -13.305197715759277
+ ],
+ [
+ "▁excepți",
+ -13.305201530456543
+ ],
+ [
+ "▁proclaim",
+ -13.305201530456543
+ ],
+ [
+ "▁Flexible",
+ -13.305295944213867
+ ],
+ [
+ "▁Hüt",
+ -13.30538272857666
+ ],
+ [
+ "▁Baltic",
+ -13.30539608001709
+ ],
+ [
+ "▁inaltime",
+ -13.30553913116455
+ ],
+ [
+ "▁montré",
+ -13.305868148803711
+ ],
+ [
+ "exécution",
+ -13.305898666381836
+ ],
+ [
+ "partei",
+ -13.305961608886719
+ ],
+ [
+ "▁specifie",
+ -13.306072235107422
+ ],
+ [
+ "▁Jackpot",
+ -13.306105613708496
+ ],
+ [
+ "▁stumble",
+ -13.306134223937988
+ ],
+ [
+ "▁individuel",
+ -13.306161880493164
+ ],
+ [
+ "▁Veteran",
+ -13.306217193603516
+ ],
+ [
+ "▁Supplies",
+ -13.306428909301758
+ ],
+ [
+ "▁excavation",
+ -13.306428909301758
+ ],
+ [
+ "▁Libraries",
+ -13.306469917297363
+ ],
+ [
+ "▁prénom",
+ -13.306476593017578
+ ],
+ [
+ "WOOD",
+ -13.30650806427002
+ ],
+ [
+ "meciul",
+ -13.306917190551758
+ ],
+ [
+ "Chef",
+ -13.306938171386719
+ ],
+ [
+ "▁SUPER",
+ -13.306940078735352
+ ],
+ [
+ "Appeals",
+ -13.30696964263916
+ ],
+ [
+ "terapia",
+ -13.307113647460938
+ ],
+ [
+ "▁relatii",
+ -13.30713939666748
+ ],
+ [
+ "modifying",
+ -13.30748462677002
+ ],
+ [
+ "▁Regulament",
+ -13.307662010192871
+ ],
+ [
+ "▁bănci",
+ -13.307662963867188
+ ],
+ [
+ "▁agility",
+ -13.307666778564453
+ ],
+ [
+ "▁Magnetic",
+ -13.307674407958984
+ ],
+ [
+ "▁piatra",
+ -13.30767822265625
+ ],
+ [
+ "▁Governance",
+ -13.307680130004883
+ ],
+ [
+ "▁clown",
+ -13.30772876739502
+ ],
+ [
+ "▁Choir",
+ -13.308337211608887
+ ],
+ [
+ "aujourd",
+ -13.308548927307129
+ ],
+ [
+ "▁vendeur",
+ -13.308732032775879
+ ],
+ [
+ "ndererseits",
+ -13.308859825134277
+ ],
+ [
+ "▁Bahrain",
+ -13.3088960647583
+ ],
+ [
+ "▁Timisoara",
+ -13.3088960647583
+ ],
+ [
+ "▁exklusive",
+ -13.3088960647583
+ ],
+ [
+ "▁Population",
+ -13.309001922607422
+ ],
+ [
+ "▁nepo",
+ -13.309073448181152
+ ],
+ [
+ "▁relish",
+ -13.309085845947266
+ ],
+ [
+ "▁Pumpkin",
+ -13.309571266174316
+ ],
+ [
+ "▁détente",
+ -13.309784889221191
+ ],
+ [
+ "▁episcop",
+ -13.309860229492188
+ ],
+ [
+ "patterned",
+ -13.309929847717285
+ ],
+ [
+ "▁THANK",
+ -13.310132026672363
+ ],
+ [
+ "▁Widerspruch",
+ -13.310132026672363
+ ],
+ [
+ "▁Crisis",
+ -13.310189247131348
+ ],
+ [
+ "▁goose",
+ -13.310226440429688
+ ],
+ [
+ "▁couture",
+ -13.310307502746582
+ ],
+ [
+ "▁hinweg",
+ -13.310446739196777
+ ],
+ [
+ "supplemental",
+ -13.310486793518066
+ ],
+ [
+ "shingles",
+ -13.31060791015625
+ ],
+ [
+ "investir",
+ -13.310635566711426
+ ],
+ [
+ "▁steriliz",
+ -13.310759544372559
+ ],
+ [
+ "tractors",
+ -13.310761451721191
+ ],
+ [
+ "cellules",
+ -13.31078815460205
+ ],
+ [
+ "▁Gloria",
+ -13.310888290405273
+ ],
+ [
+ "▁teilnehmen",
+ -13.311092376708984
+ ],
+ [
+ "companiile",
+ -13.311248779296875
+ ],
+ [
+ "surfacing",
+ -13.311279296875
+ ],
+ [
+ "▁nostalgic",
+ -13.311368942260742
+ ],
+ [
+ "▁Badezimmer",
+ -13.311369895935059
+ ],
+ [
+ "▁conjoint",
+ -13.311370849609375
+ ],
+ [
+ "vacancy",
+ -13.31145191192627
+ ],
+ [
+ "▁homeland",
+ -13.311582565307617
+ ],
+ [
+ "▁Abschnitt",
+ -13.311625480651855
+ ],
+ [
+ "Cartea",
+ -13.311653137207031
+ ],
+ [
+ "SIA",
+ -13.311782836914062
+ ],
+ [
+ "▁explode",
+ -13.311786651611328
+ ],
+ [
+ "fostering",
+ -13.311959266662598
+ ],
+ [
+ "▁ceilalti",
+ -13.31198787689209
+ ],
+ [
+ "▁gentil",
+ -13.31214714050293
+ ],
+ [
+ "oplasty",
+ -13.31218433380127
+ ],
+ [
+ "bodied",
+ -13.312424659729004
+ ],
+ [
+ "▁1906",
+ -13.312499046325684
+ ],
+ [
+ "▁BlackBerry",
+ -13.312607765197754
+ ],
+ [
+ "▁Presbyterian",
+ -13.312607765197754
+ ],
+ [
+ "▁berücksichtigt",
+ -13.312607765197754
+ ],
+ [
+ "▁compartiment",
+ -13.312607765197754
+ ],
+ [
+ "▁compulsory",
+ -13.312607765197754
+ ],
+ [
+ "Millennial",
+ -13.312609672546387
+ ],
+ [
+ "▁sanitar",
+ -13.312638282775879
+ ],
+ [
+ "▁stink",
+ -13.312975883483887
+ ],
+ [
+ "lius",
+ -13.313047409057617
+ ],
+ [
+ "thankfully",
+ -13.313136100769043
+ ],
+ [
+ "modalité",
+ -13.313173294067383
+ ],
+ [
+ "▁cunoaște",
+ -13.313226699829102
+ ],
+ [
+ "Infrastruktur",
+ -13.313227653503418
+ ],
+ [
+ "▁studenți",
+ -13.313253402709961
+ ],
+ [
+ "Bref",
+ -13.313270568847656
+ ],
+ [
+ "London",
+ -13.31360149383545
+ ],
+ [
+ "▁Arduino",
+ -13.313847541809082
+ ],
+ [
+ "▁cilantro",
+ -13.313847541809082
+ ],
+ [
+ "▁Rafael",
+ -13.313848495483398
+ ],
+ [
+ "▁untersucht",
+ -13.313861846923828
+ ],
+ [
+ "▁martyr",
+ -13.31389331817627
+ ],
+ [
+ "▁Mormon",
+ -13.313984870910645
+ ],
+ [
+ "▁wicket",
+ -13.313996315002441
+ ],
+ [
+ "cherished",
+ -13.314335823059082
+ ],
+ [
+ "liquid",
+ -13.314417839050293
+ ],
+ [
+ "▁dorinț",
+ -13.314571380615234
+ ],
+ [
+ "lehnt",
+ -13.314717292785645
+ ],
+ [
+ "meisterschaft",
+ -13.31493091583252
+ ],
+ [
+ "fondateur",
+ -13.314971923828125
+ ],
+ [
+ "câble",
+ -13.315078735351562
+ ],
+ [
+ "▁erreichbar",
+ -13.315091133117676
+ ],
+ [
+ "▁footsteps",
+ -13.315094947814941
+ ],
+ [
+ "▁Kloster",
+ -13.31519889831543
+ ],
+ [
+ "▁multiplayer",
+ -13.315218925476074
+ ],
+ [
+ "▁substitu",
+ -13.315276145935059
+ ],
+ [
+ "▁Frisch",
+ -13.315526962280273
+ ],
+ [
+ "▁arsenal",
+ -13.315712928771973
+ ],
+ [
+ "explication",
+ -13.315866470336914
+ ],
+ [
+ "▁conexiun",
+ -13.315986633300781
+ ],
+ [
+ "muddy",
+ -13.316045761108398
+ ],
+ [
+ "▁Reifen",
+ -13.316120147705078
+ ],
+ [
+ "auraient",
+ -13.316132545471191
+ ],
+ [
+ "▁biologic",
+ -13.316136360168457
+ ],
+ [
+ "▁acquainted",
+ -13.316332817077637
+ ],
+ [
+ "▁shelving",
+ -13.316341400146484
+ ],
+ [
+ "Stunning",
+ -13.316373825073242
+ ],
+ [
+ "▁Clothing",
+ -13.316394805908203
+ ],
+ [
+ "▁kidding",
+ -13.316431999206543
+ ],
+ [
+ "excellent",
+ -13.316452026367188
+ ],
+ [
+ "▁susțin",
+ -13.316487312316895
+ ],
+ [
+ "bătut",
+ -13.316502571105957
+ ],
+ [
+ "elusive",
+ -13.3165283203125
+ ],
+ [
+ "werbung",
+ -13.316743850708008
+ ],
+ [
+ "slipping",
+ -13.316813468933105
+ ],
+ [
+ "▁configura",
+ -13.316926956176758
+ ],
+ [
+ "▁proaspat",
+ -13.31695556640625
+ ],
+ [
+ "▁apporté",
+ -13.317120552062988
+ ],
+ [
+ "▁démarr",
+ -13.317328453063965
+ ],
+ [
+ "Spezialist",
+ -13.317578315734863
+ ],
+ [
+ "▁obligați",
+ -13.317578315734863
+ ],
+ [
+ "▁societăți",
+ -13.317578315734863
+ ],
+ [
+ "▁malpractice",
+ -13.31757926940918
+ ],
+ [
+ "Hundreds",
+ -13.317609786987305
+ ],
+ [
+ "▁3:1",
+ -13.318138122558594
+ ],
+ [
+ "▁computation",
+ -13.31817626953125
+ ],
+ [
+ "▁Heilig",
+ -13.318528175354004
+ ],
+ [
+ "▁Helsinki",
+ -13.318824768066406
+ ],
+ [
+ "▁firefighters",
+ -13.318824768066406
+ ],
+ [
+ "▁obedience",
+ -13.318824768066406
+ ],
+ [
+ "▁evacuate",
+ -13.318825721740723
+ ],
+ [
+ "▁Floyd",
+ -13.318840026855469
+ ],
+ [
+ "▁Disneyland",
+ -13.318859100341797
+ ],
+ [
+ "Cathy",
+ -13.319069862365723
+ ],
+ [
+ "▁Broken",
+ -13.319278717041016
+ ],
+ [
+ "cript",
+ -13.319952011108398
+ ],
+ [
+ "▁Gewähr",
+ -13.320073127746582
+ ],
+ [
+ "▁embarrassed",
+ -13.320073127746582
+ ],
+ [
+ "▁Leicht",
+ -13.32007884979248
+ ],
+ [
+ "▁témoign",
+ -13.320379257202148
+ ],
+ [
+ "▁viteze",
+ -13.3206148147583
+ ],
+ [
+ "▁hallmark",
+ -13.320731163024902
+ ],
+ [
+ "uploads",
+ -13.32082462310791
+ ],
+ [
+ "▁Submission",
+ -13.320929527282715
+ ],
+ [
+ "▁croissant",
+ -13.321049690246582
+ ],
+ [
+ "awning",
+ -13.32105827331543
+ ],
+ [
+ "detecting",
+ -13.321198463439941
+ ],
+ [
+ "▁Bahamas",
+ -13.321322441101074
+ ],
+ [
+ "▁Kathleen",
+ -13.321325302124023
+ ],
+ [
+ "▁latch",
+ -13.321377754211426
+ ],
+ [
+ "▁pronounce",
+ -13.321380615234375
+ ],
+ [
+ "▁choke",
+ -13.321428298950195
+ ],
+ [
+ "▁$50,000",
+ -13.3215970993042
+ ],
+ [
+ "▁historische",
+ -13.321642875671387
+ ],
+ [
+ "jugé",
+ -13.321829795837402
+ ],
+ [
+ "▁MasterCard",
+ -13.321949005126953
+ ],
+ [
+ "▁Horror",
+ -13.321955680847168
+ ],
+ [
+ "spoiled",
+ -13.321958541870117
+ ],
+ [
+ "▁apariți",
+ -13.32202434539795
+ ],
+ [
+ "geschaltet",
+ -13.3225736618042
+ ],
+ [
+ "▁Londra",
+ -13.322578430175781
+ ],
+ [
+ "viction",
+ -13.322580337524414
+ ],
+ [
+ "▁Disaster",
+ -13.322593688964844
+ ],
+ [
+ "▁desigur",
+ -13.322601318359375
+ ],
+ [
+ "▁substanț",
+ -13.322601318359375
+ ],
+ [
+ "▁compiler",
+ -13.322613716125488
+ ],
+ [
+ "▁vanzari",
+ -13.32262897491455
+ ],
+ [
+ "▁Simulation",
+ -13.322669982910156
+ ],
+ [
+ "Occasionally",
+ -13.322842597961426
+ ],
+ [
+ "Seite",
+ -13.322884559631348
+ ],
+ [
+ "Linked",
+ -13.322938919067383
+ ],
+ [
+ "Roll",
+ -13.323015213012695
+ ],
+ [
+ "▁trajet",
+ -13.323244094848633
+ ],
+ [
+ "Molecular",
+ -13.323834419250488
+ ],
+ [
+ "▁pragmatic",
+ -13.323843002319336
+ ],
+ [
+ "judecată",
+ -13.323915481567383
+ ],
+ [
+ "ров",
+ -13.32400894165039
+ ],
+ [
+ "serrurerie",
+ -13.324024200439453
+ ],
+ [
+ "▁reconstruct",
+ -13.324129104614258
+ ],
+ [
+ "▁heureuse",
+ -13.324179649353027
+ ],
+ [
+ "▁knight",
+ -13.32422924041748
+ ],
+ [
+ "knowingly",
+ -13.324431419372559
+ ],
+ [
+ "▁perspectiva",
+ -13.324453353881836
+ ],
+ [
+ "ordinary",
+ -13.324604034423828
+ ],
+ [
+ "▁chaudière",
+ -13.324721336364746
+ ],
+ [
+ "Neill",
+ -13.324727058410645
+ ],
+ [
+ "cellulose",
+ -13.325080871582031
+ ],
+ [
+ "▁Delicious",
+ -13.325080871582031
+ ],
+ [
+ "▁incearca",
+ -13.325080871582031
+ ],
+ [
+ "▁retrospective",
+ -13.325080871582031
+ ],
+ [
+ "▁mundane",
+ -13.325081825256348
+ ],
+ [
+ "▁definiert",
+ -13.32508659362793
+ ],
+ [
+ "▁cockpit",
+ -13.325088500976562
+ ],
+ [
+ "Aktionen",
+ -13.325363159179688
+ ],
+ [
+ "▁distanț",
+ -13.325654029846191
+ ],
+ [
+ "▁diplôme",
+ -13.325708389282227
+ ],
+ [
+ "prepaid",
+ -13.325737953186035
+ ],
+ [
+ "▁Tabellen",
+ -13.325758934020996
+ ],
+ [
+ "▁economie",
+ -13.325770378112793
+ ],
+ [
+ "December",
+ -13.325826644897461
+ ],
+ [
+ "Punkten",
+ -13.32613754272461
+ ],
+ [
+ "▁Punch",
+ -13.32614517211914
+ ],
+ [
+ "Martin",
+ -13.326154708862305
+ ],
+ [
+ "▁Espresso",
+ -13.326314926147461
+ ],
+ [
+ "▁ubiquitous",
+ -13.326335906982422
+ ],
+ [
+ "▁Mongolia",
+ -13.326337814331055
+ ],
+ [
+ "▁collabor",
+ -13.326635360717773
+ ],
+ [
+ "▁Vordergrund",
+ -13.32696533203125
+ ],
+ [
+ "cameră",
+ -13.327091217041016
+ ],
+ [
+ "represented",
+ -13.327268600463867
+ ],
+ [
+ "▁AUTO",
+ -13.327446937561035
+ ],
+ [
+ "▁Ofert",
+ -13.327542304992676
+ ],
+ [
+ "neig",
+ -13.327593803405762
+ ],
+ [
+ "▁Hazard",
+ -13.327595710754395
+ ],
+ [
+ "▁Constanta",
+ -13.327596664428711
+ ],
+ [
+ "▁tumour",
+ -13.32759952545166
+ ],
+ [
+ "▁Neighborhood",
+ -13.327603340148926
+ ],
+ [
+ "▁detaliat",
+ -13.327619552612305
+ ],
+ [
+ "▁extraordinaire",
+ -13.327665328979492
+ ],
+ [
+ "▁Therapeutic",
+ -13.327686309814453
+ ],
+ [
+ "predicting",
+ -13.327693939208984
+ ],
+ [
+ "▁institutii",
+ -13.32776165008545
+ ],
+ [
+ "ifizierung",
+ -13.327797889709473
+ ],
+ [
+ "wählt",
+ -13.328207015991211
+ ],
+ [
+ "▁remarquable",
+ -13.32822322845459
+ ],
+ [
+ "Invent",
+ -13.328512191772461
+ ],
+ [
+ "▁foloseșt",
+ -13.328514099121094
+ ],
+ [
+ "öfte",
+ -13.328703880310059
+ ],
+ [
+ "▁discreet",
+ -13.328853607177734
+ ],
+ [
+ "▁Flickr",
+ -13.32885456085205
+ ],
+ [
+ "▁trésor",
+ -13.328856468200684
+ ],
+ [
+ "▁steroids",
+ -13.328872680664062
+ ],
+ [
+ "▁personnalité",
+ -13.328953742980957
+ ],
+ [
+ "▁Krankenhaus",
+ -13.32901668548584
+ ],
+ [
+ "▁affordability",
+ -13.329218864440918
+ ],
+ [
+ "deuten",
+ -13.329398155212402
+ ],
+ [
+ "Detailed",
+ -13.329412460327148
+ ],
+ [
+ "Walk",
+ -13.329444885253906
+ ],
+ [
+ "▁parallèle",
+ -13.329483032226562
+ ],
+ [
+ "thèse",
+ -13.329649925231934
+ ],
+ [
+ "▁gefördert",
+ -13.330117225646973
+ ],
+ [
+ "Greeting",
+ -13.33014965057373
+ ],
+ [
+ "gelistet",
+ -13.330172538757324
+ ],
+ [
+ "▁chlorine",
+ -13.330392837524414
+ ],
+ [
+ "behält",
+ -13.33039665222168
+ ],
+ [
+ "emption",
+ -13.330435752868652
+ ],
+ [
+ "▁mobilité",
+ -13.330601692199707
+ ],
+ [
+ "▁randonnée",
+ -13.330668449401855
+ ],
+ [
+ "habitant",
+ -13.330718040466309
+ ],
+ [
+ "zilla",
+ -13.331082344055176
+ ],
+ [
+ "▁Lili",
+ -13.331160545349121
+ ],
+ [
+ "▁répét",
+ -13.331341743469238
+ ],
+ [
+ "trucât",
+ -13.331376075744629
+ ],
+ [
+ "▁Hospice",
+ -13.331376075744629
+ ],
+ [
+ "▁grassroots",
+ -13.331377029418945
+ ],
+ [
+ "▁affiché",
+ -13.331393241882324
+ ],
+ [
+ "pears",
+ -13.331470489501953
+ ],
+ [
+ "▁linistit",
+ -13.331497192382812
+ ],
+ [
+ "▁Patron",
+ -13.331552505493164
+ ],
+ [
+ "▁Stalin",
+ -13.331626892089844
+ ],
+ [
+ "▁închiri",
+ -13.331751823425293
+ ],
+ [
+ "▁Apostol",
+ -13.332018852233887
+ ],
+ [
+ "▁poudre",
+ -13.332246780395508
+ ],
+ [
+ "▁piscin",
+ -13.332419395446777
+ ],
+ [
+ "merlin",
+ -13.33259391784668
+ ],
+ [
+ "limited",
+ -13.33260726928711
+ ],
+ [
+ "▁métallique",
+ -13.332639694213867
+ ],
+ [
+ "gazebo",
+ -13.33267879486084
+ ],
+ [
+ "weilige",
+ -13.332718849182129
+ ],
+ [
+ "prosecutors",
+ -13.33278751373291
+ ],
+ [
+ "Expert",
+ -13.33314323425293
+ ],
+ [
+ "Assemblée",
+ -13.333271980285645
+ ],
+ [
+ "▁fauna",
+ -13.333285331726074
+ ],
+ [
+ "▁Turtle",
+ -13.333353996276855
+ ],
+ [
+ "▁Consortium",
+ -13.333905220031738
+ ],
+ [
+ "▁assemblies",
+ -13.333905220031738
+ ],
+ [
+ "▁trajectory",
+ -13.333905220031738
+ ],
+ [
+ "▁Vineyard",
+ -13.333906173706055
+ ],
+ [
+ "▁Mehrwert",
+ -13.334037780761719
+ ],
+ [
+ "▁sunflower",
+ -13.334043502807617
+ ],
+ [
+ "develop",
+ -13.334060668945312
+ ],
+ [
+ "▁heroic",
+ -13.334100723266602
+ ],
+ [
+ "▁riscuri",
+ -13.334151268005371
+ ],
+ [
+ "oeuf",
+ -13.334300994873047
+ ],
+ [
+ "influence",
+ -13.334452629089355
+ ],
+ [
+ "▁Voraussetzung",
+ -13.334500312805176
+ ],
+ [
+ "utoritatea",
+ -13.334518432617188
+ ],
+ [
+ "Produsul",
+ -13.334654808044434
+ ],
+ [
+ "▁gewährleistet",
+ -13.335171699523926
+ ],
+ [
+ "▁brûl",
+ -13.335175514221191
+ ],
+ [
+ "▁Column",
+ -13.335184097290039
+ ],
+ [
+ "▁trousers",
+ -13.335209846496582
+ ],
+ [
+ "▁posterior",
+ -13.33521556854248
+ ],
+ [
+ "glyph",
+ -13.335251808166504
+ ],
+ [
+ "▁Happen",
+ -13.335280418395996
+ ],
+ [
+ "▁créateur",
+ -13.335667610168457
+ ],
+ [
+ "▁apostle",
+ -13.335898399353027
+ ],
+ [
+ "▁padding",
+ -13.335907936096191
+ ],
+ [
+ "▁Digitalisierung",
+ -13.335908889770508
+ ],
+ [
+ "▁Laurie",
+ -13.335915565490723
+ ],
+ [
+ "▁Erwerb",
+ -13.336065292358398
+ ],
+ [
+ "▁bătrân",
+ -13.336440086364746
+ ],
+ [
+ "▁harmonious",
+ -13.336441040039062
+ ],
+ [
+ "▁ailments",
+ -13.336456298828125
+ ],
+ [
+ "▁Venue",
+ -13.33650016784668
+ ],
+ [
+ "▁Motorcycle",
+ -13.336523056030273
+ ],
+ [
+ "▁cortex",
+ -13.336551666259766
+ ],
+ [
+ "▁Sunrise",
+ -13.336636543273926
+ ],
+ [
+ "Software",
+ -13.336775779724121
+ ],
+ [
+ "▁advocat",
+ -13.336934089660645
+ ],
+ [
+ "essentiellement",
+ -13.337422370910645
+ ],
+ [
+ "•",
+ -13.337494850158691
+ ],
+ [
+ "părut",
+ -13.337522506713867
+ ],
+ [
+ "▁Suffolk",
+ -13.337711334228516
+ ],
+ [
+ "▁righteousness",
+ -13.337711334228516
+ ],
+ [
+ "▁Shirley",
+ -13.337712287902832
+ ],
+ [
+ "▁Famous",
+ -13.337749481201172
+ ],
+ [
+ "▁emulate",
+ -13.337788581848145
+ ],
+ [
+ "vermögen",
+ -13.33788776397705
+ ],
+ [
+ "generated",
+ -13.337963104248047
+ ],
+ [
+ "Ecole",
+ -13.337977409362793
+ ],
+ [
+ "▁managerial",
+ -13.338086128234863
+ ],
+ [
+ "believe",
+ -13.338091850280762
+ ],
+ [
+ "▁récupére",
+ -13.338348388671875
+ ],
+ [
+ "▁recens",
+ -13.338531494140625
+ ],
+ [
+ "▁Barrett",
+ -13.338778495788574
+ ],
+ [
+ "▁courageous",
+ -13.338814735412598
+ ],
+ [
+ "9.95",
+ -13.338961601257324
+ ],
+ [
+ "▁Odyssey",
+ -13.338982582092285
+ ],
+ [
+ "▁Violence",
+ -13.338982582092285
+ ],
+ [
+ "▁concasseur",
+ -13.338982582092285
+ ],
+ [
+ "▁evacuation",
+ -13.338982582092285
+ ],
+ [
+ "▁kontinuierlich",
+ -13.338982582092285
+ ],
+ [
+ "▁epidemi",
+ -13.3389892578125
+ ],
+ [
+ "▁disconnected",
+ -13.339197158813477
+ ],
+ [
+ "frucht",
+ -13.339339256286621
+ ],
+ [
+ "Trustees",
+ -13.339348793029785
+ ],
+ [
+ "▁Massiv",
+ -13.339459419250488
+ ],
+ [
+ "gebucht",
+ -13.339473724365234
+ ],
+ [
+ "stütze",
+ -13.339526176452637
+ ],
+ [
+ "▁febr",
+ -13.339741706848145
+ ],
+ [
+ "honoured",
+ -13.339743614196777
+ ],
+ [
+ "▁digitiz",
+ -13.340079307556152
+ ],
+ [
+ "Image",
+ -13.34021282196045
+ ],
+ [
+ "▁Brunswick",
+ -13.34025764465332
+ ],
+ [
+ "▁Therapist",
+ -13.34026050567627
+ ],
+ [
+ "accessoire",
+ -13.340264320373535
+ ],
+ [
+ "▁croqu",
+ -13.340291023254395
+ ],
+ [
+ "Pflanz",
+ -13.34052848815918
+ ],
+ [
+ "dragging",
+ -13.340536117553711
+ ],
+ [
+ "▁Facilit",
+ -13.340750694274902
+ ],
+ [
+ "soucis",
+ -13.340765953063965
+ ],
+ [
+ "Asadar",
+ -13.34081745147705
+ ],
+ [
+ "▁Thames",
+ -13.341021537780762
+ ],
+ [
+ "▁cariera",
+ -13.341116905212402
+ ],
+ [
+ "▁mercury",
+ -13.341530799865723
+ ],
+ [
+ "▁Blessed",
+ -13.341533660888672
+ ],
+ [
+ "▁Whitney",
+ -13.341630935668945
+ ],
+ [
+ "▁géant",
+ -13.341926574707031
+ ],
+ [
+ "▁coordonnée",
+ -13.342217445373535
+ ],
+ [
+ "oidal",
+ -13.342623710632324
+ ],
+ [
+ "Wohnungen",
+ -13.342696189880371
+ ],
+ [
+ "▁Spectrum",
+ -13.34280776977539
+ ],
+ [
+ "▁Avengers",
+ -13.342808723449707
+ ],
+ [
+ "▁Gloucester",
+ -13.342808723449707
+ ],
+ [
+ "▁nützlich",
+ -13.342811584472656
+ ],
+ [
+ "▁toothbrush",
+ -13.342830657958984
+ ],
+ [
+ "▁Vanessa",
+ -13.342843055725098
+ ],
+ [
+ "Saxon",
+ -13.342947959899902
+ ],
+ [
+ "▁comunități",
+ -13.343165397644043
+ ],
+ [
+ "reprezentanţi",
+ -13.343175888061523
+ ],
+ [
+ "▁întâlnire",
+ -13.343225479125977
+ ],
+ [
+ "delve",
+ -13.343234062194824
+ ],
+ [
+ "▁technologique",
+ -13.343452453613281
+ ],
+ [
+ "Describe",
+ -13.343466758728027
+ ],
+ [
+ "▁constient",
+ -13.343501091003418
+ ],
+ [
+ "gestalt",
+ -13.343600273132324
+ ],
+ [
+ "▁Tribune",
+ -13.344090461730957
+ ],
+ [
+ "▁fiberglass",
+ -13.34412956237793
+ ],
+ [
+ "verbindung",
+ -13.344210624694824
+ ],
+ [
+ "sacrificing",
+ -13.344351768493652
+ ],
+ [
+ "▁Pablo",
+ -13.344470024108887
+ ],
+ [
+ "▁adanc",
+ -13.34525203704834
+ ],
+ [
+ "omia",
+ -13.345309257507324
+ ],
+ [
+ "hâte",
+ -13.345317840576172
+ ],
+ [
+ "▁Sanctuary",
+ -13.345366477966309
+ ],
+ [
+ "▁accolade",
+ -13.345368385314941
+ ],
+ [
+ "▁Wurzel",
+ -13.345398902893066
+ ],
+ [
+ "▁spacing",
+ -13.345433235168457
+ ],
+ [
+ "▁bedeutend",
+ -13.345481872558594
+ ],
+ [
+ "▁biased",
+ -13.345499992370605
+ ],
+ [
+ "randomized",
+ -13.345747947692871
+ ],
+ [
+ "▁agenți",
+ -13.345856666564941
+ ],
+ [
+ "▁excepţi",
+ -13.346012115478516
+ ],
+ [
+ "▁fișier",
+ -13.346028327941895
+ ],
+ [
+ "▁fisier",
+ -13.34664535522461
+ ],
+ [
+ "irrespective",
+ -13.346648216247559
+ ],
+ [
+ "▁Gardner",
+ -13.34665584564209
+ ],
+ [
+ "▁aprecia",
+ -13.346884727478027
+ ],
+ [
+ "▁Klu",
+ -13.347082138061523
+ ],
+ [
+ "▁apropie",
+ -13.347535133361816
+ ],
+ [
+ "▁echival",
+ -13.347784042358398
+ ],
+ [
+ "tauchen",
+ -13.347862243652344
+ ],
+ [
+ "▁hauptsächlich",
+ -13.347930908203125
+ ],
+ [
+ "▁pollutants",
+ -13.347930908203125
+ ],
+ [
+ "▁mammals",
+ -13.347931861877441
+ ],
+ [
+ "▁Landwirtschaft",
+ -13.347936630249023
+ ],
+ [
+ "▁stăpân",
+ -13.34793758392334
+ ],
+ [
+ "▁Prüf",
+ -13.347990989685059
+ ],
+ [
+ "▁Motorsport",
+ -13.34807300567627
+ ],
+ [
+ "Leaving",
+ -13.348352432250977
+ ],
+ [
+ "schädigung",
+ -13.348573684692383
+ ],
+ [
+ "▁calendrier",
+ -13.348573684692383
+ ],
+ [
+ "plikation",
+ -13.348655700683594
+ ],
+ [
+ "▁DOE",
+ -13.348655700683594
+ ],
+ [
+ "ред",
+ -13.348966598510742
+ ],
+ [
+ "Jahr",
+ -13.34913444519043
+ ],
+ [
+ "▁entitlement",
+ -13.34921646118164
+ ],
+ [
+ "schuldig",
+ -13.349217414855957
+ ],
+ [
+ "▁Münster",
+ -13.349218368530273
+ ],
+ [
+ "pository",
+ -13.349451065063477
+ ],
+ [
+ "▁numero",
+ -13.350220680236816
+ ],
+ [
+ "▁entsprechen",
+ -13.350383758544922
+ ],
+ [
+ "▁astronaut",
+ -13.350502967834473
+ ],
+ [
+ "▁hexagon",
+ -13.350502967834473
+ ],
+ [
+ "▁DAMAGE",
+ -13.350503921508789
+ ],
+ [
+ "▁Quartz",
+ -13.350504875183105
+ ],
+ [
+ "▁rédaction",
+ -13.350504875183105
+ ],
+ [
+ "▁replenish",
+ -13.350508689880371
+ ],
+ [
+ "▁amoureux",
+ -13.350523948669434
+ ],
+ [
+ "▁opțiun",
+ -13.350616455078125
+ ],
+ [
+ "Custom",
+ -13.350622177124023
+ ],
+ [
+ "▁Telekom",
+ -13.350639343261719
+ ],
+ [
+ "▁RFID",
+ -13.351163864135742
+ ],
+ [
+ "▁Scorpio",
+ -13.351264953613281
+ ],
+ [
+ "▁thirst",
+ -13.35152816772461
+ ],
+ [
+ "▁Kosovo",
+ -13.351791381835938
+ ],
+ [
+ "▁precursor",
+ -13.351794242858887
+ ],
+ [
+ "▁sarbatori",
+ -13.351810455322266
+ ],
+ [
+ "▁Daisy",
+ -13.351828575134277
+ ],
+ [
+ "▁Dropbox",
+ -13.351898193359375
+ ],
+ [
+ "Smith",
+ -13.351949691772461
+ ],
+ [
+ "contabil",
+ -13.352191925048828
+ ],
+ [
+ "▁monnaie",
+ -13.352437973022461
+ ],
+ [
+ "capsul",
+ -13.352577209472656
+ ],
+ [
+ "treff",
+ -13.352760314941406
+ ],
+ [
+ "beauftragte",
+ -13.352761268615723
+ ],
+ [
+ "industrial",
+ -13.353006362915039
+ ],
+ [
+ "responsables",
+ -13.353010177612305
+ ],
+ [
+ "▁FIRST",
+ -13.353080749511719
+ ],
+ [
+ "▁crezut",
+ -13.35308837890625
+ ],
+ [
+ "▁reseller",
+ -13.353107452392578
+ ],
+ [
+ "▁direcți",
+ -13.353154182434082
+ ],
+ [
+ "mouvoir",
+ -13.353294372558594
+ ],
+ [
+ "▁Invite",
+ -13.353431701660156
+ ],
+ [
+ "▁constructii",
+ -13.353440284729004
+ ],
+ [
+ "▁oublié",
+ -13.353577613830566
+ ],
+ [
+ "găseșt",
+ -13.353687286376953
+ ],
+ [
+ "▁végét",
+ -13.353755950927734
+ ],
+ [
+ "idine",
+ -13.35385799407959
+ ],
+ [
+ "▁Ajout",
+ -13.353951454162598
+ ],
+ [
+ "▁Shelf",
+ -13.354195594787598
+ ],
+ [
+ "HALL",
+ -13.35422420501709
+ ],
+ [
+ "▁nostalgia",
+ -13.35437297821045
+ ],
+ [
+ "▁ottoman",
+ -13.35437297821045
+ ],
+ [
+ "▁ambalaj",
+ -13.354398727416992
+ ],
+ [
+ "municipiul",
+ -13.354405403137207
+ ],
+ [
+ "NOVA",
+ -13.354500770568848
+ ],
+ [
+ "▁disregard",
+ -13.354997634887695
+ ],
+ [
+ "▁bijuterii",
+ -13.355018615722656
+ ],
+ [
+ "▁sorgfältig",
+ -13.355018615722656
+ ],
+ [
+ "vraient",
+ -13.355307579040527
+ ],
+ [
+ "▁backsplash",
+ -13.355669975280762
+ ],
+ [
+ "▁nuisance",
+ -13.355679512023926
+ ],
+ [
+ "▁Territory",
+ -13.35568618774414
+ ],
+ [
+ "▁surprins",
+ -13.355693817138672
+ ],
+ [
+ "enchanting",
+ -13.35571002960205
+ ],
+ [
+ "trospecti",
+ -13.355847358703613
+ ],
+ [
+ "▁dvd",
+ -13.356199264526367
+ ],
+ [
+ "Totally",
+ -13.356329917907715
+ ],
+ [
+ "▁Edelstahl",
+ -13.35696029663086
+ ],
+ [
+ "▁sequencing",
+ -13.356961250305176
+ ],
+ [
+ "▁Circus",
+ -13.35696792602539
+ ],
+ [
+ "▁ashamed",
+ -13.35696792602539
+ ],
+ [
+ "▁horrific",
+ -13.357028007507324
+ ],
+ [
+ "▁taiat",
+ -13.357033729553223
+ ],
+ [
+ "▁Angehörige",
+ -13.357125282287598
+ ],
+ [
+ "Michel",
+ -13.357256889343262
+ ],
+ [
+ "▁communion",
+ -13.357298851013184
+ ],
+ [
+ "▁psiho",
+ -13.357378959655762
+ ],
+ [
+ "losigkeit",
+ -13.357405662536621
+ ],
+ [
+ "dipping",
+ -13.357512474060059
+ ],
+ [
+ "▁profesională",
+ -13.357608795166016
+ ],
+ [
+ "Indiferent",
+ -13.357609748840332
+ ],
+ [
+ "▁crestin",
+ -13.357723236083984
+ ],
+ [
+ "wholesome",
+ -13.357796669006348
+ ],
+ [
+ "▁Welfare",
+ -13.358257293701172
+ ],
+ [
+ "▁plentiful",
+ -13.358257293701172
+ ],
+ [
+ "▁Triumph",
+ -13.358258247375488
+ ],
+ [
+ "▁fascination",
+ -13.358260154724121
+ ],
+ [
+ "▁vicious",
+ -13.358291625976562
+ ],
+ [
+ "▁Höchst",
+ -13.358294486999512
+ ],
+ [
+ "▁Dunkel",
+ -13.358386039733887
+ ],
+ [
+ "▁harass",
+ -13.358406066894531
+ ],
+ [
+ "ambogia",
+ -13.358475685119629
+ ],
+ [
+ "▁synonymous",
+ -13.358598709106445
+ ],
+ [
+ "bottom",
+ -13.35879898071289
+ ],
+ [
+ "▁bénévole",
+ -13.358906745910645
+ ],
+ [
+ "▁suprafaț",
+ -13.358906745910645
+ ],
+ [
+ "▁umplut",
+ -13.358997344970703
+ ],
+ [
+ "▁Teddy",
+ -13.359162330627441
+ ],
+ [
+ "breathable",
+ -13.359292984008789
+ ],
+ [
+ "▁Toshiba",
+ -13.3595552444458
+ ],
+ [
+ "▁seismic",
+ -13.359569549560547
+ ],
+ [
+ "▁dringend",
+ -13.359583854675293
+ ],
+ [
+ "▁cultură",
+ -13.359585762023926
+ ],
+ [
+ "▁Waffen",
+ -13.359665870666504
+ ],
+ [
+ "▁Bubble",
+ -13.359702110290527
+ ],
+ [
+ "▁Brigade",
+ -13.359759330749512
+ ],
+ [
+ "▁Blatt",
+ -13.36012077331543
+ ],
+ [
+ "▁scénario",
+ -13.36020565032959
+ ],
+ [
+ "allah",
+ -13.360396385192871
+ ],
+ [
+ "▁superintendent",
+ -13.360855102539062
+ ],
+ [
+ "pflanzen",
+ -13.360856056213379
+ ],
+ [
+ "▁kurzfristig",
+ -13.360856056213379
+ ],
+ [
+ "▁raspberry",
+ -13.360876083374023
+ ],
+ [
+ "▁Evident",
+ -13.360904693603516
+ ],
+ [
+ "▁inutile",
+ -13.361076354980469
+ ],
+ [
+ "prouvé",
+ -13.361104011535645
+ ],
+ [
+ "▁obtien",
+ -13.36141300201416
+ ],
+ [
+ "▁Matthias",
+ -13.361506462097168
+ ],
+ [
+ "▁déclench",
+ -13.361506462097168
+ ],
+ [
+ "Situationen",
+ -13.361529350280762
+ ],
+ [
+ "▁Disclaimer",
+ -13.362156867980957
+ ],
+ [
+ "▁loneliness",
+ -13.362156867980957
+ ],
+ [
+ "▁Gothic",
+ -13.362164497375488
+ ],
+ [
+ "▁humility",
+ -13.362165451049805
+ ],
+ [
+ "▁machiaj",
+ -13.362175941467285
+ ],
+ [
+ "▁Sophia",
+ -13.362178802490234
+ ],
+ [
+ "▁Forecast",
+ -13.362265586853027
+ ],
+ [
+ "IBLE",
+ -13.362456321716309
+ ],
+ [
+ "ivism",
+ -13.362480163574219
+ ],
+ [
+ "israel",
+ -13.36278247833252
+ ],
+ [
+ "▁kümmern",
+ -13.362809181213379
+ ],
+ [
+ "▁verbreitet",
+ -13.362825393676758
+ ],
+ [
+ "▁capacitor",
+ -13.362832069396973
+ ],
+ [
+ "deprived",
+ -13.3634614944458
+ ],
+ [
+ "unbiased",
+ -13.3634614944458
+ ],
+ [
+ "▁Dominique",
+ -13.3634614944458
+ ],
+ [
+ "▁Bamboo",
+ -13.363462448120117
+ ],
+ [
+ "▁Heinrich",
+ -13.363465309143066
+ ],
+ [
+ "individualized",
+ -13.363550186157227
+ ],
+ [
+ "▁ansprechen",
+ -13.363776206970215
+ ],
+ [
+ "ordinaire",
+ -13.363801002502441
+ ],
+ [
+ "▁Ucraina",
+ -13.364112854003906
+ ],
+ [
+ "▁militare",
+ -13.364115715026855
+ ],
+ [
+ "massif",
+ -13.364352226257324
+ ],
+ [
+ "▁emisiuni",
+ -13.364501953125
+ ],
+ [
+ "maladies",
+ -13.364622116088867
+ ],
+ [
+ "▁pneumonia",
+ -13.364765167236328
+ ],
+ [
+ "▁graffiti",
+ -13.364767074584961
+ ],
+ [
+ "▁Determine",
+ -13.3648099899292
+ ],
+ [
+ "▁Northwestern",
+ -13.364893913269043
+ ],
+ [
+ "▁grasimi",
+ -13.364897727966309
+ ],
+ [
+ "▁lebendig",
+ -13.364920616149902
+ ],
+ [
+ "▁cifre",
+ -13.364946365356445
+ ],
+ [
+ "▁accelerator",
+ -13.36533260345459
+ ],
+ [
+ "▁nib",
+ -13.365374565124512
+ ],
+ [
+ "▁Jocuri",
+ -13.365400314331055
+ ],
+ [
+ "▁außergewöhnlich",
+ -13.365402221679688
+ ],
+ [
+ "▁orchid",
+ -13.36542797088623
+ ],
+ [
+ "zugreifen",
+ -13.365530967712402
+ ],
+ [
+ "utilisent",
+ -13.365662574768066
+ ],
+ [
+ "▁nineteenth",
+ -13.366071701049805
+ ],
+ [
+ "improvisation",
+ -13.366072654724121
+ ],
+ [
+ "▁Disclosure",
+ -13.366072654724121
+ ],
+ [
+ "▁Überraschung",
+ -13.366072654724121
+ ],
+ [
+ "▁Casual",
+ -13.366093635559082
+ ],
+ [
+ "▁Witness",
+ -13.366093635559082
+ ],
+ [
+ "teacher",
+ -13.366125106811523
+ ],
+ [
+ "Printed",
+ -13.366129875183105
+ ],
+ [
+ "▁prețuri",
+ -13.366189956665039
+ ],
+ [
+ "rues",
+ -13.366216659545898
+ ],
+ [
+ "▁cerinte",
+ -13.366338729858398
+ ],
+ [
+ "rouvent",
+ -13.36662483215332
+ ],
+ [
+ "assembling",
+ -13.36673355102539
+ ],
+ [
+ "▁atenție",
+ -13.366769790649414
+ ],
+ [
+ "▁amintiri",
+ -13.366782188415527
+ ],
+ [
+ "▁sustinut",
+ -13.366805076599121
+ ],
+ [
+ "Digital",
+ -13.367257118225098
+ ],
+ [
+ "▁Deborah",
+ -13.36738109588623
+ ],
+ [
+ "gesichts",
+ -13.367382049560547
+ ],
+ [
+ "▁temperament",
+ -13.367440223693848
+ ],
+ [
+ "▁competency",
+ -13.367447853088379
+ ],
+ [
+ "▁dwarf",
+ -13.367515563964844
+ ],
+ [
+ "▁dureaz",
+ -13.367539405822754
+ ],
+ [
+ "habilit",
+ -13.367764472961426
+ ],
+ [
+ "leaned",
+ -13.3679838180542
+ ],
+ [
+ "▁illicit",
+ -13.368348121643066
+ ],
+ [
+ "Availability",
+ -13.368691444396973
+ ],
+ [
+ "▁Brașov",
+ -13.368691444396973
+ ],
+ [
+ "▁Pyramid",
+ -13.368691444396973
+ ],
+ [
+ "▁achievable",
+ -13.368691444396973
+ ],
+ [
+ "▁judiciaire",
+ -13.368691444396973
+ ],
+ [
+ "Übrigen",
+ -13.368693351745605
+ ],
+ [
+ "▁activism",
+ -13.368795394897461
+ ],
+ [
+ "▁boycott",
+ -13.368839263916016
+ ],
+ [
+ "Desigur",
+ -13.368927001953125
+ ],
+ [
+ "klingt",
+ -13.369264602661133
+ ],
+ [
+ "▁Leidenschaft",
+ -13.369346618652344
+ ],
+ [
+ "▁Richtig",
+ -13.369701385498047
+ ],
+ [
+ "▁Airbnb",
+ -13.370002746582031
+ ],
+ [
+ "▁învățământ",
+ -13.370002746582031
+ ],
+ [
+ "Kampagne",
+ -13.370004653930664
+ ],
+ [
+ "▁thumbnail",
+ -13.370014190673828
+ ],
+ [
+ "Bestimmungen",
+ -13.370016098022461
+ ],
+ [
+ "▁vollkommen",
+ -13.37001895904541
+ ],
+ [
+ "▁biomass",
+ -13.370027542114258
+ ],
+ [
+ "▁escalate",
+ -13.370030403137207
+ ],
+ [
+ "wächst",
+ -13.370085716247559
+ ],
+ [
+ "▁scăpa",
+ -13.370098114013672
+ ],
+ [
+ "▁résult",
+ -13.37014389038086
+ ],
+ [
+ "▁shrine",
+ -13.370217323303223
+ ],
+ [
+ "maximizing",
+ -13.370370864868164
+ ],
+ [
+ "avoue",
+ -13.370492935180664
+ ],
+ [
+ "dirigeants",
+ -13.370665550231934
+ ],
+ [
+ "▁cerveau",
+ -13.370672225952148
+ ],
+ [
+ "▁proast",
+ -13.370955467224121
+ ],
+ [
+ "▁contaminants",
+ -13.371325492858887
+ ],
+ [
+ "effectue",
+ -13.37151050567627
+ ],
+ [
+ "ediție",
+ -13.371539115905762
+ ],
+ [
+ "monetiz",
+ -13.371772766113281
+ ],
+ [
+ "▁deplasare",
+ -13.371976852416992
+ ],
+ [
+ "▁Sfant",
+ -13.37209415435791
+ ],
+ [
+ "ROOM",
+ -13.372113227844238
+ ],
+ [
+ "bushes",
+ -13.372151374816895
+ ],
+ [
+ "mairie",
+ -13.37251091003418
+ ],
+ [
+ "obligate",
+ -13.372528076171875
+ ],
+ [
+ "▁tug",
+ -13.372573852539062
+ ],
+ [
+ "▁Collector",
+ -13.372632026672363
+ ],
+ [
+ "▁annoyed",
+ -13.372633934020996
+ ],
+ [
+ "▁aerobic",
+ -13.372654914855957
+ ],
+ [
+ "▁integer",
+ -13.372830390930176
+ ],
+ [
+ "▁Upload",
+ -13.373249053955078
+ ],
+ [
+ "▁impartial",
+ -13.37346076965332
+ ],
+ [
+ "▁discuţi",
+ -13.373623847961426
+ ],
+ [
+ "gastrointestinal",
+ -13.37394905090332
+ ],
+ [
+ "▁chiropractor",
+ -13.37394905090332
+ ],
+ [
+ "▁treptat",
+ -13.373950004577637
+ ],
+ [
+ "▁fishermen",
+ -13.37395191192627
+ ],
+ [
+ "levitra",
+ -13.3739595413208
+ ],
+ [
+ "Gruppe",
+ -13.373964309692383
+ ],
+ [
+ "▁Apostle",
+ -13.373970985412598
+ ],
+ [
+ "▁conseillé",
+ -13.374068260192871
+ ],
+ [
+ "Isra",
+ -13.37421703338623
+ ],
+ [
+ "▁Persönlichkeit",
+ -13.374431610107422
+ ],
+ [
+ "▁cantitati",
+ -13.374459266662598
+ ],
+ [
+ "▁incredibil",
+ -13.374614715576172
+ ],
+ [
+ "▁Berater",
+ -13.374800682067871
+ ],
+ [
+ "▁propuneri",
+ -13.374835014343262
+ ],
+ [
+ "MEDIA",
+ -13.375236511230469
+ ],
+ [
+ "▁opaque",
+ -13.37526798248291
+ ],
+ [
+ "▁Nielsen",
+ -13.375269889831543
+ ],
+ [
+ "▁cartofi",
+ -13.375277519226074
+ ],
+ [
+ "▁Whale",
+ -13.37533950805664
+ ],
+ [
+ "erzeugen",
+ -13.375890731811523
+ ],
+ [
+ "▁knack",
+ -13.375931739807129
+ ],
+ [
+ "Kandidat",
+ -13.375936508178711
+ ],
+ [
+ "▁tradițional",
+ -13.375937461853027
+ ],
+ [
+ "zählige",
+ -13.375983238220215
+ ],
+ [
+ "▁Petroleum",
+ -13.376588821411133
+ ],
+ [
+ "▁deficiencies",
+ -13.376588821411133
+ ],
+ [
+ "▁persecution",
+ -13.376588821411133
+ ],
+ [
+ "▁zgomot",
+ -13.376588821411133
+ ],
+ [
+ "▁reiterate",
+ -13.376592636108398
+ ],
+ [
+ "▁Slice",
+ -13.376670837402344
+ ],
+ [
+ "▁envy",
+ -13.376704216003418
+ ],
+ [
+ "▁stomac",
+ -13.376851081848145
+ ],
+ [
+ "Donnell",
+ -13.376914978027344
+ ],
+ [
+ "▁primordial",
+ -13.377249717712402
+ ],
+ [
+ "reclining",
+ -13.377274513244629
+ ],
+ [
+ "PASS",
+ -13.377861976623535
+ ],
+ [
+ "▁Resistance",
+ -13.377910614013672
+ ],
+ [
+ "▁Widerruf",
+ -13.377911567687988
+ ],
+ [
+ "▁vodka",
+ -13.377911567687988
+ ],
+ [
+ "▁yolk",
+ -13.377912521362305
+ ],
+ [
+ "ollywood",
+ -13.377915382385254
+ ],
+ [
+ "▁truffle",
+ -13.377933502197266
+ ],
+ [
+ "▁Sänger",
+ -13.377955436706543
+ ],
+ [
+ "▁Kenntnis",
+ -13.377968788146973
+ ],
+ [
+ "▁Kiel",
+ -13.37803840637207
+ ],
+ [
+ "▁Mutual",
+ -13.378044128417969
+ ],
+ [
+ "▁saliva",
+ -13.37816047668457
+ ],
+ [
+ "▁renforce",
+ -13.378411293029785
+ ],
+ [
+ "▁mulch",
+ -13.378680229187012
+ ],
+ [
+ "▁reviste",
+ -13.378875732421875
+ ],
+ [
+ "lucrarea",
+ -13.378978729248047
+ ],
+ [
+ "▁multiply",
+ -13.379130363464355
+ ],
+ [
+ "▁marshmallow",
+ -13.379234313964844
+ ],
+ [
+ "▁Durchschnitt",
+ -13.379288673400879
+ ],
+ [
+ "▁Authorities",
+ -13.379426002502441
+ ],
+ [
+ "▁greed",
+ -13.379521369934082
+ ],
+ [
+ "Visiting",
+ -13.379638671875
+ ],
+ [
+ "Carlton",
+ -13.379727363586426
+ ],
+ [
+ "▁splend",
+ -13.37975025177002
+ ],
+ [
+ "▁Erkenntnisse",
+ -13.379898071289062
+ ],
+ [
+ "▁Russie",
+ -13.379916191101074
+ ],
+ [
+ "Agence",
+ -13.38007926940918
+ ],
+ [
+ "schickt",
+ -13.380288124084473
+ ],
+ [
+ "##",
+ -13.3804931640625
+ ],
+ [
+ "▁Erweiterung",
+ -13.380560874938965
+ ],
+ [
+ "▁Franchise",
+ -13.380560874938965
+ ],
+ [
+ "Dedicated",
+ -13.380563735961914
+ ],
+ [
+ "▁Wisdom",
+ -13.380569458007812
+ ],
+ [
+ "▁gagnant",
+ -13.380592346191406
+ ],
+ [
+ "planetary",
+ -13.380598068237305
+ ],
+ [
+ "▁affinity",
+ -13.380619049072266
+ ],
+ [
+ "▁préférence",
+ -13.380739212036133
+ ],
+ [
+ "▁intellect",
+ -13.380810737609863
+ ],
+ [
+ "▁Translat",
+ -13.380830764770508
+ ],
+ [
+ "▁Sultan",
+ -13.38089370727539
+ ],
+ [
+ "▁birouri",
+ -13.38101577758789
+ ],
+ [
+ "▁Academie",
+ -13.381224632263184
+ ],
+ [
+ "▁consequential",
+ -13.38138484954834
+ ],
+ [
+ "▁festgestellt",
+ -13.381402015686035
+ ],
+ [
+ "▁Chanel",
+ -13.381444931030273
+ ],
+ [
+ "▁soutenu",
+ -13.381875038146973
+ ],
+ [
+ "▁Montessori",
+ -13.381888389587402
+ ],
+ [
+ "▁equitable",
+ -13.381892204284668
+ ],
+ [
+ "▁théorie",
+ -13.381893157958984
+ ],
+ [
+ "▁primavara",
+ -13.3818941116333
+ ],
+ [
+ "▁Daughter",
+ -13.38189697265625
+ ],
+ [
+ "▁Dixon",
+ -13.381898880004883
+ ],
+ [
+ "▁unravel",
+ -13.38190746307373
+ ],
+ [
+ "Olimp",
+ -13.381915092468262
+ ],
+ [
+ "▁disturbed",
+ -13.381916999816895
+ ],
+ [
+ "▁novelty",
+ -13.382004737854004
+ ],
+ [
+ "synchronous",
+ -13.382113456726074
+ ],
+ [
+ "relevant",
+ -13.382166862487793
+ ],
+ [
+ "bourgeois",
+ -13.38251781463623
+ ],
+ [
+ "▁Parfum",
+ -13.38255500793457
+ ],
+ [
+ "▁Polonia",
+ -13.382563591003418
+ ],
+ [
+ "▁monoton",
+ -13.382781028747559
+ ],
+ [
+ "tratare",
+ -13.38302230834961
+ ],
+ [
+ "dumping",
+ -13.38318157196045
+ ],
+ [
+ "▁Bibliothek",
+ -13.383217811584473
+ ],
+ [
+ "▁Saskatchewan",
+ -13.383217811584473
+ ],
+ [
+ "▁experiential",
+ -13.383217811584473
+ ],
+ [
+ "▁verursacht",
+ -13.383217811584473
+ ],
+ [
+ "intègre",
+ -13.383218765258789
+ ],
+ [
+ "▁Intermediate",
+ -13.383275032043457
+ ],
+ [
+ "Israel",
+ -13.383476257324219
+ ],
+ [
+ "lucreaza",
+ -13.383495330810547
+ ],
+ [
+ "▁quantify",
+ -13.383862495422363
+ ],
+ [
+ "▁zahăr",
+ -13.383882522583008
+ ],
+ [
+ "▁încadr",
+ -13.383902549743652
+ ],
+ [
+ "Personalized",
+ -13.383946418762207
+ ],
+ [
+ "▁Chronic",
+ -13.384309768676758
+ ],
+ [
+ "hôpital",
+ -13.384549140930176
+ ],
+ [
+ "▁diskutiert",
+ -13.384549140930176
+ ],
+ [
+ "electrique",
+ -13.3848876953125
+ ],
+ [
+ "ethos",
+ -13.384978294372559
+ ],
+ [
+ "Nase",
+ -13.385059356689453
+ ],
+ [
+ "atmosphère",
+ -13.385214805603027
+ ],
+ [
+ "▁ungefähr",
+ -13.385215759277344
+ ],
+ [
+ "évaluer",
+ -13.385251998901367
+ ],
+ [
+ "▁scuz",
+ -13.385321617126465
+ ],
+ [
+ "haltige",
+ -13.38533878326416
+ ],
+ [
+ "January",
+ -13.38557243347168
+ ],
+ [
+ "▁Sharma",
+ -13.385603904724121
+ ],
+ [
+ "▁seizures",
+ -13.385881423950195
+ ],
+ [
+ "▁zucchini",
+ -13.385881423950195
+ ],
+ [
+ "▁Stadi",
+ -13.385885238647461
+ ],
+ [
+ "▁eccentric",
+ -13.385885238647461
+ ],
+ [
+ "▁offensichtlich",
+ -13.385909080505371
+ ],
+ [
+ "▁Irvine",
+ -13.385920524597168
+ ],
+ [
+ "cuprinse",
+ -13.38601303100586
+ ],
+ [
+ "▁Arbitr",
+ -13.386157035827637
+ ],
+ [
+ "Buenos",
+ -13.386183738708496
+ ],
+ [
+ "▁Shelter",
+ -13.386210441589355
+ ],
+ [
+ "CEPT",
+ -13.386454582214355
+ ],
+ [
+ "ouvri",
+ -13.386455535888672
+ ],
+ [
+ "acryl",
+ -13.386539459228516
+ ],
+ [
+ "▁Gourmet",
+ -13.38654899597168
+ ],
+ [
+ "scented",
+ -13.386595726013184
+ ],
+ [
+ "doubling",
+ -13.38659954071045
+ ],
+ [
+ "▁rafina",
+ -13.386608123779297
+ ],
+ [
+ "▁Vereinbarung",
+ -13.38721752166748
+ ],
+ [
+ "▁Dashboard",
+ -13.387218475341797
+ ],
+ [
+ "▁Sandwich",
+ -13.387218475341797
+ ],
+ [
+ "▁Riviera",
+ -13.387226104736328
+ ],
+ [
+ "échec",
+ -13.387237548828125
+ ],
+ [
+ "Giro",
+ -13.387253761291504
+ ],
+ [
+ "▁oasis",
+ -13.38725757598877
+ ],
+ [
+ "▁apology",
+ -13.3872709274292
+ ],
+ [
+ "▁YEAR",
+ -13.387272834777832
+ ],
+ [
+ "▁realtor",
+ -13.387504577636719
+ ],
+ [
+ "acheteur",
+ -13.38754653930664
+ ],
+ [
+ "▁larva",
+ -13.387613296508789
+ ],
+ [
+ "▁invitați",
+ -13.388097763061523
+ ],
+ [
+ "exhibiting",
+ -13.38830852508545
+ ],
+ [
+ "modernen",
+ -13.388331413269043
+ ],
+ [
+ "▁Collaboration",
+ -13.38855266571045
+ ],
+ [
+ "▁dezvălui",
+ -13.38855266571045
+ ],
+ [
+ "▁kiosk",
+ -13.38855266571045
+ ],
+ [
+ "▁Bermuda",
+ -13.388553619384766
+ ],
+ [
+ "Copiii",
+ -13.388564109802246
+ ],
+ [
+ "▁goddess",
+ -13.388581275939941
+ ],
+ [
+ "uplifting",
+ -13.388609886169434
+ ],
+ [
+ "▁simultan",
+ -13.388808250427246
+ ],
+ [
+ "▁episod",
+ -13.388884544372559
+ ],
+ [
+ "▁Braşov",
+ -13.38922119140625
+ ],
+ [
+ "cunoscută",
+ -13.389634132385254
+ ],
+ [
+ "▁Cherokee",
+ -13.389890670776367
+ ],
+ [
+ "▁Kazakhstan",
+ -13.389890670776367
+ ],
+ [
+ "▁Lauderdale",
+ -13.389890670776367
+ ],
+ [
+ "▁închisoare",
+ -13.389898300170898
+ ],
+ [
+ "▁Christchurch",
+ -13.389934539794922
+ ],
+ [
+ "▁influenţ",
+ -13.389982223510742
+ ],
+ [
+ "▁Meghan",
+ -13.390019416809082
+ ],
+ [
+ "▁Dienstleistung",
+ -13.390557289123535
+ ],
+ [
+ "▁cladiri",
+ -13.390564918518066
+ ],
+ [
+ "▁evrei",
+ -13.391148567199707
+ ],
+ [
+ "▁oatmeal",
+ -13.391230583190918
+ ],
+ [
+ "▁chronique",
+ -13.3912353515625
+ ],
+ [
+ "▁associée",
+ -13.391264915466309
+ ],
+ [
+ "▁Goose",
+ -13.391283988952637
+ ],
+ [
+ "gänz",
+ -13.391855239868164
+ ],
+ [
+ "▁Blätter",
+ -13.391901969909668
+ ],
+ [
+ "▁jurnalist",
+ -13.392212867736816
+ ],
+ [
+ "cedat",
+ -13.392263412475586
+ ],
+ [
+ "nommée",
+ -13.392315864562988
+ ],
+ [
+ "écrivain",
+ -13.392572402954102
+ ],
+ [
+ "▁epoxy",
+ -13.392577171325684
+ ],
+ [
+ "▁verlangt",
+ -13.392590522766113
+ ],
+ [
+ "Störung",
+ -13.392708778381348
+ ],
+ [
+ "▁Doyle",
+ -13.392729759216309
+ ],
+ [
+ "▁Philharmoni",
+ -13.392844200134277
+ ],
+ [
+ "▁déclare",
+ -13.393044471740723
+ ],
+ [
+ "effort",
+ -13.393045425415039
+ ],
+ [
+ "ström",
+ -13.393118858337402
+ ],
+ [
+ "▁cunoaşte",
+ -13.393244743347168
+ ],
+ [
+ "▁gigantic",
+ -13.3932466506958
+ ],
+ [
+ "któ",
+ -13.393378257751465
+ ],
+ [
+ "▁ilustr",
+ -13.393529891967773
+ ],
+ [
+ "▁frec",
+ -13.39371109008789
+ ],
+ [
+ "▁Syracuse",
+ -13.393916130065918
+ ],
+ [
+ "▁Einwilligung",
+ -13.393917083740234
+ ],
+ [
+ "▁miraculous",
+ -13.393917083740234
+ ],
+ [
+ "▁ökologisch",
+ -13.393917083740234
+ ],
+ [
+ "▁Simmons",
+ -13.393922805786133
+ ],
+ [
+ "▁albastru",
+ -13.393926620483398
+ ],
+ [
+ "besser",
+ -13.393962860107422
+ ],
+ [
+ "▁interioare",
+ -13.394006729125977
+ ],
+ [
+ "▁Trocken",
+ -13.394068717956543
+ ],
+ [
+ "niveau",
+ -13.39406967163086
+ ],
+ [
+ "▁Torah",
+ -13.394122123718262
+ ],
+ [
+ "▁beobachten",
+ -13.3945894241333
+ ],
+ [
+ "▁behandeln",
+ -13.394637107849121
+ ],
+ [
+ "staffed",
+ -13.394742965698242
+ ],
+ [
+ "hütte",
+ -13.394824028015137
+ ],
+ [
+ "Central",
+ -13.394939422607422
+ ],
+ [
+ "▁Freiburg",
+ -13.395198822021484
+ ],
+ [
+ "▁Netanyahu",
+ -13.395261764526367
+ ],
+ [
+ "▁Lexington",
+ -13.395302772521973
+ ],
+ [
+ "▁insotit",
+ -13.395492553710938
+ ],
+ [
+ "▁depasi",
+ -13.39560604095459
+ ],
+ [
+ "sewage",
+ -13.395853996276855
+ ],
+ [
+ "erkrankung",
+ -13.395951271057129
+ ],
+ [
+ "▁părţi",
+ -13.396234512329102
+ ],
+ [
+ "▁Nixon",
+ -13.39661693572998
+ ],
+ [
+ "Byron",
+ -13.396905899047852
+ ],
+ [
+ "▁varietat",
+ -13.39724063873291
+ ],
+ [
+ "▁Bildschirm",
+ -13.397299766540527
+ ],
+ [
+ "▁accompli",
+ -13.397424697875977
+ ],
+ [
+ "affirmed",
+ -13.397525787353516
+ ],
+ [
+ "▁phyto",
+ -13.397533416748047
+ ],
+ [
+ "sectiune",
+ -13.397592544555664
+ ],
+ [
+ "abteilung",
+ -13.397932052612305
+ ],
+ [
+ "▁voastre",
+ -13.397957801818848
+ ],
+ [
+ "GitHub",
+ -13.397958755493164
+ ],
+ [
+ "▁Jorge",
+ -13.39796257019043
+ ],
+ [
+ "ACTION",
+ -13.397972106933594
+ ],
+ [
+ "voastra",
+ -13.397984504699707
+ ],
+ [
+ "▁Peanut",
+ -13.397987365722656
+ ],
+ [
+ "▁bilingual",
+ -13.398011207580566
+ ],
+ [
+ "▁nourriture",
+ -13.39803695678711
+ ],
+ [
+ "▁Asphalt",
+ -13.398640632629395
+ ],
+ [
+ "emballage",
+ -13.399310111999512
+ ],
+ [
+ "▁sanitation",
+ -13.399310111999512
+ ],
+ [
+ "▁Dessert",
+ -13.399313926696777
+ ],
+ [
+ "intitulé",
+ -13.399322509765625
+ ],
+ [
+ "▁acţiune",
+ -13.399374008178711
+ ],
+ [
+ "▁Übersetzung",
+ -13.399402618408203
+ ],
+ [
+ "destinate",
+ -13.39941692352295
+ ],
+ [
+ "▁Goddess",
+ -13.399504661560059
+ ],
+ [
+ "poziție",
+ -13.399576187133789
+ ],
+ [
+ "denumirea",
+ -13.400002479553223
+ ],
+ [
+ "cantitatea",
+ -13.40002727508545
+ ],
+ [
+ "▁Stereo",
+ -13.400223731994629
+ ],
+ [
+ "object",
+ -13.400373458862305
+ ],
+ [
+ "▁décè",
+ -13.40058708190918
+ ],
+ [
+ "▁Handeln",
+ -13.400665283203125
+ ],
+ [
+ "▁ambience",
+ -13.400697708129883
+ ],
+ [
+ "▁Lindsay",
+ -13.4006986618042
+ ],
+ [
+ "▁tensiune",
+ -13.400781631469727
+ ],
+ [
+ "▁thrift",
+ -13.400788307189941
+ ],
+ [
+ "▁Optimiz",
+ -13.400843620300293
+ ],
+ [
+ "▁beantworten",
+ -13.401338577270508
+ ],
+ [
+ "▁magistrat",
+ -13.401342391967773
+ ],
+ [
+ "évidence",
+ -13.402016639709473
+ ],
+ [
+ "▁Eclipse",
+ -13.402016639709473
+ ],
+ [
+ "▁Ribbon",
+ -13.402016639709473
+ ],
+ [
+ "▁condensation",
+ -13.402016639709473
+ ],
+ [
+ "▁innocence",
+ -13.402018547058105
+ ],
+ [
+ "▁mascara",
+ -13.402023315429688
+ ],
+ [
+ "▁seventeen",
+ -13.402290344238281
+ ],
+ [
+ "▁compétent",
+ -13.402694702148438
+ ],
+ [
+ "bewertet",
+ -13.402717590332031
+ ],
+ [
+ "▁Muzic",
+ -13.40285587310791
+ ],
+ [
+ "complexities",
+ -13.402928352355957
+ ],
+ [
+ "ddington",
+ -13.403324127197266
+ ],
+ [
+ "Entwickler",
+ -13.403372764587402
+ ],
+ [
+ "masonry",
+ -13.4033784866333
+ ],
+ [
+ "Führer",
+ -13.403386116027832
+ ],
+ [
+ "▁awakening",
+ -13.403388977050781
+ ],
+ [
+ "▁lovitur",
+ -13.403806686401367
+ ],
+ [
+ "gebrochen",
+ -13.404068946838379
+ ],
+ [
+ "indexed",
+ -13.404478073120117
+ ],
+ [
+ "campania",
+ -13.404515266418457
+ ],
+ [
+ "▁Fountain",
+ -13.404730796813965
+ ],
+ [
+ "▁Joomla",
+ -13.404730796813965
+ ],
+ [
+ "▁Superintendent",
+ -13.404730796813965
+ ],
+ [
+ "▁Dahl",
+ -13.404742240905762
+ ],
+ [
+ "▁Benefici",
+ -13.404863357543945
+ ],
+ [
+ "optimiser",
+ -13.404919624328613
+ ],
+ [
+ "bursting",
+ -13.405380249023438
+ ],
+ [
+ "diplom",
+ -13.405427932739258
+ ],
+ [
+ "microsoft",
+ -13.405621528625488
+ ],
+ [
+ "▁correlate",
+ -13.405776977539062
+ ],
+ [
+ "▁arhitectura",
+ -13.405848503112793
+ ],
+ [
+ "▁lunette",
+ -13.40611743927002
+ ],
+ [
+ "Statistical",
+ -13.406147003173828
+ ],
+ [
+ "▁iarnă",
+ -13.406201362609863
+ ],
+ [
+ "▁importanț",
+ -13.406932830810547
+ ],
+ [
+ "sistence",
+ -13.407366752624512
+ ],
+ [
+ "associated",
+ -13.407402992248535
+ ],
+ [
+ "Occident",
+ -13.407452583312988
+ ],
+ [
+ "▁Heidelberg",
+ -13.407452583312988
+ ],
+ [
+ "▁acquaintance",
+ -13.407452583312988
+ ],
+ [
+ "Introducing",
+ -13.407453536987305
+ ],
+ [
+ "▁ripple",
+ -13.407480239868164
+ ],
+ [
+ "▁Childhood",
+ -13.407563209533691
+ ],
+ [
+ "drywall",
+ -13.407577514648438
+ ],
+ [
+ "Vreau",
+ -13.40771770477295
+ ],
+ [
+ "▁compétence",
+ -13.407967567443848
+ ],
+ [
+ "▁asteapta",
+ -13.408135414123535
+ ],
+ [
+ "▁duhovnic",
+ -13.408135414123535
+ ],
+ [
+ "▁învăţământ",
+ -13.408141136169434
+ ],
+ [
+ "encompassing",
+ -13.40829849243164
+ ],
+ [
+ "1997)",
+ -13.408370018005371
+ ],
+ [
+ "▁atractiv",
+ -13.408515930175781
+ ],
+ [
+ "Majoritatea",
+ -13.408775329589844
+ ],
+ [
+ "▁bungalow",
+ -13.40881633758545
+ ],
+ [
+ "▁Introduce",
+ -13.408817291259766
+ ],
+ [
+ "▁culprit",
+ -13.408817291259766
+ ],
+ [
+ "▁malheureusement",
+ -13.408817291259766
+ ],
+ [
+ "▁voudrai",
+ -13.408817291259766
+ ],
+ [
+ "Europäische",
+ -13.408825874328613
+ ],
+ [
+ "wunsch",
+ -13.408880233764648
+ ],
+ [
+ "▁înțeles",
+ -13.408892631530762
+ ],
+ [
+ "▁infestation",
+ -13.40889835357666
+ ],
+ [
+ "Bringing",
+ -13.409186363220215
+ ],
+ [
+ "▁Mehrheit",
+ -13.409229278564453
+ ],
+ [
+ "ски",
+ -13.409456253051758
+ ],
+ [
+ "▁procéder",
+ -13.409499168395996
+ ],
+ [
+ "grupului",
+ -13.409504890441895
+ ],
+ [
+ "▁dispoziti",
+ -13.40964412689209
+ ],
+ [
+ "▁snug",
+ -13.409950256347656
+ ],
+ [
+ "▁Afrika",
+ -13.41018295288086
+ ],
+ [
+ "▁Madagascar",
+ -13.41018295288086
+ ],
+ [
+ "Părinte",
+ -13.410195350646973
+ ],
+ [
+ "▁Clayton",
+ -13.410223960876465
+ ],
+ [
+ "▁antagonist",
+ -13.410239219665527
+ ],
+ [
+ "termeni",
+ -13.410250663757324
+ ],
+ [
+ "▁Literary",
+ -13.410391807556152
+ ],
+ [
+ "▁Babylon",
+ -13.410452842712402
+ ],
+ [
+ "▁überprüfen",
+ -13.410865783691406
+ ],
+ [
+ "▁duminica",
+ -13.410879135131836
+ ],
+ [
+ "farbig",
+ -13.410970687866211
+ ],
+ [
+ "nennt",
+ -13.411064147949219
+ ],
+ [
+ "annual",
+ -13.411487579345703
+ ],
+ [
+ "▁Qualcomm",
+ -13.41154956817627
+ ],
+ [
+ "▁Slovakia",
+ -13.41154956817627
+ ],
+ [
+ "▁plictis",
+ -13.411552429199219
+ ],
+ [
+ "▁prairie",
+ -13.411554336547852
+ ],
+ [
+ "▁Schatten",
+ -13.411622047424316
+ ],
+ [
+ "▁compléter",
+ -13.41223430633545
+ ],
+ [
+ "inauguration",
+ -13.412376403808594
+ ],
+ [
+ "▁apărare",
+ -13.412407875061035
+ ],
+ [
+ "▁întăr",
+ -13.412412643432617
+ ],
+ [
+ "▁pronunciation",
+ -13.412919044494629
+ ],
+ [
+ "▁bewährt",
+ -13.412919998168945
+ ],
+ [
+ "▁Viertel",
+ -13.413084983825684
+ ],
+ [
+ "▁Heidi",
+ -13.413252830505371
+ ],
+ [
+ "▁Gummi",
+ -13.413507461547852
+ ],
+ [
+ "▁veggie",
+ -13.413552284240723
+ ],
+ [
+ "▁monsieur",
+ -13.413604736328125
+ ],
+ [
+ "éveil",
+ -13.413630485534668
+ ],
+ [
+ "shipments",
+ -13.413928985595703
+ ],
+ [
+ "▁Medikamente",
+ -13.414290428161621
+ ],
+ [
+ "▁Johannesburg",
+ -13.414314270019531
+ ],
+ [
+ "▁ermittelt",
+ -13.414321899414062
+ ],
+ [
+ "▁bataille",
+ -13.414440155029297
+ ],
+ [
+ "extrem",
+ -13.414609909057617
+ ],
+ [
+ "▁1:2",
+ -13.414671897888184
+ ],
+ [
+ "Array",
+ -13.414725303649902
+ ],
+ [
+ "▁portail",
+ -13.414857864379883
+ ],
+ [
+ "▁găzdui",
+ -13.414977073669434
+ ],
+ [
+ "▁Calcium",
+ -13.41497802734375
+ ],
+ [
+ "▁Correction",
+ -13.415104866027832
+ ],
+ [
+ "bureaux",
+ -13.41528034210205
+ ],
+ [
+ "bestselling",
+ -13.415338516235352
+ ],
+ [
+ "Übungen",
+ -13.415420532226562
+ ],
+ [
+ "paramètres",
+ -13.415633201599121
+ ],
+ [
+ "▁Provincial",
+ -13.415663719177246
+ ],
+ [
+ "▁outrageous",
+ -13.415680885314941
+ ],
+ [
+ "▁Giveaway",
+ -13.415775299072266
+ ],
+ [
+ "▁LGBTQ",
+ -13.41589641571045
+ ],
+ [
+ "geklärt",
+ -13.416854858398438
+ ],
+ [
+ "▁Karlsruhe",
+ -13.417038917541504
+ ],
+ [
+ "▁esențial",
+ -13.417038917541504
+ ],
+ [
+ "avancée",
+ -13.41703987121582
+ ],
+ [
+ "hesitant",
+ -13.417040824890137
+ ],
+ [
+ "enlarged",
+ -13.417069435119629
+ ],
+ [
+ "▁inherit",
+ -13.417121887207031
+ ],
+ [
+ "Food",
+ -13.4171724319458
+ ],
+ [
+ "bucuria",
+ -13.417181015014648
+ ],
+ [
+ "▁BTW",
+ -13.417400360107422
+ ],
+ [
+ "associe",
+ -13.417579650878906
+ ],
+ [
+ "▁Möchte",
+ -13.417742729187012
+ ],
+ [
+ "demokrat",
+ -13.417789459228516
+ ],
+ [
+ "Turcia",
+ -13.417964935302734
+ ],
+ [
+ "forged",
+ -13.418370246887207
+ ],
+ [
+ "▁Zhao",
+ -13.418442726135254
+ ],
+ [
+ "▁cherries",
+ -13.418556213378906
+ ],
+ [
+ "▁evangelical",
+ -13.418631553649902
+ ],
+ [
+ "▁jüng",
+ -13.418792724609375
+ ],
+ [
+ "spans",
+ -13.41880989074707
+ ],
+ [
+ "▁străluc",
+ -13.41888427734375
+ ],
+ [
+ "▁geschie",
+ -13.41893196105957
+ ],
+ [
+ "▁Tattoo",
+ -13.419112205505371
+ ],
+ [
+ "sanitary",
+ -13.419114112854004
+ ],
+ [
+ "▁biopsy",
+ -13.419353485107422
+ ],
+ [
+ "▁imprumut",
+ -13.419795036315918
+ ],
+ [
+ "▁unreasonable",
+ -13.419795036315918
+ ],
+ [
+ "Funktion",
+ -13.419800758361816
+ ],
+ [
+ "▁prohibition",
+ -13.419904708862305
+ ],
+ [
+ "▁Prezent",
+ -13.419939041137695
+ ],
+ [
+ "boosted",
+ -13.419967651367188
+ ],
+ [
+ "▁chalet",
+ -13.420382499694824
+ ],
+ [
+ "▁tanar",
+ -13.420450210571289
+ ],
+ [
+ "Faktoren",
+ -13.420489311218262
+ ],
+ [
+ "▁Mozilla",
+ -13.420550346374512
+ ],
+ [
+ "▁Lambert",
+ -13.420760154724121
+ ],
+ [
+ "▁Cruci",
+ -13.420927047729492
+ ],
+ [
+ "▁Flugzeug",
+ -13.421198844909668
+ ],
+ [
+ "reassure",
+ -13.421205520629883
+ ],
+ [
+ "envisioned",
+ -13.421542167663574
+ ],
+ [
+ "Traditionally",
+ -13.421773910522461
+ ],
+ [
+ "▁parametri",
+ -13.42185115814209
+ ],
+ [
+ "▁unicorn",
+ -13.421891212463379
+ ],
+ [
+ "▁adéquat",
+ -13.421894073486328
+ ],
+ [
+ "▁Colonial",
+ -13.421915054321289
+ ],
+ [
+ "▁Kwa",
+ -13.422097206115723
+ ],
+ [
+ "▁SERV",
+ -13.422333717346191
+ ],
+ [
+ "tourism",
+ -13.422627449035645
+ ],
+ [
+ "▁Kiev",
+ -13.422974586486816
+ ],
+ [
+ "heightened",
+ -13.42309284210205
+ ],
+ [
+ "circulating",
+ -13.423099517822266
+ ],
+ [
+ "▁Kreditkarte",
+ -13.42310619354248
+ ],
+ [
+ "gedruckt",
+ -13.423110008239746
+ ],
+ [
+ "▁Depend",
+ -13.423120498657227
+ ],
+ [
+ "Style",
+ -13.423196792602539
+ ],
+ [
+ "▁Rettungs",
+ -13.42325496673584
+ ],
+ [
+ "wrongful",
+ -13.423418998718262
+ ],
+ [
+ "▁devour",
+ -13.423453330993652
+ ],
+ [
+ "▁manevr",
+ -13.423582077026367
+ ],
+ [
+ "carora",
+ -13.423628807067871
+ ],
+ [
+ "erfolgreichen",
+ -13.423723220825195
+ ],
+ [
+ "überwiegend",
+ -13.423942565917969
+ ],
+ [
+ "▁Sauvignon",
+ -13.423942565917969
+ ],
+ [
+ "händler",
+ -13.423944473266602
+ ],
+ [
+ "▁annotation",
+ -13.424009323120117
+ ],
+ [
+ "▁expans",
+ -13.424020767211914
+ ],
+ [
+ "▁recital",
+ -13.424080848693848
+ ],
+ [
+ "inhabited",
+ -13.424367904663086
+ ],
+ [
+ "OnePlus",
+ -13.424549102783203
+ ],
+ [
+ "Gästen",
+ -13.424588203430176
+ ],
+ [
+ "beliebig",
+ -13.424613952636719
+ ],
+ [
+ "▁Anonymous",
+ -13.424635887145996
+ ],
+ [
+ "▁Ansprechpartner",
+ -13.424635887145996
+ ],
+ [
+ "▁tamb",
+ -13.42464542388916
+ ],
+ [
+ "estimating",
+ -13.424670219421387
+ ],
+ [
+ "frequent",
+ -13.424769401550293
+ ],
+ [
+ "▁disciplin",
+ -13.425241470336914
+ ],
+ [
+ "▁plombier",
+ -13.425329208374023
+ ],
+ [
+ "▁teoretic",
+ -13.42533016204834
+ ],
+ [
+ "greift",
+ -13.425339698791504
+ ],
+ [
+ "▁Einschränkung",
+ -13.42537784576416
+ ],
+ [
+ "obscur",
+ -13.426115989685059
+ ],
+ [
+ "architecte",
+ -13.426233291625977
+ ],
+ [
+ "▁détour",
+ -13.42647647857666
+ ],
+ [
+ "▁spaghetti",
+ -13.426717758178711
+ ],
+ [
+ "croft",
+ -13.42693042755127
+ ],
+ [
+ "▁Grammar",
+ -13.426953315734863
+ ],
+ [
+ "▁investitii",
+ -13.427062034606934
+ ],
+ [
+ "▁glorif",
+ -13.427067756652832
+ ],
+ [
+ "architekt",
+ -13.427412033081055
+ ],
+ [
+ "Oricum",
+ -13.427451133728027
+ ],
+ [
+ "▁bruise",
+ -13.427692413330078
+ ],
+ [
+ "▁McCarthy",
+ -13.428107261657715
+ ],
+ [
+ "▁Uruguay",
+ -13.428107261657715
+ ],
+ [
+ "Produsele",
+ -13.428109169006348
+ ],
+ [
+ "▁Comparison",
+ -13.42811107635498
+ ],
+ [
+ "▁fondamental",
+ -13.42811107635498
+ ],
+ [
+ "▁stradă",
+ -13.428115844726562
+ ],
+ [
+ "▁Countries",
+ -13.428131103515625
+ ],
+ [
+ "▁guéri",
+ -13.42825698852539
+ ],
+ [
+ "▁bâti",
+ -13.428339004516602
+ ],
+ [
+ "▁blunt",
+ -13.428515434265137
+ ],
+ [
+ "▁Sistem",
+ -13.428645133972168
+ ],
+ [
+ "▁Betroffenen",
+ -13.428803443908691
+ ],
+ [
+ "efectuare",
+ -13.428823471069336
+ ],
+ [
+ "▁scharf",
+ -13.428899765014648
+ ],
+ [
+ "naps",
+ -13.429057121276855
+ ],
+ [
+ "▁plaid",
+ -13.429163932800293
+ ],
+ [
+ "▁investiții",
+ -13.429367065429688
+ ],
+ [
+ "evenimentele",
+ -13.42948055267334
+ ],
+ [
+ "▁Phuket",
+ -13.429499626159668
+ ],
+ [
+ "▁testosterone",
+ -13.429499626159668
+ ],
+ [
+ "▁scaffold",
+ -13.429500579833984
+ ],
+ [
+ "▁rasch",
+ -13.430022239685059
+ ],
+ [
+ "▁adânc",
+ -13.430076599121094
+ ],
+ [
+ "atteinte",
+ -13.430228233337402
+ ],
+ [
+ "▁educație",
+ -13.430320739746094
+ ],
+ [
+ "▁leopard",
+ -13.430893898010254
+ ],
+ [
+ "▁superioare",
+ -13.430893898010254
+ ],
+ [
+ "▁téléchargement",
+ -13.430893898010254
+ ],
+ [
+ "▁Weapon",
+ -13.431103706359863
+ ],
+ [
+ "favourable",
+ -13.431336402893066
+ ],
+ [
+ "nourishing",
+ -13.43143367767334
+ ],
+ [
+ "▁verfolgt",
+ -13.43160629272461
+ ],
+ [
+ "▁tablou",
+ -13.431633949279785
+ ],
+ [
+ "Algérie",
+ -13.431657791137695
+ ],
+ [
+ "Islam",
+ -13.431700706481934
+ ],
+ [
+ "faser",
+ -13.431825637817383
+ ],
+ [
+ "rhythm",
+ -13.432214736938477
+ ],
+ [
+ "▁Anthropolog",
+ -13.432291030883789
+ ],
+ [
+ "▁clôtur",
+ -13.432291030883789
+ ],
+ [
+ "spüren",
+ -13.432291984558105
+ ],
+ [
+ "▁Architectural",
+ -13.432294845581055
+ ],
+ [
+ "▁imaginary",
+ -13.432368278503418
+ ],
+ [
+ "cône",
+ -13.432456016540527
+ ],
+ [
+ "▁snuggl",
+ -13.432744026184082
+ ],
+ [
+ "disadvantaged",
+ -13.432745933532715
+ ],
+ [
+ "radically",
+ -13.4329195022583
+ ],
+ [
+ "Première",
+ -13.433011054992676
+ ],
+ [
+ "▁combinaison",
+ -13.433027267456055
+ ],
+ [
+ "▁Algeria",
+ -13.43303108215332
+ ],
+ [
+ "▁Wände",
+ -13.43317985534668
+ ],
+ [
+ "aesthetically",
+ -13.43336009979248
+ ],
+ [
+ "▁McKe",
+ -13.433368682861328
+ ],
+ [
+ "interroge",
+ -13.433473587036133
+ ],
+ [
+ "exclusive",
+ -13.433475494384766
+ ],
+ [
+ "▁Thomson",
+ -13.433688163757324
+ ],
+ [
+ "▁Gujarat",
+ -13.43368911743164
+ ],
+ [
+ "irgendwo",
+ -13.433690071105957
+ ],
+ [
+ "Severin",
+ -13.433767318725586
+ ],
+ [
+ "▁imitation",
+ -13.433926582336426
+ ],
+ [
+ "constructed",
+ -13.434194564819336
+ ],
+ [
+ "▁Montpellier",
+ -13.434388160705566
+ ],
+ [
+ "cedent",
+ -13.434539794921875
+ ],
+ [
+ "accelerating",
+ -13.434563636779785
+ ],
+ [
+ "dommages",
+ -13.4346284866333
+ ],
+ [
+ "lideri",
+ -13.434730529785156
+ ],
+ [
+ "▁Millennium",
+ -13.435089111328125
+ ],
+ [
+ "▁imprisonment",
+ -13.435089111328125
+ ],
+ [
+ "machining",
+ -13.435111999511719
+ ],
+ [
+ "▁anxiet",
+ -13.43521499633789
+ ],
+ [
+ "Contains",
+ -13.435298919677734
+ ],
+ [
+ "pleade",
+ -13.435563087463379
+ ],
+ [
+ "DOWN",
+ -13.43564510345459
+ ],
+ [
+ "geschehen",
+ -13.435797691345215
+ ],
+ [
+ "restaurant",
+ -13.435811996459961
+ ],
+ [
+ "Totusi",
+ -13.435839653015137
+ ],
+ [
+ "amintesc",
+ -13.436158180236816
+ ],
+ [
+ "▁Crisp",
+ -13.436233520507812
+ ],
+ [
+ "aduse",
+ -13.436278343200684
+ ],
+ [
+ "▁imposé",
+ -13.436351776123047
+ ],
+ [
+ "Jubiläum",
+ -13.436490058898926
+ ],
+ [
+ "▁Plaintiff",
+ -13.436491012573242
+ ],
+ [
+ "▁authoritative",
+ -13.436491966247559
+ ],
+ [
+ "▁rendition",
+ -13.436633110046387
+ ],
+ [
+ "Royce",
+ -13.436707496643066
+ ],
+ [
+ "1996)",
+ -13.436724662780762
+ ],
+ [
+ "Asociația",
+ -13.437192916870117
+ ],
+ [
+ "▁Gluten",
+ -13.437264442443848
+ ],
+ [
+ "feature",
+ -13.43741226196289
+ ],
+ [
+ "Behavioral",
+ -13.437454223632812
+ ],
+ [
+ "tearing",
+ -13.437763214111328
+ ],
+ [
+ "▁Entfernung",
+ -13.437894821166992
+ ],
+ [
+ "▁Responsibility",
+ -13.437894821166992
+ ],
+ [
+ "▁negligent",
+ -13.437894821166992
+ ],
+ [
+ "▁syllabus",
+ -13.437894821166992
+ ],
+ [
+ "▁Cycling",
+ -13.437895774841309
+ ],
+ [
+ "generell",
+ -13.438114166259766
+ ],
+ [
+ "customised",
+ -13.438392639160156
+ ],
+ [
+ "Management",
+ -13.43850326538086
+ ],
+ [
+ "▁timid",
+ -13.438518524169922
+ ],
+ [
+ "Tagged",
+ -13.438730239868164
+ ],
+ [
+ "▁susţinut",
+ -13.438809394836426
+ ],
+ [
+ "anchored",
+ -13.43892765045166
+ ],
+ [
+ "alternating",
+ -13.439055442810059
+ ],
+ [
+ "▁obligatoriu",
+ -13.439300537109375
+ ],
+ [
+ "▁reinstate",
+ -13.439456939697266
+ ],
+ [
+ "Können",
+ -13.43946361541748
+ ],
+ [
+ "▁Paol",
+ -13.439596176147461
+ ],
+ [
+ "öhr",
+ -13.439603805541992
+ ],
+ [
+ "▁Asociati",
+ -13.439876556396484
+ ],
+ [
+ "▁commenc",
+ -13.440285682678223
+ ],
+ [
+ "reinigt",
+ -13.440293312072754
+ ],
+ [
+ "commended",
+ -13.440350532531738
+ ],
+ [
+ "▁Proceed",
+ -13.440675735473633
+ ],
+ [
+ "beutel",
+ -13.440702438354492
+ ],
+ [
+ "▁Experimental",
+ -13.44070816040039
+ ],
+ [
+ "▁constellation",
+ -13.44070816040039
+ ],
+ [
+ "▁gepflegt",
+ -13.44070816040039
+ ],
+ [
+ "▁Ergänzung",
+ -13.440709114074707
+ ],
+ [
+ "Judith",
+ -13.440713882446289
+ ],
+ [
+ "▁Quartet",
+ -13.440720558166504
+ ],
+ [
+ "complemented",
+ -13.440742492675781
+ ],
+ [
+ "ausbildung",
+ -13.440750122070312
+ ],
+ [
+ "▁uncertainties",
+ -13.44077205657959
+ ],
+ [
+ "▁humiliat",
+ -13.440914154052734
+ ],
+ [
+ "luta",
+ -13.441121101379395
+ ],
+ [
+ "▁complexion",
+ -13.441482543945312
+ ],
+ [
+ "Serviciul",
+ -13.441612243652344
+ ],
+ [
+ "▁Toast",
+ -13.441722869873047
+ ],
+ [
+ "ummies",
+ -13.442425727844238
+ ],
+ [
+ "▁irit",
+ -13.442463874816895
+ ],
+ [
+ "producing",
+ -13.442585945129395
+ ],
+ [
+ "amenajare",
+ -13.442825317382812
+ ],
+ [
+ "▁béton",
+ -13.442828178405762
+ ],
+ [
+ "▁serpent",
+ -13.442851066589355
+ ],
+ [
+ "▁vizită",
+ -13.442996978759766
+ ],
+ [
+ "▁Beamte",
+ -13.443017959594727
+ ],
+ [
+ "▁Füße",
+ -13.443166732788086
+ ],
+ [
+ "▁Norwich",
+ -13.443531036376953
+ ],
+ [
+ "▁acronym",
+ -13.443531036376953
+ ],
+ [
+ "▁eradicate",
+ -13.443531036376953
+ ],
+ [
+ "▁solidarité",
+ -13.44353199005127
+ ],
+ [
+ "▁eggplant",
+ -13.443582534790039
+ ],
+ [
+ "▁sailors",
+ -13.443619728088379
+ ],
+ [
+ "waschen",
+ -13.444538116455078
+ ],
+ [
+ "Editura",
+ -13.444757461547852
+ ],
+ [
+ "▁erwerben",
+ -13.444944381713867
+ ],
+ [
+ "▁unconventional",
+ -13.444944381713867
+ ],
+ [
+ "▁boulder",
+ -13.444948196411133
+ ],
+ [
+ "Diplom",
+ -13.445013046264648
+ ],
+ [
+ "influx",
+ -13.446162223815918
+ ],
+ [
+ "▁Twelve",
+ -13.446361541748047
+ ],
+ [
+ "▁Sexual",
+ -13.44636344909668
+ ],
+ [
+ "numite",
+ -13.446369171142578
+ ],
+ [
+ "▁kontaktieren",
+ -13.446370124816895
+ ],
+ [
+ "▁strâns",
+ -13.44637680053711
+ ],
+ [
+ "▁précisément",
+ -13.446382522583008
+ ],
+ [
+ "empfindlich",
+ -13.446405410766602
+ ],
+ [
+ "▁divulg",
+ -13.446490287780762
+ ],
+ [
+ "▁delicat",
+ -13.446539878845215
+ ],
+ [
+ "compete",
+ -13.446542739868164
+ ],
+ [
+ "▁implique",
+ -13.446616172790527
+ ],
+ [
+ "implantation",
+ -13.44672966003418
+ ],
+ [
+ "frères",
+ -13.447328567504883
+ ],
+ [
+ "shedding",
+ -13.44758415222168
+ ],
+ [
+ "découvrez",
+ -13.447657585144043
+ ],
+ [
+ "rith",
+ -13.447735786437988
+ ],
+ [
+ "▁réglementation",
+ -13.447778701782227
+ ],
+ [
+ "▁transistor",
+ -13.447785377502441
+ ],
+ [
+ "inflated",
+ -13.447792053222656
+ ],
+ [
+ "▁Bluff",
+ -13.447887420654297
+ ],
+ [
+ "▁Aquarium",
+ -13.448526382446289
+ ],
+ [
+ "▁mananc",
+ -13.448638916015625
+ ],
+ [
+ "▁disinfect",
+ -13.448700904846191
+ ],
+ [
+ "tuft",
+ -13.448740005493164
+ ],
+ [
+ "Public",
+ -13.449081420898438
+ ],
+ [
+ "conceivabl",
+ -13.449197769165039
+ ],
+ [
+ "▁Cadillac",
+ -13.449197769165039
+ ],
+ [
+ "Assassin",
+ -13.449199676513672
+ ],
+ [
+ "issuance",
+ -13.449252128601074
+ ],
+ [
+ "▁Achtung",
+ -13.449287414550781
+ ],
+ [
+ "▁grundlegend",
+ -13.449909210205078
+ ],
+ [
+ "▁Băsescu",
+ -13.449910163879395
+ ],
+ [
+ "schaden",
+ -13.45014476776123
+ ],
+ [
+ "coached",
+ -13.450409889221191
+ ],
+ [
+ "▁betreffend",
+ -13.45046329498291
+ ],
+ [
+ "ergebnis",
+ -13.450541496276855
+ ],
+ [
+ "▁Lieutenant",
+ -13.4506196975708
+ ],
+ [
+ "WORLD",
+ -13.450620651245117
+ ],
+ [
+ "▁Moroccan",
+ -13.450620651245117
+ ],
+ [
+ "▁Butterfly",
+ -13.450621604919434
+ ],
+ [
+ "would",
+ -13.450737953186035
+ ],
+ [
+ "▁Metropol",
+ -13.451025009155273
+ ],
+ [
+ "lexic",
+ -13.451192855834961
+ ],
+ [
+ "comunitatea",
+ -13.45124340057373
+ ],
+ [
+ "vapeur",
+ -13.451456069946289
+ ],
+ [
+ "4.000",
+ -13.451559066772461
+ ],
+ [
+ "Pentru",
+ -13.451581954956055
+ ],
+ [
+ "üblichen",
+ -13.451613426208496
+ ],
+ [
+ "▁Général",
+ -13.451770782470703
+ ],
+ [
+ "▁Versailles",
+ -13.452046394348145
+ ],
+ [
+ "▁engraving",
+ -13.452046394348145
+ ],
+ [
+ "▁pédagogique",
+ -13.452192306518555
+ ],
+ [
+ "▁Policies",
+ -13.452759742736816
+ ],
+ [
+ "descending",
+ -13.453235626220703
+ ],
+ [
+ "stärkt",
+ -13.453349113464355
+ ],
+ [
+ "▁démocratie",
+ -13.453470230102539
+ ],
+ [
+ "▁granddaughter",
+ -13.453470230102539
+ ],
+ [
+ "▁buffalo",
+ -13.453474998474121
+ ],
+ [
+ "Datorita",
+ -13.45347785949707
+ ],
+ [
+ "hydroxy",
+ -13.453537940979004
+ ],
+ [
+ "▁ganduri",
+ -13.453566551208496
+ ],
+ [
+ "▁hijack",
+ -13.453624725341797
+ ],
+ [
+ "zahn",
+ -13.453699111938477
+ ],
+ [
+ "poziția",
+ -13.45406436920166
+ ],
+ [
+ "▁Zähne",
+ -13.454184532165527
+ ],
+ [
+ "▁grossesse",
+ -13.454296112060547
+ ],
+ [
+ "embassy",
+ -13.4548978805542
+ ],
+ [
+ "▁cérémonie",
+ -13.4548978805542
+ ],
+ [
+ "Rhône",
+ -13.454898834228516
+ ],
+ [
+ "▁Cabernet",
+ -13.454898834228516
+ ],
+ [
+ "▁Namibia",
+ -13.454902648925781
+ ],
+ [
+ "▁pedestal",
+ -13.454902648925781
+ ],
+ [
+ "▁Fighting",
+ -13.45490550994873
+ ],
+ [
+ "▁Threat",
+ -13.454962730407715
+ ],
+ [
+ "▁ideological",
+ -13.455047607421875
+ ],
+ [
+ "▁restitu",
+ -13.455183029174805
+ ],
+ [
+ "gelangt",
+ -13.455510139465332
+ ],
+ [
+ "Mitgliedern",
+ -13.455537796020508
+ ],
+ [
+ "acquérir",
+ -13.455613136291504
+ ],
+ [
+ "▁inferioar",
+ -13.45561695098877
+ ],
+ [
+ "Thierry",
+ -13.455619812011719
+ ],
+ [
+ "▁Entspannung",
+ -13.455638885498047
+ ],
+ [
+ "frequency",
+ -13.45566177368164
+ ],
+ [
+ "▁Fluid",
+ -13.455686569213867
+ ],
+ [
+ "▁betreut",
+ -13.455901145935059
+ ],
+ [
+ "Biological",
+ -13.455965995788574
+ ],
+ [
+ "▁Constanţa",
+ -13.456328392028809
+ ],
+ [
+ "▁beschäftigen",
+ -13.456328392028809
+ ],
+ [
+ "▁undesirable",
+ -13.456328392028809
+ ],
+ [
+ "▁protégé",
+ -13.456365585327148
+ ],
+ [
+ "▁nautical",
+ -13.456474304199219
+ ],
+ [
+ "▁sniff",
+ -13.456507682800293
+ ],
+ [
+ "Decizi",
+ -13.456510543823242
+ ],
+ [
+ "▁căldur",
+ -13.45706558227539
+ ],
+ [
+ "▁ideologi",
+ -13.457335472106934
+ ],
+ [
+ "Fraktion",
+ -13.457545280456543
+ ],
+ [
+ "collegiate",
+ -13.45776081085205
+ ],
+ [
+ "▁sănătos",
+ -13.45776081085205
+ ],
+ [
+ "▁Observatory",
+ -13.45776653289795
+ ],
+ [
+ "▁saturation",
+ -13.457769393920898
+ ],
+ [
+ "organizate",
+ -13.457771301269531
+ ],
+ [
+ "mergem",
+ -13.458321571350098
+ ],
+ [
+ "Publish",
+ -13.458451271057129
+ ],
+ [
+ "▁rattle",
+ -13.458460807800293
+ ],
+ [
+ "▁întâlniri",
+ -13.458663940429688
+ ],
+ [
+ "emporte",
+ -13.458741188049316
+ ],
+ [
+ "▁înscris",
+ -13.459046363830566
+ ],
+ [
+ "▁Patterson",
+ -13.459195137023926
+ ],
+ [
+ "▁ehrenamtlich",
+ -13.459195137023926
+ ],
+ [
+ "linux",
+ -13.459213256835938
+ ],
+ [
+ "conduire",
+ -13.45921802520752
+ ],
+ [
+ "▁absolven",
+ -13.459223747253418
+ ],
+ [
+ "▁einzigartig",
+ -13.459598541259766
+ ],
+ [
+ "▁_____",
+ -13.459803581237793
+ ],
+ [
+ "▁Beschäftigung",
+ -13.459912300109863
+ ],
+ [
+ "▁erfasst",
+ -13.459927558898926
+ ],
+ [
+ "▁Datum",
+ -13.459992408752441
+ ],
+ [
+ "raportul",
+ -13.460284233093262
+ ],
+ [
+ "ennemi",
+ -13.460460662841797
+ ],
+ [
+ "default",
+ -13.460643768310547
+ ],
+ [
+ "icillin",
+ -13.46066951751709
+ ],
+ [
+ "▁diamant",
+ -13.460671424865723
+ ],
+ [
+ "amerika",
+ -13.460684776306152
+ ],
+ [
+ "▁pescuit",
+ -13.46070384979248
+ ],
+ [
+ "▁grappl",
+ -13.460797309875488
+ ],
+ [
+ "▁Homeland",
+ -13.46082592010498
+ ],
+ [
+ "▁tromb",
+ -13.46112060546875
+ ],
+ [
+ "▁reduzieren",
+ -13.461349487304688
+ ],
+ [
+ "▁Statut",
+ -13.461593627929688
+ ],
+ [
+ "booming",
+ -13.461670875549316
+ ],
+ [
+ "fenced",
+ -13.461723327636719
+ ],
+ [
+ "measure",
+ -13.461888313293457
+ ],
+ [
+ "témoin",
+ -13.462069511413574
+ ],
+ [
+ "▁Inventory",
+ -13.462069511413574
+ ],
+ [
+ "▁circonstance",
+ -13.462069511413574
+ ],
+ [
+ "▁téléphonique",
+ -13.462069511413574
+ ],
+ [
+ "▁împiedic",
+ -13.46207046508789
+ ],
+ [
+ "▁Settlement",
+ -13.462072372436523
+ ],
+ [
+ "kannte",
+ -13.462076187133789
+ ],
+ [
+ "▁substantive",
+ -13.462385177612305
+ ],
+ [
+ "miterea",
+ -13.462642669677734
+ ],
+ [
+ "▁noştri",
+ -13.462790489196777
+ ],
+ [
+ "▁plăcere",
+ -13.462791442871094
+ ],
+ [
+ "▁eticheta",
+ -13.462823867797852
+ ],
+ [
+ "quickest",
+ -13.462993621826172
+ ],
+ [
+ "▁pasageri",
+ -13.463089942932129
+ ],
+ [
+ "▁Publi",
+ -13.463495254516602
+ ],
+ [
+ "▁Suzanne",
+ -13.463509559631348
+ ],
+ [
+ "▁bucătări",
+ -13.463509559631348
+ ],
+ [
+ "Regulatory",
+ -13.463510513305664
+ ],
+ [
+ "▁Mandarin",
+ -13.463647842407227
+ ],
+ [
+ "surgical",
+ -13.463947296142578
+ ],
+ [
+ "▁Smash",
+ -13.463950157165527
+ ],
+ [
+ "▁mândr",
+ -13.46403694152832
+ ],
+ [
+ "▁Unterkunft",
+ -13.464315414428711
+ ],
+ [
+ "moos",
+ -13.464374542236328
+ ],
+ [
+ "Camere",
+ -13.464510917663574
+ ],
+ [
+ "/03/",
+ -13.464651107788086
+ ],
+ [
+ "▁ethno",
+ -13.464677810668945
+ ],
+ [
+ "▁Eröffnung",
+ -13.46495246887207
+ ],
+ [
+ "▁Snyder",
+ -13.46495246887207
+ ],
+ [
+ "▁Wilmington",
+ -13.46495246887207
+ ],
+ [
+ "▁Canberra",
+ -13.464953422546387
+ ],
+ [
+ "▁Tahoe",
+ -13.464953422546387
+ ],
+ [
+ "▁slippery",
+ -13.464953422546387
+ ],
+ [
+ "▁Snake",
+ -13.464957237243652
+ ],
+ [
+ "▁turmeric",
+ -13.464963912963867
+ ],
+ [
+ "▁Cartoon",
+ -13.46499252319336
+ ],
+ [
+ "▁scrisoare",
+ -13.46500015258789
+ ],
+ [
+ "▁reprend",
+ -13.465425491333008
+ ],
+ [
+ "▁Konkurrenz",
+ -13.46567440032959
+ ],
+ [
+ "▁raisins",
+ -13.465693473815918
+ ],
+ [
+ "▁Werkstatt",
+ -13.465713500976562
+ ],
+ [
+ "▁agresiv",
+ -13.465795516967773
+ ],
+ [
+ "hugs",
+ -13.46615219116211
+ ],
+ [
+ "cazurile",
+ -13.46618938446045
+ ],
+ [
+ "spirited",
+ -13.466232299804688
+ ],
+ [
+ "▁britisch",
+ -13.466307640075684
+ ],
+ [
+ "spritz",
+ -13.466367721557617
+ ],
+ [
+ "auxiliary",
+ -13.46639633178711
+ ],
+ [
+ "interprétation",
+ -13.46639633178711
+ ],
+ [
+ "▁verbindet",
+ -13.46639633178711
+ ],
+ [
+ "▁fuzzy",
+ -13.466429710388184
+ ],
+ [
+ "▁turmoil",
+ -13.466432571411133
+ ],
+ [
+ "▁redefine",
+ -13.466819763183594
+ ],
+ [
+ "▁Kiwi",
+ -13.466890335083008
+ ],
+ [
+ "oiseaux",
+ -13.46712875366211
+ ],
+ [
+ "▁pamper",
+ -13.467146873474121
+ ],
+ [
+ "▁desfaso",
+ -13.46719741821289
+ ],
+ [
+ "▁pragu",
+ -13.467576026916504
+ ],
+ [
+ "prevenirea",
+ -13.467730522155762
+ ],
+ [
+ "▁convergence",
+ -13.467846870422363
+ ],
+ [
+ "tufted",
+ -13.467878341674805
+ ],
+ [
+ "brewed",
+ -13.467981338500977
+ ],
+ [
+ "villagers",
+ -13.468003273010254
+ ],
+ [
+ "▁Irving",
+ -13.468170166015625
+ ],
+ [
+ "nigsten",
+ -13.468660354614258
+ ],
+ [
+ "▁embod",
+ -13.468742370605469
+ ],
+ [
+ "Alicia",
+ -13.468938827514648
+ ],
+ [
+ "probably",
+ -13.469009399414062
+ ],
+ [
+ "divider",
+ -13.46904468536377
+ ],
+ [
+ "Attempt",
+ -13.469223022460938
+ ],
+ [
+ "▁Cognitive",
+ -13.469292640686035
+ ],
+ [
+ "▁Recognition",
+ -13.469292640686035
+ ],
+ [
+ "▁concierge",
+ -13.469292640686035
+ ],
+ [
+ "▁Semester",
+ -13.4692964553833
+ ],
+ [
+ "Economie",
+ -13.469417572021484
+ ],
+ [
+ "sortiment",
+ -13.469460487365723
+ ],
+ [
+ "shortest",
+ -13.46961498260498
+ ],
+ [
+ "üchtig",
+ -13.469650268554688
+ ],
+ [
+ "▁conveyanc",
+ -13.469978332519531
+ ],
+ [
+ "▁Ferdinand",
+ -13.470017433166504
+ ],
+ [
+ "▁permanence",
+ -13.470019340515137
+ ],
+ [
+ "▁incadr",
+ -13.470145225524902
+ ],
+ [
+ "▁estrogen",
+ -13.470290184020996
+ ],
+ [
+ "February",
+ -13.470661163330078
+ ],
+ [
+ "gedeckt",
+ -13.470704078674316
+ ],
+ [
+ "▁reagieren",
+ -13.470743179321289
+ ],
+ [
+ "▁meditate",
+ -13.470980644226074
+ ],
+ [
+ "simulated",
+ -13.471010208129883
+ ],
+ [
+ "▁supprimer",
+ -13.471468925476074
+ ],
+ [
+ "▁bumbac",
+ -13.47146987915039
+ ],
+ [
+ "▁vânzări",
+ -13.471477508544922
+ ],
+ [
+ "▁Kapitel",
+ -13.471478462219238
+ ],
+ [
+ "▁Weltkrieg",
+ -13.471513748168945
+ ],
+ [
+ "déposer",
+ -13.471674919128418
+ ],
+ [
+ "Asus",
+ -13.4718017578125
+ ],
+ [
+ "▁Communicat",
+ -13.471851348876953
+ ],
+ [
+ "Finished",
+ -13.47188949584961
+ ],
+ [
+ "▁Telegraph",
+ -13.472054481506348
+ ],
+ [
+ "▁Competitive",
+ -13.472196578979492
+ ],
+ [
+ "▁collectivités",
+ -13.472197532653809
+ ],
+ [
+ "▁protège",
+ -13.472199440002441
+ ],
+ [
+ "▁scallop",
+ -13.472219467163086
+ ],
+ [
+ "Happy",
+ -13.472335815429688
+ ],
+ [
+ "tehnică",
+ -13.472352981567383
+ ],
+ [
+ "▁Gestalt",
+ -13.47270393371582
+ ],
+ [
+ "▁benign",
+ -13.47295093536377
+ ],
+ [
+ "kraut",
+ -13.473149299621582
+ ],
+ [
+ "louer",
+ -13.473221778869629
+ ],
+ [
+ "▁Printr",
+ -13.47326946258545
+ ],
+ [
+ "mputation",
+ -13.473346710205078
+ ],
+ [
+ "▁dicke",
+ -13.473429679870605
+ ],
+ [
+ "▁Halifax",
+ -13.473650932312012
+ ],
+ [
+ "▁bounty",
+ -13.473650932312012
+ ],
+ [
+ "▁cauliflower",
+ -13.473650932312012
+ ],
+ [
+ "▁Survival",
+ -13.473654747009277
+ ],
+ [
+ "▁Chandler",
+ -13.473684310913086
+ ],
+ [
+ "▁bemüh",
+ -13.473760604858398
+ ],
+ [
+ "phro",
+ -13.473855972290039
+ ],
+ [
+ "Friday",
+ -13.474018096923828
+ ],
+ [
+ "particularly",
+ -13.474032402038574
+ ],
+ [
+ "arteries",
+ -13.474197387695312
+ ],
+ [
+ "Lösung",
+ -13.474771499633789
+ ],
+ [
+ "▁causal",
+ -13.474817276000977
+ ],
+ [
+ "▁recueilli",
+ -13.475075721740723
+ ],
+ [
+ "Stylish",
+ -13.47510814666748
+ ],
+ [
+ "schränke",
+ -13.47510814666748
+ ],
+ [
+ "▁francophone",
+ -13.47510814666748
+ ],
+ [
+ "▁limousine",
+ -13.47510814666748
+ ],
+ [
+ "▁statistiques",
+ -13.47510814666748
+ ],
+ [
+ "▁Kleider",
+ -13.475111961364746
+ ],
+ [
+ "▁dunkel",
+ -13.475127220153809
+ ],
+ [
+ "tätigkeit",
+ -13.475190162658691
+ ],
+ [
+ "▁punished",
+ -13.475257873535156
+ ],
+ [
+ "▁implică",
+ -13.475539207458496
+ ],
+ [
+ "▁inițial",
+ -13.475568771362305
+ ],
+ [
+ "▁Eminescu",
+ -13.475837707519531
+ ],
+ [
+ "▁expliqué",
+ -13.475837707519531
+ ],
+ [
+ "▁Eduard",
+ -13.475839614868164
+ ],
+ [
+ "▁psychologique",
+ -13.475870132446289
+ ],
+ [
+ "▁protejeaz",
+ -13.476580619812012
+ ],
+ [
+ "spül",
+ -13.476709365844727
+ ],
+ [
+ "▁Virtu",
+ -13.477021217346191
+ ],
+ [
+ "▁régulière",
+ -13.477044105529785
+ ],
+ [
+ "▁Outreach",
+ -13.477130889892578
+ ],
+ [
+ "▁Apprentice",
+ -13.47729778289795
+ ],
+ [
+ "▁compréhension",
+ -13.47729778289795
+ ],
+ [
+ "▁zwölf",
+ -13.47729778289795
+ ],
+ [
+ "Surgical",
+ -13.477315902709961
+ ],
+ [
+ "latéral",
+ -13.477417945861816
+ ],
+ [
+ "▁Ceremony",
+ -13.47803020477295
+ ],
+ [
+ "▁Shampoo",
+ -13.47803783416748
+ ],
+ [
+ "Global",
+ -13.478239059448242
+ ],
+ [
+ "▁paradis",
+ -13.478302955627441
+ ],
+ [
+ "Developed",
+ -13.478493690490723
+ ],
+ [
+ "▁figurine",
+ -13.478549003601074
+ ],
+ [
+ "sujets",
+ -13.478574752807617
+ ],
+ [
+ "▁Naomi",
+ -13.478772163391113
+ ],
+ [
+ "financed",
+ -13.478838920593262
+ ],
+ [
+ "forestry",
+ -13.478896141052246
+ ],
+ [
+ "▁Anregung",
+ -13.479494094848633
+ ],
+ [
+ "▁spectateur",
+ -13.479804039001465
+ ],
+ [
+ "▁exercitii",
+ -13.479815483093262
+ ],
+ [
+ "▁russisch",
+ -13.479888916015625
+ ],
+ [
+ "gefunden",
+ -13.479988098144531
+ ],
+ [
+ "schleunig",
+ -13.480225563049316
+ ],
+ [
+ "▁géographique",
+ -13.480225563049316
+ ],
+ [
+ "▁Delphi",
+ -13.480317115783691
+ ],
+ [
+ "Freddie",
+ -13.4806489944458
+ ],
+ [
+ "▁muzici",
+ -13.480958938598633
+ ],
+ [
+ "▁Edmund",
+ -13.48095989227295
+ ],
+ [
+ "finanzielle",
+ -13.481032371520996
+ ],
+ [
+ "(2003)",
+ -13.481319427490234
+ ],
+ [
+ "accentuate",
+ -13.481437683105469
+ ],
+ [
+ "overlapping",
+ -13.48151969909668
+ ],
+ [
+ "▁Pluto",
+ -13.481595993041992
+ ],
+ [
+ "românii",
+ -13.481683731079102
+ ],
+ [
+ "▁Timişoara",
+ -13.48169231414795
+ ],
+ [
+ "▁poivr",
+ -13.481754302978516
+ ],
+ [
+ "▁repris",
+ -13.481852531433105
+ ],
+ [
+ "▁Geschlecht",
+ -13.482426643371582
+ ],
+ [
+ "▁thieves",
+ -13.482426643371582
+ ],
+ [
+ "▁Transformer",
+ -13.482431411743164
+ ],
+ [
+ "▁shortcomings",
+ -13.482438087463379
+ ],
+ [
+ "▁aptitude",
+ -13.48244571685791
+ ],
+ [
+ "pitfalls",
+ -13.482468605041504
+ ],
+ [
+ "▁manicure",
+ -13.482577323913574
+ ],
+ [
+ "mystical",
+ -13.482723236083984
+ ],
+ [
+ "▁abolish",
+ -13.482833862304688
+ ],
+ [
+ "▁Zielgruppe",
+ -13.482873916625977
+ ],
+ [
+ "▁naţionale",
+ -13.483160972595215
+ ],
+ [
+ "▁trandafir",
+ -13.483160972595215
+ ],
+ [
+ "▁matematic",
+ -13.483193397521973
+ ],
+ [
+ "▁Hirsch",
+ -13.483257293701172
+ ],
+ [
+ "Fahr",
+ -13.483458518981934
+ ],
+ [
+ "connaissent",
+ -13.483476638793945
+ ],
+ [
+ "browned",
+ -13.483846664428711
+ ],
+ [
+ "▁bearbeitet",
+ -13.483881950378418
+ ],
+ [
+ "▁usturoi",
+ -13.483896255493164
+ ],
+ [
+ "▁Surprise",
+ -13.48389720916748
+ ],
+ [
+ "▁Tehran",
+ -13.483899116516113
+ ],
+ [
+ "▁BLACK",
+ -13.483901023864746
+ ],
+ [
+ "▁abonament",
+ -13.483904838562012
+ ],
+ [
+ "▁mêl",
+ -13.483972549438477
+ ],
+ [
+ "Angebot",
+ -13.484091758728027
+ ],
+ [
+ "ajungi",
+ -13.48410415649414
+ ],
+ [
+ "▁Woodland",
+ -13.48420524597168
+ ],
+ [
+ "▁gradini",
+ -13.484305381774902
+ ],
+ [
+ "▁Marilyn",
+ -13.48464584350586
+ ],
+ [
+ "kilometer",
+ -13.484880447387695
+ ],
+ [
+ "tempered",
+ -13.485230445861816
+ ],
+ [
+ "▁intimacy",
+ -13.485371589660645
+ ],
+ [
+ "▁thunderstorm",
+ -13.485373497009277
+ ],
+ [
+ "▁Uttar",
+ -13.485413551330566
+ ],
+ [
+ "▁varnish",
+ -13.485535621643066
+ ],
+ [
+ "opathie",
+ -13.485982894897461
+ ],
+ [
+ "▁școlar",
+ -13.48611068725586
+ ],
+ [
+ "▁raisonnable",
+ -13.486114501953125
+ ],
+ [
+ "proactively",
+ -13.486490249633789
+ ],
+ [
+ "▁gib",
+ -13.486536979675293
+ ],
+ [
+ "▁hospice",
+ -13.48684310913086
+ ],
+ [
+ "▁constă",
+ -13.486896514892578
+ ],
+ [
+ "▁Crescent",
+ -13.48690128326416
+ ],
+ [
+ "▁ambasad",
+ -13.486933708190918
+ ],
+ [
+ "hotărâre",
+ -13.486969947814941
+ ],
+ [
+ "▁fraîche",
+ -13.48709774017334
+ ],
+ [
+ "▁bundesweit",
+ -13.487581253051758
+ ],
+ [
+ "nsbesondere",
+ -13.487812042236328
+ ],
+ [
+ "▁intoarce",
+ -13.487863540649414
+ ],
+ [
+ "▁Schokolade",
+ -13.488319396972656
+ ],
+ [
+ "▁adjective",
+ -13.488319396972656
+ ],
+ [
+ "▁incalzire",
+ -13.488319396972656
+ ],
+ [
+ "▁Qualification",
+ -13.488320350646973
+ ],
+ [
+ "▁Bolivia",
+ -13.488324165344238
+ ],
+ [
+ "▁cruelty",
+ -13.488334655761719
+ ],
+ [
+ "pläne",
+ -13.48834228515625
+ ],
+ [
+ "▁solitude",
+ -13.488354682922363
+ ],
+ [
+ "▁Bosnia",
+ -13.488568305969238
+ ],
+ [
+ "rohr",
+ -13.488643646240234
+ ],
+ [
+ "▁regrette",
+ -13.48877239227295
+ ],
+ [
+ "zusammengestellt",
+ -13.48924732208252
+ ],
+ [
+ "▁Kardashian",
+ -13.489798545837402
+ ],
+ [
+ "▁Picasso",
+ -13.489798545837402
+ ],
+ [
+ "▁unverbindlich",
+ -13.489798545837402
+ ],
+ [
+ "▁Headquarters",
+ -13.489799499511719
+ ],
+ [
+ "métrage",
+ -13.4898099899292
+ ],
+ [
+ "▁Magento",
+ -13.489816665649414
+ ],
+ [
+ "▁exhibitors",
+ -13.489898681640625
+ ],
+ [
+ "utty",
+ -13.490381240844727
+ ],
+ [
+ "▁Fünf",
+ -13.490538597106934
+ ],
+ [
+ "▁Peugeot",
+ -13.490538597106934
+ ],
+ [
+ "▁verdienen",
+ -13.490538597106934
+ ],
+ [
+ "▁absolviert",
+ -13.49053955078125
+ ],
+ [
+ "schutzerklärung",
+ -13.490679740905762
+ ],
+ [
+ "sistemele",
+ -13.49089241027832
+ ],
+ [
+ "▁concrète",
+ -13.491279602050781
+ ],
+ [
+ "▁rhyme",
+ -13.491279602050781
+ ],
+ [
+ "▁Continuous",
+ -13.49128246307373
+ ],
+ [
+ "versprechen",
+ -13.491312026977539
+ ],
+ [
+ "▁Melanie",
+ -13.49202823638916
+ ],
+ [
+ "▁clienţi",
+ -13.492046356201172
+ ],
+ [
+ "luckily",
+ -13.492205619812012
+ ],
+ [
+ "▁counterfeit",
+ -13.492762565612793
+ ],
+ [
+ "▁locomotive",
+ -13.492889404296875
+ ],
+ [
+ "▁reacți",
+ -13.492908477783203
+ ],
+ [
+ "ampered",
+ -13.493005752563477
+ ],
+ [
+ "atenția",
+ -13.493011474609375
+ ],
+ [
+ "Suppose",
+ -13.493062973022461
+ ],
+ [
+ "hinweis",
+ -13.493464469909668
+ ],
+ [
+ "verletzung",
+ -13.493504524230957
+ ],
+ [
+ "▁mănânc",
+ -13.493504524230957
+ ],
+ [
+ "▁provoac",
+ -13.493507385253906
+ ],
+ [
+ "▁regizor",
+ -13.493511199951172
+ ],
+ [
+ "kundig",
+ -13.49352741241455
+ ],
+ [
+ "embarqu",
+ -13.493584632873535
+ ],
+ [
+ "Radio",
+ -13.493690490722656
+ ],
+ [
+ "Ministrul",
+ -13.493896484375
+ ],
+ [
+ "weakened",
+ -13.494214057922363
+ ],
+ [
+ "▁translucent",
+ -13.494247436523438
+ ],
+ [
+ "George",
+ -13.494380950927734
+ ],
+ [
+ "▁bacterii",
+ -13.494402885437012
+ ],
+ [
+ "intervalul",
+ -13.494803428649902
+ ],
+ [
+ "▁vizualiz",
+ -13.494832038879395
+ ],
+ [
+ "▁Feuchtigkeit",
+ -13.494991302490234
+ ],
+ [
+ "▁choisissez",
+ -13.494991302490234
+ ],
+ [
+ "▁plausible",
+ -13.494991302490234
+ ],
+ [
+ "▁perpetu",
+ -13.495122909545898
+ ],
+ [
+ "▁bucati",
+ -13.495194435119629
+ ],
+ [
+ "▁Giovanni",
+ -13.495735168457031
+ ],
+ [
+ "▁bluetooth",
+ -13.495736122131348
+ ],
+ [
+ "▁translating",
+ -13.49573802947998
+ ],
+ [
+ "▁Kyoto",
+ -13.495739936828613
+ ],
+ [
+ "▁homosexual",
+ -13.495745658874512
+ ],
+ [
+ "treabă",
+ -13.495820045471191
+ ],
+ [
+ "ntrepid",
+ -13.495983123779297
+ ],
+ [
+ "▁fachlich",
+ -13.496664047241211
+ ],
+ [
+ "Vaccin",
+ -13.496774673461914
+ ],
+ [
+ "▁Treib",
+ -13.497248649597168
+ ],
+ [
+ "varsity",
+ -13.497272491455078
+ ],
+ [
+ "▁Tavern",
+ -13.497278213500977
+ ],
+ [
+ "▁ensue",
+ -13.497330665588379
+ ],
+ [
+ "flexibel",
+ -13.497971534729004
+ ],
+ [
+ "retrieved",
+ -13.498102188110352
+ ],
+ [
+ "traditionellen",
+ -13.498230934143066
+ ],
+ [
+ "▁circulati",
+ -13.498546600341797
+ ],
+ [
+ "▁Diagnose",
+ -13.498717308044434
+ ],
+ [
+ "▁Strawberry",
+ -13.498717308044434
+ ],
+ [
+ "Societatea",
+ -13.49871826171875
+ ],
+ [
+ "expertise",
+ -13.498849868774414
+ ],
+ [
+ "▁naturii",
+ -13.499464988708496
+ ],
+ [
+ "▁4:1",
+ -13.499515533447266
+ ],
+ [
+ "Frequently",
+ -13.500210762023926
+ ],
+ [
+ "disproportionate",
+ -13.500210762023926
+ ],
+ [
+ "▁LIMITED",
+ -13.500210762023926
+ ],
+ [
+ "▁ancestral",
+ -13.500227928161621
+ ],
+ [
+ "▁Logistik",
+ -13.500237464904785
+ ],
+ [
+ "▁recolt",
+ -13.50042724609375
+ ],
+ [
+ "▁liebevoll",
+ -13.500436782836914
+ ],
+ [
+ "importing",
+ -13.500452041625977
+ ],
+ [
+ "aparatul",
+ -13.500458717346191
+ ],
+ [
+ "poziţia",
+ -13.500564575195312
+ ],
+ [
+ "facerilor",
+ -13.500658988952637
+ ],
+ [
+ "Submitted",
+ -13.50086784362793
+ ],
+ [
+ "ografia",
+ -13.501221656799316
+ ],
+ [
+ "onformément",
+ -13.50168228149414
+ ],
+ [
+ "▁dissemination",
+ -13.501708030700684
+ ],
+ [
+ "afli",
+ -13.501834869384766
+ ],
+ [
+ "luminous",
+ -13.502154350280762
+ ],
+ [
+ "▁draußen",
+ -13.502456665039062
+ ],
+ [
+ "▁Zauber",
+ -13.502535820007324
+ ],
+ [
+ "▁Ibrahim",
+ -13.503207206726074
+ ],
+ [
+ "▁eruption",
+ -13.503216743469238
+ ],
+ [
+ "écrite",
+ -13.50357723236084
+ ],
+ [
+ "avril",
+ -13.503898620605469
+ ],
+ [
+ "Increasing",
+ -13.504171371459961
+ ],
+ [
+ "hingeg",
+ -13.504411697387695
+ ],
+ [
+ "fidelity",
+ -13.504707336425781
+ ],
+ [
+ "étonnant",
+ -13.504707336425781
+ ],
+ [
+ "▁créativité",
+ -13.504707336425781
+ ],
+ [
+ "▁Required",
+ -13.504708290100098
+ ],
+ [
+ "▁Edison",
+ -13.504719734191895
+ ],
+ [
+ "▁Stuhl",
+ -13.504719734191895
+ ],
+ [
+ "outhwestern",
+ -13.506060600280762
+ ],
+ [
+ "▁Beschwerden",
+ -13.506210327148438
+ ],
+ [
+ "▁angajaţi",
+ -13.506210327148438
+ ],
+ [
+ "▁Currency",
+ -13.506211280822754
+ ],
+ [
+ "▁reagiert",
+ -13.506214141845703
+ ],
+ [
+ "Science",
+ -13.506229400634766
+ ],
+ [
+ "hospital",
+ -13.506253242492676
+ ],
+ [
+ "professionellen",
+ -13.50649356842041
+ ],
+ [
+ "▁Trouve",
+ -13.506768226623535
+ ],
+ [
+ "▁utopi",
+ -13.50683307647705
+ ],
+ [
+ "gypte",
+ -13.506928443908691
+ ],
+ [
+ "▁Konsequenz",
+ -13.506962776184082
+ ],
+ [
+ "▁pacienți",
+ -13.506962776184082
+ ],
+ [
+ "▁orizont",
+ -13.506988525390625
+ ],
+ [
+ "Corey",
+ -13.506999015808105
+ ],
+ [
+ "▁quartet",
+ -13.507009506225586
+ ],
+ [
+ "▁Sherlock",
+ -13.50710678100586
+ ],
+ [
+ "▁gagné",
+ -13.507237434387207
+ ],
+ [
+ "▁Jusqu",
+ -13.50732707977295
+ ],
+ [
+ "▁Clickfunnel",
+ -13.507465362548828
+ ],
+ [
+ "Survivor",
+ -13.507716178894043
+ ],
+ [
+ "▁Beethoven",
+ -13.507716178894043
+ ],
+ [
+ "▁Exemplar",
+ -13.507716178894043
+ ],
+ [
+ "▁Gonzalez",
+ -13.507716178894043
+ ],
+ [
+ "▁Illustrator",
+ -13.507716178894043
+ ],
+ [
+ "▁Verpflichtung",
+ -13.507718086242676
+ ],
+ [
+ "Possibly",
+ -13.507719993591309
+ ],
+ [
+ "Maintenant",
+ -13.507721900939941
+ ],
+ [
+ "▁incendiu",
+ -13.507721900939941
+ ],
+ [
+ "▁poêl",
+ -13.507747650146484
+ ],
+ [
+ "▁aşez",
+ -13.507757186889648
+ ],
+ [
+ "phenol",
+ -13.508248329162598
+ ],
+ [
+ "▁magician",
+ -13.508421897888184
+ ],
+ [
+ "éventuellement",
+ -13.508512496948242
+ ],
+ [
+ "▁amortiz",
+ -13.508736610412598
+ ],
+ [
+ "bouchage",
+ -13.50873851776123
+ ],
+ [
+ "▁Accommodation",
+ -13.509223937988281
+ ],
+ [
+ "▁Significant",
+ -13.509223937988281
+ ],
+ [
+ "▁rejoice",
+ -13.509223937988281
+ ],
+ [
+ "▁Lorraine",
+ -13.509224891662598
+ ],
+ [
+ "▁Necklace",
+ -13.509234428405762
+ ],
+ [
+ "▁hamburger",
+ -13.509273529052734
+ ],
+ [
+ "Enhanced",
+ -13.5095796585083
+ ],
+ [
+ "▁Audrey",
+ -13.509978294372559
+ ],
+ [
+ "▁considère",
+ -13.509986877441406
+ ],
+ [
+ "hafen",
+ -13.51050853729248
+ ],
+ [
+ "acordare",
+ -13.510509490966797
+ ],
+ [
+ "▁ediți",
+ -13.51075553894043
+ ],
+ [
+ "▁militia",
+ -13.510767936706543
+ ],
+ [
+ "captivate",
+ -13.510771751403809
+ ],
+ [
+ "▁rebellion",
+ -13.510777473449707
+ ],
+ [
+ "▁veranstalte",
+ -13.510844230651855
+ ],
+ [
+ "▁matelas",
+ -13.510859489440918
+ ],
+ [
+ "originating",
+ -13.510873794555664
+ ],
+ [
+ "Typical",
+ -13.51092529296875
+ ],
+ [
+ "▁législat",
+ -13.511360168457031
+ ],
+ [
+ "▁Kräfte",
+ -13.511488914489746
+ ],
+ [
+ "▁Eigentümer",
+ -13.511489868164062
+ ],
+ [
+ "▁gonfl",
+ -13.511608123779297
+ ],
+ [
+ "dispoziție",
+ -13.512028694152832
+ ],
+ [
+ "▁Fabulous",
+ -13.512246131896973
+ ],
+ [
+ "▁Guillaume",
+ -13.512246131896973
+ ],
+ [
+ "▁Genuine",
+ -13.512247085571289
+ ],
+ [
+ "selbe",
+ -13.512449264526367
+ ],
+ [
+ "(2002)",
+ -13.512616157531738
+ ],
+ [
+ "Einen",
+ -13.512908935546875
+ ],
+ [
+ "▁Snapdragon",
+ -13.513002395629883
+ ],
+ [
+ "▁plagiarism",
+ -13.513002395629883
+ ],
+ [
+ "▁Rendez",
+ -13.513019561767578
+ ],
+ [
+ "▁înregistrare",
+ -13.513033866882324
+ ],
+ [
+ "probiert",
+ -13.513081550598145
+ ],
+ [
+ "gestiegen",
+ -13.513153076171875
+ ],
+ [
+ "Teatrul",
+ -13.513370513916016
+ ],
+ [
+ "trove",
+ -13.513469696044922
+ ],
+ [
+ "ntsprechend",
+ -13.513566017150879
+ ],
+ [
+ "Städten",
+ -13.513691902160645
+ ],
+ [
+ "unforeseen",
+ -13.513760566711426
+ ],
+ [
+ "▁Meridian",
+ -13.513761520385742
+ ],
+ [
+ "▁Ministries",
+ -13.513763427734375
+ ],
+ [
+ "plaît",
+ -13.513769149780273
+ ],
+ [
+ "▁Telefonnummer",
+ -13.513772010803223
+ ],
+ [
+ "welded",
+ -13.513788223266602
+ ],
+ [
+ "pondere",
+ -13.513976097106934
+ ],
+ [
+ "▁funcţiona",
+ -13.514012336730957
+ ],
+ [
+ "▁politicieni",
+ -13.514187812805176
+ ],
+ [
+ "fleck",
+ -13.514240264892578
+ ],
+ [
+ "▁Nitro",
+ -13.514264106750488
+ ],
+ [
+ "wettbewerb",
+ -13.514518737792969
+ ],
+ [
+ "▁ingrijire",
+ -13.514518737792969
+ ],
+ [
+ "▁Gehirn",
+ -13.514521598815918
+ ],
+ [
+ "sigură",
+ -13.514904022216797
+ ],
+ [
+ "400,000",
+ -13.515237808227539
+ ],
+ [
+ "▁cataract",
+ -13.515277862548828
+ ],
+ [
+ "outskirt",
+ -13.515280723571777
+ ],
+ [
+ "▁Identification",
+ -13.515287399291992
+ ],
+ [
+ "▁imperfections",
+ -13.515317916870117
+ ],
+ [
+ "▁Dokumentation",
+ -13.515474319458008
+ ],
+ [
+ "Engine",
+ -13.515851974487305
+ ],
+ [
+ "extindere",
+ -13.516046524047852
+ ],
+ [
+ "bijoux",
+ -13.516797065734863
+ ],
+ [
+ "▁dărui",
+ -13.516802787780762
+ ],
+ [
+ "▁Moderator",
+ -13.516913414001465
+ ],
+ [
+ "biblio",
+ -13.517024040222168
+ ],
+ [
+ "енн",
+ -13.517024040222168
+ ],
+ [
+ "▁Relevan",
+ -13.51728630065918
+ ],
+ [
+ "ansprüche",
+ -13.517557144165039
+ ],
+ [
+ "épaisseur",
+ -13.517580032348633
+ ],
+ [
+ "▁emoţi",
+ -13.517677307128906
+ ],
+ [
+ "exacerbate",
+ -13.518318176269531
+ ],
+ [
+ "▁Wimbledon",
+ -13.518318176269531
+ ],
+ [
+ "▁Pandora",
+ -13.518319129943848
+ ],
+ [
+ "perhaps",
+ -13.518725395202637
+ ],
+ [
+ "certify",
+ -13.518762588500977
+ ],
+ [
+ "Strukturen",
+ -13.5189208984375
+ ],
+ [
+ "▁Kreativität",
+ -13.519079208374023
+ ],
+ [
+ "schlägt",
+ -13.51908016204834
+ ],
+ [
+ "▁certifié",
+ -13.51911735534668
+ ],
+ [
+ "/09/",
+ -13.519211769104004
+ ],
+ [
+ "▁suprafaţ",
+ -13.519493103027344
+ ],
+ [
+ "verständnis",
+ -13.519841194152832
+ ],
+ [
+ "presedintele",
+ -13.519842147827148
+ ],
+ [
+ "▁orthopedic",
+ -13.519842147827148
+ ],
+ [
+ "▁superioara",
+ -13.519843101501465
+ ],
+ [
+ "älteste",
+ -13.519903182983398
+ ],
+ [
+ "▁conducător",
+ -13.520153999328613
+ ],
+ [
+ "supplementary",
+ -13.520243644714355
+ ],
+ [
+ "wetlands",
+ -13.520438194274902
+ ],
+ [
+ "▁suprafete",
+ -13.520605087280273
+ ],
+ [
+ "▁aparțin",
+ -13.520951271057129
+ ],
+ [
+ "analiză",
+ -13.521014213562012
+ ],
+ [
+ "Uneori",
+ -13.52115535736084
+ ],
+ [
+ "Toujours",
+ -13.521368026733398
+ ],
+ [
+ "▁Nairobi",
+ -13.521368026733398
+ ],
+ [
+ "▁asparagus",
+ -13.521368026733398
+ ],
+ [
+ "▁crowdfunding",
+ -13.521368026733398
+ ],
+ [
+ "gutachten",
+ -13.521369934082031
+ ],
+ [
+ "smelling",
+ -13.521659851074219
+ ],
+ [
+ "▁elektrisch",
+ -13.521718978881836
+ ],
+ [
+ "begging",
+ -13.522055625915527
+ ],
+ [
+ "▁Renewable",
+ -13.522896766662598
+ ],
+ [
+ "▁Trouble",
+ -13.522896766662598
+ ],
+ [
+ "▁devastated",
+ -13.522896766662598
+ ],
+ [
+ "▁remplacé",
+ -13.522896766662598
+ ],
+ [
+ "▁schmeckt",
+ -13.522896766662598
+ ],
+ [
+ "▁exerciți",
+ -13.523005485534668
+ ],
+ [
+ "▁vermute",
+ -13.523650169372559
+ ],
+ [
+ "▁Constanța",
+ -13.523661613464355
+ ],
+ [
+ "expunere",
+ -13.523693084716797
+ ],
+ [
+ "▁Fitzgerald",
+ -13.52442741394043
+ ],
+ [
+ "▁Mechanism",
+ -13.524429321289062
+ ],
+ [
+ "▁underscore",
+ -13.524484634399414
+ ],
+ [
+ "poziţie",
+ -13.524901390075684
+ ],
+ [
+ "stöbern",
+ -13.525193214416504
+ ],
+ [
+ "▁littérature",
+ -13.525193214416504
+ ],
+ [
+ "▁împrumut",
+ -13.525193214416504
+ ],
+ [
+ "Vision",
+ -13.525771141052246
+ ],
+ [
+ "▁overwhelm",
+ -13.525773048400879
+ ],
+ [
+ "▁erweitern",
+ -13.525959968566895
+ ],
+ [
+ "skeletal",
+ -13.525960922241211
+ ],
+ [
+ "▁terrified",
+ -13.525960922241211
+ ],
+ [
+ "aggravate",
+ -13.525962829589844
+ ],
+ [
+ "▁Malawi",
+ -13.525969505310059
+ ],
+ [
+ "▁neuroscience",
+ -13.526009559631348
+ ],
+ [
+ "trecută",
+ -13.526097297668457
+ ],
+ [
+ "▁maestr",
+ -13.52634334564209
+ ],
+ [
+ "нов",
+ -13.526555061340332
+ ],
+ [
+ "▁Cobb",
+ -13.52667236328125
+ ],
+ [
+ "▁Schwangerschaft",
+ -13.526727676391602
+ ],
+ [
+ "▁internationaux",
+ -13.526727676391602
+ ],
+ [
+ "▁entspannen",
+ -13.526729583740234
+ ],
+ [
+ "▁Früchte",
+ -13.52676773071289
+ ],
+ [
+ "mâine",
+ -13.526805877685547
+ ],
+ [
+ "stützt",
+ -13.526938438415527
+ ],
+ [
+ "flipped",
+ -13.527076721191406
+ ],
+ [
+ "Palatul",
+ -13.527252197265625
+ ],
+ [
+ "▁Gérard",
+ -13.527496337890625
+ ],
+ [
+ "▁Kensington",
+ -13.527498245239258
+ ],
+ [
+ "chargée",
+ -13.52807331085205
+ ],
+ [
+ "iolo",
+ -13.528203964233398
+ ],
+ [
+ "▁excesiv",
+ -13.52904987335205
+ ],
+ [
+ "▁Gymnas",
+ -13.52962875366211
+ ],
+ [
+ "▁optimise",
+ -13.529678344726562
+ ],
+ [
+ "possibilités",
+ -13.529717445373535
+ ],
+ [
+ "▁periculoas",
+ -13.529810905456543
+ ],
+ [
+ "mechanical",
+ -13.529839515686035
+ ],
+ [
+ "▁confruntă",
+ -13.529868125915527
+ ],
+ [
+ "quatrième",
+ -13.530573844909668
+ ],
+ [
+ "▁Preservation",
+ -13.530573844909668
+ ],
+ [
+ "▁Juventus",
+ -13.530574798583984
+ ],
+ [
+ "vorsitzende",
+ -13.5305757522583
+ ],
+ [
+ "électora",
+ -13.530586242675781
+ ],
+ [
+ "▁fascinant",
+ -13.53061580657959
+ ],
+ [
+ "▁lagoon",
+ -13.530671119689941
+ ],
+ [
+ "referencing",
+ -13.53079605102539
+ ],
+ [
+ "appointed",
+ -13.530988693237305
+ ],
+ [
+ "Audible",
+ -13.531112670898438
+ ],
+ [
+ "sighted",
+ -13.531612396240234
+ ],
+ [
+ "▁gewünscht",
+ -13.532061576843262
+ ],
+ [
+ "▁Expedition",
+ -13.532115936279297
+ ],
+ [
+ "▁genunchi",
+ -13.532115936279297
+ ],
+ [
+ "▁PROVIDE",
+ -13.53211784362793
+ ],
+ [
+ "▁rosemary",
+ -13.532118797302246
+ ],
+ [
+ "▁cleanliness",
+ -13.532130241394043
+ ],
+ [
+ "commanded",
+ -13.53223991394043
+ ],
+ [
+ "ältere",
+ -13.532530784606934
+ ],
+ [
+ "ност",
+ -13.532547950744629
+ ],
+ [
+ "kühlen",
+ -13.532917976379395
+ ],
+ [
+ "mettez",
+ -13.533548355102539
+ ],
+ [
+ "connaitre",
+ -13.533661842346191
+ ],
+ [
+ "Qaeda",
+ -13.533662796020508
+ ],
+ [
+ "▁traumhaft",
+ -13.53366470336914
+ ],
+ [
+ "kommst",
+ -13.533666610717773
+ ],
+ [
+ "▁Abbott",
+ -13.533669471740723
+ ],
+ [
+ "▁Fool",
+ -13.533686637878418
+ ],
+ [
+ "▁médaill",
+ -13.533687591552734
+ ],
+ [
+ "▁genotyp",
+ -13.533693313598633
+ ],
+ [
+ "▁Fälle",
+ -13.53375244140625
+ ],
+ [
+ "▁actuator",
+ -13.533843994140625
+ ],
+ [
+ "CLASS",
+ -13.534042358398438
+ ],
+ [
+ "progressively",
+ -13.534421920776367
+ ],
+ [
+ "negative",
+ -13.53469467163086
+ ],
+ [
+ "bundled",
+ -13.535009384155273
+ ],
+ [
+ "▁dezbatere",
+ -13.535208702087402
+ ],
+ [
+ "kamagra",
+ -13.535237312316895
+ ],
+ [
+ "gardinen",
+ -13.535250663757324
+ ],
+ [
+ "unsecured",
+ -13.535271644592285
+ ],
+ [
+ "Assisted",
+ -13.535298347473145
+ ],
+ [
+ "Gymnasium",
+ -13.535386085510254
+ ],
+ [
+ "▁brusc",
+ -13.535591125488281
+ ],
+ [
+ "prinzip",
+ -13.535655975341797
+ ],
+ [
+ "Torrent",
+ -13.535964965820312
+ ],
+ [
+ "Presented",
+ -13.535967826843262
+ ],
+ [
+ "▁impressionnant",
+ -13.53628921508789
+ ],
+ [
+ "charakter",
+ -13.536758422851562
+ ],
+ [
+ "▁Acoustic",
+ -13.536762237548828
+ ],
+ [
+ "▁appartient",
+ -13.536763191223145
+ ],
+ [
+ "gesteuert",
+ -13.536879539489746
+ ],
+ [
+ "▁condiți",
+ -13.537089347839355
+ ],
+ [
+ "authentic",
+ -13.537313461303711
+ ],
+ [
+ "▁Erholung",
+ -13.537534713745117
+ ],
+ [
+ "▁Veranstalter",
+ -13.537534713745117
+ ],
+ [
+ "▁Filial",
+ -13.537665367126465
+ ],
+ [
+ "ruhigen",
+ -13.537714958190918
+ ],
+ [
+ "symptôme",
+ -13.538311004638672
+ ],
+ [
+ "▁Efficiency",
+ -13.538311004638672
+ ],
+ [
+ "▁stunned",
+ -13.538311004638672
+ ],
+ [
+ "▁sympathique",
+ -13.538311004638672
+ ],
+ [
+ "Uploaded",
+ -13.538352966308594
+ ],
+ [
+ "▁geistig",
+ -13.538453102111816
+ ],
+ [
+ "Pläne",
+ -13.538509368896484
+ ],
+ [
+ "▁Apartament",
+ -13.53855037689209
+ ],
+ [
+ "▁ușoar",
+ -13.539119720458984
+ ],
+ [
+ "▁locuinț",
+ -13.539122581481934
+ ],
+ [
+ "épouse",
+ -13.539166450500488
+ ],
+ [
+ "îngrijire",
+ -13.539215087890625
+ ],
+ [
+ "Obtain",
+ -13.539261817932129
+ ],
+ [
+ "Detect",
+ -13.539590835571289
+ ],
+ [
+ "▁Dumitru",
+ -13.539865493774414
+ ],
+ [
+ "▁refrigeration",
+ -13.539865493774414
+ ],
+ [
+ "ärztliche",
+ -13.539881706237793
+ ],
+ [
+ "efficiency",
+ -13.540032386779785
+ ],
+ [
+ "▁snail",
+ -13.540328979492188
+ ],
+ [
+ "gelände",
+ -13.540419578552246
+ ],
+ [
+ "expected",
+ -13.540620803833008
+ ],
+ [
+ "kompetenz",
+ -13.540643692016602
+ ],
+ [
+ "▁sfânt",
+ -13.540643692016602
+ ],
+ [
+ "océan",
+ -13.540685653686523
+ ],
+ [
+ "▁Plasma",
+ -13.540717124938965
+ ],
+ [
+ "▁vulgar",
+ -13.54075813293457
+ ],
+ [
+ "▁slump",
+ -13.541083335876465
+ ],
+ [
+ "autoimmune",
+ -13.541422843933105
+ ],
+ [
+ "▁Cynthia",
+ -13.541422843933105
+ ],
+ [
+ "▁dimineaţ",
+ -13.541422843933105
+ ],
+ [
+ "▁whimsical",
+ -13.541422843933105
+ ],
+ [
+ "▁evaporate",
+ -13.541488647460938
+ ],
+ [
+ "▁calorii",
+ -13.54186725616455
+ ],
+ [
+ "portion",
+ -13.54187297821045
+ ],
+ [
+ "crowned",
+ -13.5419282913208
+ ],
+ [
+ "▁întâmpin",
+ -13.54220199584961
+ ],
+ [
+ "▁Centenar",
+ -13.542620658874512
+ ],
+ [
+ "▁Genehmigung",
+ -13.54298210144043
+ ],
+ [
+ "▁Wahrscheinlich",
+ -13.54298210144043
+ ],
+ [
+ "▁accompaniment",
+ -13.54298210144043
+ ],
+ [
+ "▁Negoti",
+ -13.542984962463379
+ ],
+ [
+ "▁Vanilla",
+ -13.543000221252441
+ ],
+ [
+ "▁Receiv",
+ -13.543014526367188
+ ],
+ [
+ "▁bestseller",
+ -13.543052673339844
+ ],
+ [
+ "tendons",
+ -13.543069839477539
+ ],
+ [
+ "Reilly",
+ -13.543192863464355
+ ],
+ [
+ "▁refroidi",
+ -13.543731689453125
+ ],
+ [
+ "▁überrascht",
+ -13.543763160705566
+ ],
+ [
+ "Gitarre",
+ -13.543828964233398
+ ],
+ [
+ "wände",
+ -13.544173240661621
+ ],
+ [
+ "veniturile",
+ -13.544321060180664
+ ],
+ [
+ "▁portofoliu",
+ -13.54454517364502
+ ],
+ [
+ "▁temporaire",
+ -13.54454517364502
+ ],
+ [
+ "▁Dawson",
+ -13.544546127319336
+ ],
+ [
+ "foreseeable",
+ -13.544547080993652
+ ],
+ [
+ "▁Gastgeber",
+ -13.545344352722168
+ ],
+ [
+ "Access",
+ -13.545432090759277
+ ],
+ [
+ "▁Defender",
+ -13.545537948608398
+ ],
+ [
+ "▁Quarry",
+ -13.546109199523926
+ ],
+ [
+ "▁trolley",
+ -13.546110153198242
+ ],
+ [
+ "▁carburant",
+ -13.546111106872559
+ ],
+ [
+ "▁titluri",
+ -13.54631233215332
+ ],
+ [
+ "comparatively",
+ -13.546327590942383
+ ],
+ [
+ "nachfolgend",
+ -13.54659652709961
+ ],
+ [
+ "anfang",
+ -13.546740531921387
+ ],
+ [
+ "▁faszinieren",
+ -13.546891212463379
+ ],
+ [
+ "trăiesc",
+ -13.547082901000977
+ ],
+ [
+ "▁Travail",
+ -13.547159194946289
+ ],
+ [
+ "Contact",
+ -13.547235488891602
+ ],
+ [
+ "fashion",
+ -13.547245025634766
+ ],
+ [
+ "▁épais",
+ -13.547585487365723
+ ],
+ [
+ "plattform",
+ -13.547676086425781
+ ],
+ [
+ "ventricular",
+ -13.547677040100098
+ ],
+ [
+ "▁Portsmouth",
+ -13.547677993774414
+ ],
+ [
+ "▁împărat",
+ -13.54767894744873
+ ],
+ [
+ "▁vândut",
+ -13.547698020935059
+ ],
+ [
+ "▁evidenț",
+ -13.547708511352539
+ ],
+ [
+ "Purchasing",
+ -13.547877311706543
+ ],
+ [
+ "discerning",
+ -13.54804801940918
+ ],
+ [
+ "odonti",
+ -13.548080444335938
+ ],
+ [
+ "distilled",
+ -13.548316955566406
+ ],
+ [
+ "saveur",
+ -13.548447608947754
+ ],
+ [
+ "▁récompense",
+ -13.54845905303955
+ ],
+ [
+ "confortul",
+ -13.548552513122559
+ ],
+ [
+ "arbeitete",
+ -13.548787117004395
+ ],
+ [
+ "partenerii",
+ -13.549064636230469
+ ],
+ [
+ "mirrored",
+ -13.54908561706543
+ ],
+ [
+ "Dienstleister",
+ -13.549243927001953
+ ],
+ [
+ "▁Jakarta",
+ -13.549243927001953
+ ],
+ [
+ "▁WEBSITE",
+ -13.549243927001953
+ ],
+ [
+ "▁Acquisition",
+ -13.549262046813965
+ ],
+ [
+ "▁Miranda",
+ -13.549287796020508
+ ],
+ [
+ "Syndic",
+ -13.549356460571289
+ ],
+ [
+ "▁stadiu",
+ -13.549450874328613
+ ],
+ [
+ "▁Parchet",
+ -13.549498558044434
+ ],
+ [
+ "Générale",
+ -13.54954719543457
+ ],
+ [
+ "▁jpl",
+ -13.549579620361328
+ ],
+ [
+ "attainable",
+ -13.549949645996094
+ ],
+ [
+ "École",
+ -13.550041198730469
+ ],
+ [
+ "Sphere",
+ -13.550538063049316
+ ],
+ [
+ "obtainable",
+ -13.550592422485352
+ ],
+ [
+ "▁Sapphire",
+ -13.55081558227539
+ ],
+ [
+ "▁aérienne",
+ -13.55081558227539
+ ],
+ [
+ "▁bărbați",
+ -13.55081558227539
+ ],
+ [
+ "▁irritating",
+ -13.55081558227539
+ ],
+ [
+ "▁ultraviolet",
+ -13.550816535949707
+ ],
+ [
+ "untouched",
+ -13.550817489624023
+ ],
+ [
+ "▁Ramsey",
+ -13.550819396972656
+ ],
+ [
+ "titres",
+ -13.551087379455566
+ ],
+ [
+ "▁Coordinat",
+ -13.551218032836914
+ ],
+ [
+ "believable",
+ -13.551358222961426
+ ],
+ [
+ "▁Grundsätzlich",
+ -13.551602363586426
+ ],
+ [
+ "▁konsequent",
+ -13.551602363586426
+ ],
+ [
+ "▁Cerceta",
+ -13.551909446716309
+ ],
+ [
+ "dirigé",
+ -13.552116394042969
+ ],
+ [
+ "▁disturb",
+ -13.552151679992676
+ ],
+ [
+ "conciliation",
+ -13.552210807800293
+ ],
+ [
+ "▁gelöscht",
+ -13.552390098571777
+ ],
+ [
+ "▁sauvegarde",
+ -13.552391052246094
+ ],
+ [
+ "▁cavities",
+ -13.552393913269043
+ ],
+ [
+ "stunde",
+ -13.55241584777832
+ ],
+ [
+ "▁foloseasc",
+ -13.552430152893066
+ ],
+ [
+ "▁simpati",
+ -13.552873611450195
+ ],
+ [
+ "Chacun",
+ -13.553032875061035
+ ],
+ [
+ "adversaire",
+ -13.553178787231445
+ ],
+ [
+ "Eigentlich",
+ -13.55319881439209
+ ],
+ [
+ "defense",
+ -13.553593635559082
+ ],
+ [
+ "consider",
+ -13.553672790527344
+ ],
+ [
+ "▁Trinidad",
+ -13.553966522216797
+ ],
+ [
+ "▁strategist",
+ -13.553966522216797
+ ],
+ [
+ "distorted",
+ -13.553967475891113
+ ],
+ [
+ "▁hypothetical",
+ -13.553967475891113
+ ],
+ [
+ "▁ramburs",
+ -13.55396842956543
+ ],
+ [
+ "▁Mallorca",
+ -13.553970336914062
+ ],
+ [
+ "▁Domino",
+ -13.554018020629883
+ ],
+ [
+ "arrondissement",
+ -13.554756164550781
+ ],
+ [
+ "konferenz",
+ -13.554756164550781
+ ],
+ [
+ "▁Beleuchtung",
+ -13.554756164550781
+ ],
+ [
+ "aggregat",
+ -13.55484676361084
+ ],
+ [
+ "subsidize",
+ -13.554896354675293
+ ],
+ [
+ "shri",
+ -13.555503845214844
+ ],
+ [
+ "Kaufentscheidung",
+ -13.555545806884766
+ ],
+ [
+ "▁Hernandez",
+ -13.555545806884766
+ ],
+ [
+ "▁Upholster",
+ -13.555546760559082
+ ],
+ [
+ "atlantic",
+ -13.555614471435547
+ ],
+ [
+ "▁locuinte",
+ -13.555652618408203
+ ],
+ [
+ "integrates",
+ -13.55583381652832
+ ],
+ [
+ "ewusst",
+ -13.555878639221191
+ ],
+ [
+ "▁Avocado",
+ -13.556337356567383
+ ],
+ [
+ "Decorative",
+ -13.557014465332031
+ ],
+ [
+ "▁Corinthians",
+ -13.557127952575684
+ ],
+ [
+ "▁clădire",
+ -13.557127952575684
+ ],
+ [
+ "▁plomberie",
+ -13.557127952575684
+ ],
+ [
+ "vases",
+ -13.557143211364746
+ ],
+ [
+ "▁crippl",
+ -13.557247161865234
+ ],
+ [
+ "cluttered",
+ -13.557487487792969
+ ],
+ [
+ "departed",
+ -13.557807922363281
+ ],
+ [
+ "▁entscheidet",
+ -13.5579195022583
+ ],
+ [
+ "Certaine",
+ -13.558243751525879
+ ],
+ [
+ "honda",
+ -13.558294296264648
+ ],
+ [
+ "triggering",
+ -13.558527946472168
+ ],
+ [
+ "▁Erdogan",
+ -13.558712005615234
+ ],
+ [
+ "▁Widerstand",
+ -13.558712005615234
+ ],
+ [
+ "▁Bhutan",
+ -13.558713912963867
+ ],
+ [
+ "▁ascunde",
+ -13.558736801147461
+ ],
+ [
+ "▁shading",
+ -13.558748245239258
+ ],
+ [
+ "behavioural",
+ -13.559172630310059
+ ],
+ [
+ "▁transfér",
+ -13.55960750579834
+ ],
+ [
+ "versichert",
+ -13.559623718261719
+ ],
+ [
+ "▁vinovat",
+ -13.559646606445312
+ ],
+ [
+ "▁airfare",
+ -13.560142517089844
+ ],
+ [
+ "▁simplistic",
+ -13.56030559539795
+ ],
+ [
+ "▁Asigura",
+ -13.560320854187012
+ ],
+ [
+ "Chauffe",
+ -13.560480117797852
+ ],
+ [
+ "scrisă",
+ -13.560585975646973
+ ],
+ [
+ "trouvez",
+ -13.560702323913574
+ ],
+ [
+ "greasy",
+ -13.560709953308105
+ ],
+ [
+ "bottled",
+ -13.560809135437012
+ ],
+ [
+ "grouped",
+ -13.560934066772461
+ ],
+ [
+ "▁beeinflussen",
+ -13.561092376708984
+ ],
+ [
+ "▁chronological",
+ -13.561114311218262
+ ],
+ [
+ "(2000)",
+ -13.56127643585205
+ ],
+ [
+ "sheltered",
+ -13.561298370361328
+ ],
+ [
+ "Historically",
+ -13.561931610107422
+ ],
+ [
+ "piled",
+ -13.562012672424316
+ ],
+ [
+ "publicate",
+ -13.562378883361816
+ ],
+ [
+ "▁étudié",
+ -13.56268310546875
+ ],
+ [
+ "▁vertraut",
+ -13.562688827514648
+ ],
+ [
+ "▁Anpassung",
+ -13.562697410583496
+ ],
+ [
+ "cifra",
+ -13.562705993652344
+ ],
+ [
+ "▁recueil",
+ -13.562762260437012
+ ],
+ [
+ "enforceable",
+ -13.563183784484863
+ ],
+ [
+ "Distinguished",
+ -13.56347942352295
+ ],
+ [
+ "Empfänger",
+ -13.56347942352295
+ ],
+ [
+ "▁Acrylic",
+ -13.56347942352295
+ ],
+ [
+ "▁Encyclopedia",
+ -13.56347942352295
+ ],
+ [
+ "▁proaspete",
+ -13.56347942352295
+ ],
+ [
+ "▁unrealistic",
+ -13.56347942352295
+ ],
+ [
+ "▁Assignment",
+ -13.563481330871582
+ ],
+ [
+ "▁incubator",
+ -13.563491821289062
+ ],
+ [
+ "▁unilateral",
+ -13.563501358032227
+ ],
+ [
+ "elasticity",
+ -13.564398765563965
+ ],
+ [
+ "amintim",
+ -13.564475059509277
+ ],
+ [
+ "fournit",
+ -13.564553260803223
+ ],
+ [
+ "semblent",
+ -13.564763069152832
+ ],
+ [
+ "▁$69.",
+ -13.56496524810791
+ ],
+ [
+ "▁prominence",
+ -13.56507396697998
+ ],
+ [
+ "Übertragung",
+ -13.565075874328613
+ ],
+ [
+ "▁2014-11-",
+ -13.565075874328613
+ ],
+ [
+ "▁Giurgiu",
+ -13.565104484558105
+ ],
+ [
+ "étendue",
+ -13.565123558044434
+ ],
+ [
+ "ceputul",
+ -13.565187454223633
+ ],
+ [
+ "Schwierigkeiten",
+ -13.565872192382812
+ ],
+ [
+ "▁subtract",
+ -13.565881729125977
+ ],
+ [
+ "▁gesichert",
+ -13.56589126586914
+ ],
+ [
+ "▁uimit",
+ -13.565925598144531
+ ],
+ [
+ "▁mensuel",
+ -13.565967559814453
+ ],
+ [
+ "Vorgaben",
+ -13.566215515136719
+ ],
+ [
+ "▁legitimacy",
+ -13.566670417785645
+ ],
+ [
+ "▁Kendall",
+ -13.566673278808594
+ ],
+ [
+ "▁détach",
+ -13.566790580749512
+ ],
+ [
+ "▁kennenlernen",
+ -13.567469596862793
+ ],
+ [
+ "▁gewöhnlich",
+ -13.56747055053711
+ ],
+ [
+ "Octav",
+ -13.567917823791504
+ ],
+ [
+ "responsive",
+ -13.568169593811035
+ ],
+ [
+ "▁Mängel",
+ -13.568269729614258
+ ],
+ [
+ "▁mișcare",
+ -13.568269729614258
+ ],
+ [
+ "▁ludique",
+ -13.568270683288574
+ ],
+ [
+ "▁Exeter",
+ -13.568324089050293
+ ],
+ [
+ "▁respins",
+ -13.569114685058594
+ ],
+ [
+ "oraşului",
+ -13.569173812866211
+ ],
+ [
+ "▁sfârşit",
+ -13.56949520111084
+ ],
+ [
+ "BUSINESS",
+ -13.56987190246582
+ ],
+ [
+ "illustrating",
+ -13.56987190246582
+ ],
+ [
+ "▁Tottenham",
+ -13.56987190246582
+ ],
+ [
+ "▁pruning",
+ -13.569886207580566
+ ],
+ [
+ "▁Înainte",
+ -13.569904327392578
+ ],
+ [
+ "▁interesel",
+ -13.570096969604492
+ ],
+ [
+ "discovered",
+ -13.57031536102295
+ ],
+ [
+ "(0)",
+ -13.570572853088379
+ ],
+ [
+ "▁Bewerber",
+ -13.570673942565918
+ ],
+ [
+ "▁DESIGN",
+ -13.570673942565918
+ ],
+ [
+ "▁Orientierung",
+ -13.570686340332031
+ ],
+ [
+ "library",
+ -13.571041107177734
+ ],
+ [
+ "cheltuielile",
+ -13.571419715881348
+ ],
+ [
+ "▁Canterbury",
+ -13.571475982666016
+ ],
+ [
+ "▁intellectuelle",
+ -13.571477890014648
+ ],
+ [
+ "▁amalgam",
+ -13.571497917175293
+ ],
+ [
+ "▁Toledo",
+ -13.57150650024414
+ ],
+ [
+ "gezahlt",
+ -13.571531295776367
+ ],
+ [
+ "Veronica",
+ -13.571659088134766
+ ],
+ [
+ "deleting",
+ -13.571946144104004
+ ],
+ [
+ "▁Merlin",
+ -13.572442054748535
+ ],
+ [
+ "▁opérationnel",
+ -13.572554588317871
+ ],
+ [
+ "schmutz",
+ -13.572568893432617
+ ],
+ [
+ "hyroid",
+ -13.57279109954834
+ ],
+ [
+ "▁Compatible",
+ -13.57308292388916
+ ],
+ [
+ "▁Leopard",
+ -13.57308292388916
+ ],
+ [
+ "▁cylindrical",
+ -13.57308292388916
+ ],
+ [
+ "▁terrestrial",
+ -13.57308292388916
+ ],
+ [
+ "conferencing",
+ -13.573088645935059
+ ],
+ [
+ "▁Variety",
+ -13.573097229003906
+ ],
+ [
+ "▁Screw",
+ -13.573164939880371
+ ],
+ [
+ "character",
+ -13.573637962341309
+ ],
+ [
+ "shortened",
+ -13.573643684387207
+ ],
+ [
+ "▁întrerup",
+ -13.573736190795898
+ ],
+ [
+ "freude",
+ -13.573884010314941
+ ],
+ [
+ "▁dezbateri",
+ -13.573887825012207
+ ],
+ [
+ "viteză",
+ -13.574563026428223
+ ],
+ [
+ "formațiile",
+ -13.574600219726562
+ ],
+ [
+ "▁responsibly",
+ -13.574692726135254
+ ],
+ [
+ "Dimensiuni",
+ -13.574695587158203
+ ],
+ [
+ "Arrangement",
+ -13.57469654083252
+ ],
+ [
+ "▁Leisure",
+ -13.574712753295898
+ ],
+ [
+ "escaping",
+ -13.5750732421875
+ ],
+ [
+ "flexion",
+ -13.575104713439941
+ ],
+ [
+ "▁religieuse",
+ -13.575308799743652
+ ],
+ [
+ "crystalline",
+ -13.575457572937012
+ ],
+ [
+ "▁clasp",
+ -13.575520515441895
+ ],
+ [
+ "festigt",
+ -13.57554817199707
+ ],
+ [
+ "▁trouvai",
+ -13.57596206665039
+ ],
+ [
+ "cutaneous",
+ -13.576305389404297
+ ],
+ [
+ "▁carcinoma",
+ -13.576305389404297
+ ],
+ [
+ "▁juxtapos",
+ -13.576305389404297
+ ],
+ [
+ "assemblage",
+ -13.576306343078613
+ ],
+ [
+ "▁Messiah",
+ -13.576306343078613
+ ],
+ [
+ "▁Sleeve",
+ -13.576306343078613
+ ],
+ [
+ "▁șofer",
+ -13.576386451721191
+ ],
+ [
+ "/05/",
+ -13.57666301727295
+ ],
+ [
+ "▁expoziți",
+ -13.576703071594238
+ ],
+ [
+ "▁pătrun",
+ -13.577343940734863
+ ],
+ [
+ "▁Lydia",
+ -13.57739543914795
+ ],
+ [
+ "▁grădini",
+ -13.577919006347656
+ ],
+ [
+ "▁toothpaste",
+ -13.577919960021973
+ ],
+ [
+ "ordained",
+ -13.577921867370605
+ ],
+ [
+ "▁Renovation",
+ -13.577922821044922
+ ],
+ [
+ "voicing",
+ -13.578327178955078
+ ],
+ [
+ "président",
+ -13.578595161437988
+ ],
+ [
+ "▁gestartet",
+ -13.578728675842285
+ ],
+ [
+ "Multi",
+ -13.579121589660645
+ ],
+ [
+ "itinéraire",
+ -13.579537391662598
+ ],
+ [
+ "▁influenza",
+ -13.579537391662598
+ ],
+ [
+ "▁psychiatrist",
+ -13.579537391662598
+ ],
+ [
+ "▁schizophrenia",
+ -13.579537391662598
+ ],
+ [
+ "▁Magnolia",
+ -13.57953929901123
+ ],
+ [
+ "▁Scottsdale",
+ -13.579541206359863
+ ],
+ [
+ "▁interessieren",
+ -13.579548835754395
+ ],
+ [
+ "▁asfalt",
+ -13.579643249511719
+ ],
+ [
+ "▁Journalism",
+ -13.57977294921875
+ ],
+ [
+ "Multe",
+ -13.580089569091797
+ ],
+ [
+ "Westfalen",
+ -13.580347061157227
+ ],
+ [
+ "▁Vorschriften",
+ -13.580348014831543
+ ],
+ [
+ "Angleterre",
+ -13.58034896850586
+ ],
+ [
+ "sustainable",
+ -13.580354690551758
+ ],
+ [
+ "▁Retour",
+ -13.580589294433594
+ ],
+ [
+ "▁pâr",
+ -13.5809965133667
+ ],
+ [
+ "steigert",
+ -13.581120491027832
+ ],
+ [
+ "▁AMAZING",
+ -13.581157684326172
+ ],
+ [
+ "▁turbulent",
+ -13.581157684326172
+ ],
+ [
+ "costing",
+ -13.58155345916748
+ ],
+ [
+ "▁Carolyn",
+ -13.581634521484375
+ ],
+ [
+ "utti",
+ -13.581802368164062
+ ],
+ [
+ "dürftig",
+ -13.581968307495117
+ ],
+ [
+ "Keep",
+ -13.582038879394531
+ ],
+ [
+ "▁Théâtre",
+ -13.582780838012695
+ ],
+ [
+ "▁combustibil",
+ -13.582780838012695
+ ],
+ [
+ "▁halloween",
+ -13.582780838012695
+ ],
+ [
+ "▁emulator",
+ -13.582785606384277
+ ],
+ [
+ "▁povești",
+ -13.582785606384277
+ ],
+ [
+ "broyeur",
+ -13.582810401916504
+ ],
+ [
+ "▁émerg",
+ -13.582927703857422
+ ],
+ [
+ "overwhelmingly",
+ -13.583025932312012
+ ],
+ [
+ "regulă",
+ -13.583124160766602
+ ],
+ [
+ "goutte",
+ -13.583125114440918
+ ],
+ [
+ "▁Fertigung",
+ -13.583593368530273
+ ],
+ [
+ "constituted",
+ -13.584304809570312
+ ],
+ [
+ "▁QuickBooks",
+ -13.584406852722168
+ ],
+ [
+ "▁genealogy",
+ -13.584407806396484
+ ],
+ [
+ "▁laundering",
+ -13.584432601928711
+ ],
+ [
+ "▁échéan",
+ -13.584491729736328
+ ],
+ [
+ "Account",
+ -13.584601402282715
+ ],
+ [
+ "oyons",
+ -13.584792137145996
+ ],
+ [
+ "nitro",
+ -13.584905624389648
+ ],
+ [
+ "▁corespund",
+ -13.585219383239746
+ ],
+ [
+ "▁suggér",
+ -13.58527660369873
+ ],
+ [
+ "manipulated",
+ -13.585348129272461
+ ],
+ [
+ "deseori",
+ -13.585817337036133
+ ],
+ [
+ "permeabil",
+ -13.585912704467773
+ ],
+ [
+ "Australia",
+ -13.58594799041748
+ ],
+ [
+ "▁Erasmus",
+ -13.586034774780273
+ ],
+ [
+ "▁disrespect",
+ -13.586034774780273
+ ],
+ [
+ "▁trimestre",
+ -13.586038589477539
+ ],
+ [
+ "▁emanat",
+ -13.586103439331055
+ ],
+ [
+ "Schraub",
+ -13.58624267578125
+ ],
+ [
+ "distinctly",
+ -13.586319923400879
+ ],
+ [
+ "Germain",
+ -13.586637496948242
+ ],
+ [
+ "▁pedepse",
+ -13.5868501663208
+ ],
+ [
+ "réglage",
+ -13.5868558883667
+ ],
+ [
+ "făcute",
+ -13.587308883666992
+ ],
+ [
+ "▁garanteaz",
+ -13.587434768676758
+ ],
+ [
+ "▁unterlieg",
+ -13.587701797485352
+ ],
+ [
+ "▁cheddar",
+ -13.587712287902832
+ ],
+ [
+ "▁refugi",
+ -13.587756156921387
+ ],
+ [
+ "▁inférieur",
+ -13.587836265563965
+ ],
+ [
+ "dimension",
+ -13.588440895080566
+ ],
+ [
+ "▁erkennt",
+ -13.588570594787598
+ ],
+ [
+ "amitié",
+ -13.588632583618164
+ ],
+ [
+ "▁predominant",
+ -13.588680267333984
+ ],
+ [
+ "nourishe",
+ -13.588800430297852
+ ],
+ [
+ "exerce",
+ -13.588907241821289
+ ],
+ [
+ "▁disguise",
+ -13.589225769042969
+ ],
+ [
+ "▁traditi",
+ -13.589289665222168
+ ],
+ [
+ "▁Intellectual",
+ -13.5892972946167
+ ],
+ [
+ "▁imunitar",
+ -13.589299201965332
+ ],
+ [
+ "▁Cushion",
+ -13.589300155639648
+ ],
+ [
+ "▁erwachsene",
+ -13.589517593383789
+ ],
+ [
+ "▁Internațional",
+ -13.590115547180176
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "",
+ 0.0
+ ],
+ [
+ "